%% -*- mode: erlang; indent-tabs-mode: nil; -*- %%============================================================================= %% Copyright 2014 Ulf Wiger %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %%============================================================================= -module(setup_gen). -export([main/1, % escript-style run/1, % when called from within erlang help/0]). % prints help text. -import(setup_lib, [abort/2]). -define(if_verbose(Expr), case get(verbose) of true -> Expr; _ -> ok end). main([]) -> help(), halt(1); main([H]) when H=="-h"; H=="-help" -> help(), halt(0); main(["-" ++ _|_] = Args) -> Opts = try options(Args) catch error:E -> abort(E, []) end, run(Opts); main([Name, Config, Out| InArgs]) -> Opts = try options(InArgs) catch error:E -> abort(E, []) end, run([{name, Name}, {conf, Config}, {outdir, Out} | Opts]). help() -> setup_lib:help(). %% @spec run(Options) -> ok %% @doc Generates .rel file(s) and boot scripts for a given configuration. %% %% This function reads a configuration specification and generates the %% files needed to start a node from an OTP boot script. Optionally, it can %% also generate a 'setup' script, which contains the same applications, but %% only loaded (except the `setup' application, if present, which is started). %% This way, a node can be started with all paths set, and all environment %% variables defined, such that a database can be created, and other setup %% tasks be performed. %% %% Mandatory options: %% * `{name, Name}' - Name of the release (and of the .rel and .script files) %% * `{outdir, Dir}' - Where to put the generated files. Dir is created if not %% already present. %% * `{conf, Conf}' - Config file listing apps and perhaps other options %% * `{relconf, File}' - can be used instead of `conf', and identifies a %% reltool.config file (see {@link //reltool. `reltool'}) to be used as %% system description. If a `conf' option is present, it will be used; %% otherwise, a `relconf' option must be present. %% %% Additional options: %% %% * `{apps, [App]}' - List of applications to include in the release. Only the %% first instance of this option is considered. %% * `{add_apps, [App]}' - Adds applications to the ones given in the `apps' %% option. %% * `{remove_apps, Apps}' - Remove `Apps' from the list of applications. %% * `{sort_app, App, Before}' - Change the sort order so that `App' comes %% before `Before'. `Before' can be either an application %% name or a list of names. In the latter case, `App' %% is inserted before either of the applications in %% the list, whichever comes first. %% * `{include, ConfigFile}' - include options from the given file. The file %% is processed using `file:script/2'. %% * `{include, ConfigFile, Vars}' - as above, but passing along a list of %% variable bindings to ConfigFile. %% * `{include_lib, ConfigFile}' - As above, but ConfigFile is named as with %% the `-include_lib(...)' directive in Erlang source code. %% * `{include_lib, ConfigFile, Vars}' - as above, but passing along a list of %% variable bindings to ConfigFile. %% * `{sys, SysConfigFile}' - Read an existing sys.config file. The environment %% found in this file may be redefined by `env' and `set_env' entries %% (see below). %% * `{env, [{App, [{K,V}]}]}' - Environment variables for the `sys.config' %% file. `setup_gen' will merge all `env' entries, where later entries %% replace earlier entries (based on the environment variable name). %% * `{set_env, [{App, [{KeyPath, V}]}]}' - Modifies existing environment %% structures, where `KeyPath' is a list of names (top name must be %% an atom) describing a path in a tree structure, where each node %% is either a `{Key, SubTree}' or a `{Key, Any, SubTree}' tuple. The %% `set_env' function will continue into `SubTree' and either replace %% the value representing the full `KeyPath' or create the remaining %% subtree. %% * `{target, Dir}' - Where to produce the generated files. The files will %% end up in `Dir/releases/Vsn/'. If a `reltool.config' file is used, %% the `{target_dir, D}' option will be translated into `{target,D}'. %% * `{vsn, Vsn}' - System version, used to determine where to generate the %% files (see `target' above). %% * `{root, RootDir}' - Where to look for applications. Normally, `RootDir' %% should represent either `RootDir/lib/*/ebin', or `RootDir/*/ebin', %% but if the option `{wild_roots,true}' is given, it can be either %% an "ebin" directory, or any parent directory to "ebin" directories. %% Multiple `root' options can be given. If `target' is not given %% "boot variables" will be generated for each root directory in turn, %% named `V1 ... Vn', then generating a relocatable boot script. %% * `{pa, Path}' - Prepends `Path' to the code path. Multiple `pa' options %% can be given. %% * `{pz, Path}' - Appends `Path' to the code path. Multiple `pz' options %% can be given. %% * `{install, true|false}' - Tells setup whether to also build "install" %% scripts and config files. An install script contains the same %% applications as the normal script, but only loads them, starting %% only the `setup' application. This allows a system to be installed %% using "setup hooks", while having all the target system code %% and environment available. An "install.config" file is also created, %% which, if a `{nodes, Ns}' option is given, also configures Erlang %% to wait for all given nodes, and then start the `setup' application %% on the first node. %% * `{start_setup, true|false}' - Tells whether setup should be started %% automatically. The default is `true' (as it should be). The best way %% to include setup, but not start it, would be to add `{setup, load}' to %% the `apps' list. %% * `{verbose, true|false}' - (Default: `false') Turns on verbose printouts. %% %% == Application entries == %% %% Applications can be represented in a number of different ways: %% * `AppName::atom()' - `setup' will search for the latest version %% along the current code path. %% * `{App::atom(), Vsn::latest|list()}' - where `Vsn' is an explicit version %% identifying the application. `latest' instructs `setup' to pick the %% latest version, if several versions can be found along the path. %% * `{App::atom(), Type::atom()}' - where %% `Type::permanent|temporary|transient|load' is the application start %% type (or, in the case of 'load', no start at all). %% * `{App, Vsn, Type}' - see `App', `Vsn' and `Type' above %% * `{App, Vsn, Incl}' - where `Incl' is a list of included applications. %% * `{App, Vsn, Type, Incl}' %% %% == Command-line options == %% %% The following options can be given on the command line of `setup_gen': %% * `-target Dir' - Equivalent to `{target, Dir}' %% * `-name Name' - Equivalent to `{name, Name}' %% * `-root Dir' - Equivalent to `{root, Dir}' %% * `-out Dir' - Equivalent to `{outdir, Dir}' %% * `-relconf F' - Equivalent to `{relconf, F}' %% * `-conf F' - Equivalent to `{conf, F}' %% * `-install' - Equivalent to `{install, true}' %% * `-sys F' - Equivalent to `{sys, F}' %% * `-vsn V' - Equivalent to `{vsn, V}' %% * `-pa Dir' - Equivalent to `{pa, Dir}' %% * `-pz Dir' - Equivalent to `{pa, Dir}' %% * `-v' - Equivalent to `{verbose, true}' %% %% == Config File evaluation == %% %% `setup' uses a customized version of `file:script()'. The return value %% from the script will be treated as a list of instructions to `setup'. %% Currently, a pseudo-local function, `b()' allows the script to inspect %% the current variable bindings, and using instructions like %% `{include, ConfigFile, Vars}', variables can be passed along to helper %% scripts. Using the pattern `{include, ConfigFile, [{Key, Value}|b()]}', %% all current variables can be passed to the helper script. Note that %% `setup' may override the values of variables `Name', `SCRIPT', `CWD' %% and `OPTIONS'. Specifically, these variables are bound to: %% %% * `Name': the name of the system being installed %% * `SCRIPT': the (absolute) name of the script currently being evaluated %% * `CWD': the current working directory when setup_gen was invoked %% * `OPTIONS': the options passed to the setup_gen script %% %% The following local functions are handled by the script evaluator: %% %% * `b() -> Bindings' %% * `eval(File) -> {ok, Result} | {error, Reason}' %% * `eval(File, Vars) -> {ok, Result} | {error, Reason}' %% * `eval_lib(File) -> {ok, Result} | {error, Reason}' %% * `eval_lib(File, Vars) -> {ok, Result} | {error, Reason}' %% %% The `eval/[1,2]' and `eval_lib/[1,2]' functions work like the %% `include' and `include_lib' instructions above, except the result is %% returned as a normal function return value rather than being embedded %% into the `setup' data. Essentially, they work like `file:script()', but %% with the variable bindings expected by a `setup' script and these local %% functions supported. %% @end %% run(Options) -> case lists:keyfind(verbose, 1, Options) of {_, true} -> put(verbose, true); _ -> ignore end, setup_lib:determine_if_escript(), ?if_verbose(io:fwrite("Options = ~p~n", [Options])), Config = read_config(Options), ?if_verbose(io:fwrite("Config = ~p~n", [Config])), FullOpts = insert_config(Config, Options), ?if_verbose(io:fwrite("FullOpts = ~p~n", [FullOpts])), {Name, OutDir, RelDir, RelVsn, GenTarget} = name_and_target(FullOpts), ensure_dir(RelDir), Roots = roots(FullOpts), ?if_verbose(io:fwrite("Roots = ~p~n", [Roots])), check_config(FullOpts), Env = env_vars(FullOpts), InstEnv = install_env(Env, FullOpts), add_paths(Roots, FullOpts), Apps0 = apps(FullOpts, Env), Rel = {release, {Name, RelVsn}, {erts, erts_vsn()}, [A || {A,_} <- Apps0]}, ?if_verbose(io:fwrite("Rel: ~p~n", [Rel])), build_target_lib(GenTarget, OutDir, Apps0), copy_erts(GenTarget, OutDir), in_dir(RelDir, fun() -> setup_lib:write_eterm("start.rel", Rel), make_boot("start", GenTarget, Roots), setup_lib:write_eterm("sys.config", Env), if_install(FullOpts, fun() -> InstRel = make_install_rel(Rel), setup_lib:write_eterm( "install.rel", InstRel), setup_lib:write_eterm( "install.config", InstEnv), make_boot("install", GenTarget, Roots) end, ok), setup_lib:write_eterm("setup_gen.eterm", FullOpts) end). insert_config(Conf, Options) -> lists:flatmap( fun({conf, _} = C) -> [C|Conf]; (Other) -> [Other] end, Options). name_and_target(FullOpts) -> Name = option(name, FullOpts), case proplists:get_value(target, FullOpts, false) of false -> RelDir = option(outdir, FullOpts), RelVsn = rel_vsn(RelDir, FullOpts), {Name, RelDir, RelDir, RelVsn, false}; TargetDir -> RelVsn = option(vsn, FullOpts), RelDir = filename:join([TargetDir, "releases", RelVsn]), case filelib:is_dir(TargetDir) of true -> abort("Target directory exists: ~s~n", [TargetDir]); false -> ok end, {Name, TargetDir, RelDir, RelVsn, true} end. build_target_lib(false, _, _) -> ok; build_target_lib(true, Root, Apps) -> LibRoot = filename:join(Root, "lib"), file:make_dir(LibRoot), in_dir(LibRoot, fun() -> lists:foreach( fun({A, D}) -> AppName = element(1, A), AppVsn = element(2, A), AppD = atom_to_list(AppName) ++ "-" ++ AppVsn, Dir = filename:dirname(D), copy_app(Dir, AppD) end, Apps) end). copy_erts(false, _) -> ok; copy_erts(true, Root) -> {ok, [[ErlRoot]]} = init:get_argument(root), [Erts] = filelib:wildcard(filename:join(ErlRoot, "erts-*")), BaseName = filename:basename(Erts), file:make_dir(filename:join(Root, BaseName)), Cmd = "cp -r " ++ Erts ++ "/bin " ++ Root ++ "/" ++ BaseName ++ "/", os:cmd(Cmd). copy_app(A0, To) -> file:make_dir(To), os:cmd("cp -r " ++ A0 ++ "/priv " ++ To), os:cmd("cp -r " ++ A0 ++ "/ebin " ++ To). if_install(Options, F, Else) -> case proplists:get_value(install,Options,false) of true -> F(); _ -> Else end. options(["-target" , D|T]) -> [{target,D}|options(T)]; options(["-name" , N|T]) -> [{name, N}|options(T)]; options(["-root" , D|T]) -> [{root, D}|options(T)]; options(["-out" , D|T]) -> [{outdir, D}|options(T)]; options(["-relconf" , F|T]) -> [{relconf, F}|options(T)]; options(["-conf" , F|T]) -> [{conf, F}|options(T)]; %% options(["-target_subdir", D|T]) -> [{target_subdir, D}|options(T)]; options(["-install"]) -> [{install, true}]; options(["-install" | ["-" ++ _|_] = T]) -> [{install, true}|options(T)]; options(["-install" , D|T]) -> [{install, mk_bool(D)}|options(T)]; options(["-start_setup" , D|T]) -> [{start_setup, mk_bool(D)}|options(T)]; options(["-sys" , D|T]) -> [{sys, D}|options(T)]; options(["-vsn" , D|T]) -> [{vsn, D}|options(T)]; options(["-pa" , D|T]) -> [{pa, D}|options(T)]; options(["-pz" , D|T]) -> [{pz, D}|options(T)]; options(["-v" |T]) -> [{verbose, true}|options(T)]; options(["-V" ++ VarName, ExprStr | T]) -> Var = list_to_atom(VarName), Term = parse_term(ExprStr), [{var, Var, Term}|options(T)]; options([_Other|_] = L) -> abort("Unknown_option: ~p~n", [L]); options([]) -> []. mk_bool(T) when T=="true" ; T=="1" -> true; mk_bool(F) when F=="false"; F=="0" -> false; mk_bool(Other) -> abort("Expected truth value (~p)~n", [Other]). parse_term(Str) -> case erl_scan:string(Str) of {ok, Ts, _} -> case erl_parse:parse_term(ensure_dot(Ts)) of {ok, T} -> T; {error,_} = EParse -> abort(EParse, []) end; {error,_,_} = EScan -> abort(EScan, []) end. ensure_dot(Ts) -> case lists:reverse(Ts) of [{dot,_}|_] -> Ts; Rev -> lists:reverse([{dot,1}|Rev]) end. %% target_dir(RelDir, Config) -> %% D = case proplists:get_value(target_subdir, Config) of %% undefined -> %% RelDir; %% Sub -> %% filename:join(RelDir, Sub) %% end, %% ensure_dir(D), %% D. ensure_dir(D) -> case filelib:is_dir(D) of true -> ok; false -> case filelib:ensure_dir(D) of ok -> case file:make_dir(D) of ok -> ok; MakeErr -> abort("Could not create ~s (~p)~n", [D, MakeErr]) end; EnsureErr -> abort("Parent of ~s could not be created or is not " "writeable (~p)~n", [D, EnsureErr]) end end. read_config(Opts) -> case lists:keyfind(conf, 1, Opts) of false -> read_rel_config(Opts); {_, F} -> Name = option(name, Opts), setup:read_config_script(F, Name, Opts) end. read_rel_config(Opts) -> case lists:keyfind(relconf, 1, Opts) of {relconf, F} -> Name = option(name, Opts), case file:consult(F) of {ok, Conf} -> ?if_verbose(io:fwrite("Relconf = ~p~n", [Conf])), SysConf = option(sys, Conf), LibDirs = option(lib_dirs, SysConf), TargetOpt = rel_conf_target_dir(Conf, Opts), case [{V,As} || {rel,N,V,As} <- SysConf, N == Name] of [] -> abort("No matching 'rel' (~w) in ~s~n", [Name, F]); [{V,Apps}] -> TargetOpt ++ [{vsn, V}, {apps, Apps} | [{root, D} || D <- LibDirs]] end; Error -> abort("Error reading relconf ~s:~n" "~p~n", [F, Error]) end; false -> abort("No usable config file~n", []) end. rel_conf_target_dir(Conf, Opts) -> case lists:keyfind(target, 1, Opts) of {_, _} -> %% The 'target' option overrides whatever is in the relconf []; false -> case lists:keyfind(target_dir, 1, Conf) of false -> []; {_, TargetDir} -> [{target, TargetDir}] end end. roots(Opts) -> [R || {root, R} <- Opts]. check_config(Conf) -> _ = [mandatory(K, Conf) || K <- [apps]], ok. option(K, Opts) -> case lists:keyfind(K, 1, Opts) of {_, V} -> V; false -> abort("Mandatory: -~s~n", [atom_to_list(K)]) end. env_vars(Options) -> Env0 = case proplists:get_value(sys, Options) of undefined -> []; Sys -> case file:consult(Sys) of {ok, [E]} -> E; {error, Reason} -> abort("Error reading ~s:~n" "~p~n", [Sys, Reason]) end end, SetupEnv = if_install(Options, fun() -> [{env, [{setup, [{conf,Options}]}]}] end, []), lists:foldl(fun merge_env/2, Env0, [{K,E} || {K, E} <- Options ++ SetupEnv, K == env orelse K == set_env]). install_env(Env, Options) -> Dist = case proplists:get_value(nodes, Options, []) of [] -> []; [_] -> []; [_,_|_] = Nodes -> [{sync_nodes_mandatory, Nodes}, {sync_nodes_timeout, infinity}, {distributed, [{setup, [hd(Nodes)]}]}] end, case lists:keyfind(kernel, 1, Env) of false -> [{kernel, Dist} | Env]; {_, KEnv} -> Env1 = Dist ++ [E || {K,_} = E <- KEnv, not lists:member(K, [sync_nodes_optional, sync_nodes_mandatory, sync_nodes_timeout, distributed])], lists:keyreplace(kernel, 1, Env, {kernel, Env1}) end. merge_env({env, E}, Env) -> lists:foldl( fun({App, AEnv}, Acc) -> case lists:keyfind(App, 1, Env) of false -> Acc ++ [{App, AEnv}]; {_, AEnv1} -> New = {App, lists:foldl( fun({K,V}, Acc1) -> lists:keystore(K,1,Acc1,{K,V}); ({K,W,V}, Acc1) -> lists:keystore(K,1,Acc1,{K,W,V}) end, AEnv1, AEnv)}, lists:keyreplace(App, 1, Acc, New) end end, Env, E); merge_env({set_env, E}, Env) -> lists:foldl( fun({App, AEnv}, Acc) -> case lists:keyfind(App, 1, Env) of false -> Acc ++ [{App, merge_set_env(AEnv, [])}]; {_, AEnv1} -> New = {App, merge_set_env(AEnv, AEnv1)}, lists:keyreplace(App, 1, Acc, New) end end, Env, E). merge_set_env(E, E0) -> lists:foldl( fun({K, V}, Acc) when is_atom(K) -> lists:keystore(K, 1, Acc, {K, V}); ({[H|T], V}, Acc) -> case lists:keyfind(H, 1, Acc) of false -> Acc ++ [{H, mktree(T, V)}]; {_, Tree} -> lists:keyreplace(H, 1, Acc, {H, into_tree(T, Tree, V)}); {_, V1, Tree} -> lists:keyreplace( H, 1, Acc, {H, V1, into_tree(T, Tree, V)}) end; ({[], _}, Acc) -> %% allow, but ignore Acc end, E0, E). mktree([K], V) -> {K, V}; mktree([H|T], V) -> {H, [mktree(T, V)]}. into_tree([K], T0, V) -> lists:keystore(K, 1, T0, {K, V}); into_tree([H|T] = L, T0, V) when is_list(T0) -> case lists:keyfind(H, 1, T0) of false -> T0 ++ [mktree(L, V)]; {_, T1} -> lists:keystore(H, 1, T0, {H, into_tree(T, T1, V)}); {_, V1, T1} -> lists:keystore(H, 1, T0, {H, V1, into_tree(T, T1, V)}) end; into_tree([_|_] = L, _, V) -> [mktree(L, V)]. mandatory(K, Conf) -> case lists:keymember(K, 1, Conf) of false -> abort("missing mandatory config item: ~p~n", [K]); true -> ok end. in_dir(D, F) -> {ok, Old} = file:get_cwd(), try file:set_cwd(D) of ok -> ?if_verbose(io:fwrite("entering directory ~s~n", [D])), F(); Error -> abort("Error entering rel dir (~p): ~p~n", [D,Error]) after ok = file:set_cwd(Old) end. -define(is_type(T), T==permanent;T==temporary;T==transient;T==load). apps(Options, Env) -> %% Apps0 = proplists:get_value(apps, Options, []) %% ++ lists:concat(proplists:get_all_values(add_apps, Options)), {AddApps, RemoveApps} = add_remove_apps(Options, Env), Apps0 = remove_apps(RemoveApps, proplists:get_value(apps, Options, []) ++ AddApps), Apps1 = trim_duplicates( if_install(Options, fun() -> ensure_setup(Apps0) end, Apps0)), AppNames = app_names(Apps1), ?if_verbose(io:fwrite("Apps1 = ~p~n", [Apps1])), AppVsns = lists:flatmap( fun(A) when is_atom(A) -> {V,D} = app_vsn(A), [{{A, V}, D}]; ({A,V}) when is_list(V) -> {V1,D} = app_vsn(A, V), [{{A, V1}, D}]; ({A,Type}) when ?is_type(Type) -> {V1,D} = app_vsn(A), [{{A, V1, Type}, D}]; ({A,V,Type}) when ?is_type(Type) -> {V1,D} = app_vsn(A, V), [{{A, V1, Type}, D}]; ({A,V,Incl}) when is_list(Incl) -> {V1, D} = app_vsn(A, V), expand_included(Incl, AppNames) ++ [{{A, V1, Incl}, D}]; ({A,V,Type,Incl}) when ?is_type(Type) -> {V1, D} = app_vsn(A, V), expand_included(Incl, AppNames) ++ [{{A, V1, Type, Incl}, D}] end, sort_apps(Options, Apps1)), ?if_verbose(io:fwrite("AppVsns = ~p~n", [AppVsns])), %% setup_is_load_only(replace_versions(AppVsns, Apps1)). case proplists:get_value(start_setup, Options, true) of true -> AppVsns; false -> setup_is_load_only(AppVsns) end. add_remove_apps(Options, _Env) -> lists:foldl( fun({add_apps, As}, {Incl, Excl}) -> {add_to_set(As, Incl), del_from_set(As, Excl)}; ({remove_apps, As}, {Incl, Excl}) -> {del_from_set(As, Incl), add_to_set(app_names(As), Excl)}; (_, Acc) -> Acc end, {[], []}, Options). sort_apps(Options, Apps) -> lists:foldl(fun({sort_app, A, Before}, Acc) -> case is_in_set(A, Acc) of {true, App} -> insert_before(Acc -- [App], App, mk_set(Before)); false -> abort("Cannot re-sort ~p - not found~n", [A]) end; (_, Acc) -> Acc end, Apps, Options). insert_before([H|T], App, Before) -> case is_in_set(H, Before) of {true, _} -> [App, H|T]; false -> [H|insert_before(T, App, Before)] end. add_to_set(As, Set) -> lists:foldl(fun(A, Acc) -> case is_in_set(A, Acc) of {true, Prev} -> lists:delete(Prev, Acc) ++ [A]; false -> Acc ++ [A] end end, Set, As). del_from_set(As, Set) -> lists:foldl(fun(A, Acc) -> case is_in_set(A, Acc) of {true, Prev} -> lists:delete(Prev, Acc); false -> Acc end end, Set, As). %% ensure that this is a 'set' (well, at least a list) mk_set(Set) when is_list(Set) -> Set; mk_set(Entry) -> [Entry]. is_in_set(Entry, Set) -> A = if is_tuple(Entry) -> element(1, Entry); is_atom(Entry) -> Entry end, name_in_set(A, Set). app_name(A) when is_atom(A) -> A; app_name(T) when is_tuple(T) -> element(1, T). app_names(As) -> [app_name(A) || A <- As]. name_in_set(A, [A|_]) -> {true, A}; name_in_set(A, [H|_]) when element(1,H) == A -> {true, H}; name_in_set(A, [_|T]) -> name_in_set(A, T); name_in_set(_, []) -> false. remove_apps(Remove, Apps) -> lists:filter( fun(Entry) -> A = case Entry of Am when is_atom(Am) -> Am; T when is_tuple(T) -> element(1,T) end, not lists:member(A, Remove) end, Apps). trim_duplicates([A|As0]) when is_atom(A) -> As1 = [Ax || Ax <- As0, Ax =/= A], case lists:keymember(A, 1, As0) of false -> [A|trim_duplicates(As1)]; true -> %% a more well-defined entry exists; use that one. trim_duplicates(As1) end; trim_duplicates([At|As0]) when is_tuple(At) -> %% Remove all exact duplicates As1 = [Ax || Ax <- As0, Ax =/= At], %% If other detailed entries (though not duplicates) exist, we should %% perhaps try to combine them. For now, let's just abort. case [Ay || Ay <- As1, element(1,Ay) == element(1,At)] of [] -> [At|trim_duplicates(As0)]; [_|_] = Duplicates -> abort("Conflicting app entries: ~p~n", [[At|Duplicates]]) end; trim_duplicates([]) -> []. expand_included(Incl, AppNames) -> R = case Incl -- AppNames of [] -> []; Implicit -> [{A, app_vsn(A), load} || A <- Implicit] end, ?if_verbose(io:fwrite("expand_included(~p, ~p) -> ~p~n", [Incl, AppNames, R])), R. ensure_setup([setup|_] = As) -> As; ensure_setup([A|_] = As) when element(1,A) == setup -> As; ensure_setup([H|T]) -> [H|ensure_setup(T)]; ensure_setup([]) -> [setup]. setup_is_load_only(Apps) -> lists:map(fun({{setup,V}, D}) -> {{setup,V,load}, D}; (A) -> A end, Apps). add_paths(Roots, Opts) -> APaths = proplists:get_all_values(pa, Opts), _ = [ true = code:add_patha(P) || P <- APaths ], ZPaths = proplists:get_all_values(pz, Opts), _ = [ true = code:add_pathz(P) || P <- ZPaths ], Paths = case proplists:get_value(wild_roots, Opts, false) of true -> lists:foldl(fun(R, Acc) -> expand_root(R, Acc) end, [], Roots); false -> lists:concat( lists:map(fun(R) -> case filelib:wildcard( filename:join(R, "lib/*/ebin")) of [] -> filelib:wildcard( filename:join(R, "*/ebin")); Ds -> Ds end end, Roots)) end, ?if_verbose(io:fwrite("Paths = ~p~n", [Paths])), Res = code:set_path(Paths ++ (code:get_path() -- Paths)), %% Res = code:add_pathsa(Paths -- code:get_path()), ?if_verbose(io:fwrite("add path Res = ~p~n", [Res])). expand_root(R, Acc) -> case filename:basename(R) of "ebin" -> [R|Acc]; _ -> case file:list_dir(R) of {ok, Fs} -> lists:foldl(fun(F, Acc1) -> expand_root(filename:join(R, F), Acc1) end, Acc, Fs); {error,enotdir} -> Acc; {error,_} = E -> ?if_verbose(io:fwrite("warning: ~p (~s)~n", [E, R])), Acc end end. rel_vsn(RelDir, Options) -> case proplists:get_value(vsn, Options) of undefined -> Dir = case RelDir of "." -> {ok,Cwd} = file:get_cwd(), Cwd; D -> D end, filename:basename(Dir); V when is_list(V) -> V; Other -> abort("Invalid release version ~p~n", [Other]) end. erts_vsn() -> erlang:system_info(version). app_vsn(A) -> app_vsn(A, latest). %% AName = if is_atom(A) -> A; %% true -> element(1, A) %% end, %% D = code:lib_dir(AName), %% AppFile = filename:join(D, "ebin/" ++ atom_to_list(AName) ++ ".app"), %% case file:consult(AppFile) of %% {ok, [{application, _, Opts}]} -> %% V = proplists:get_value(vsn, Opts), %% ?if_verbose(io:fwrite("app_vsn(~p) -> ~p~n", [A,V])), %% V; %% Other -> %% abort("Oops reading .app file (~p): ~p~n", [AppFile, Other]) %% end. app_vsn(A, V) -> AppStr = atom_to_list(A), Path = code:get_path(), Found = [D || D <- Path, is_app(AppStr, D)], Sorted = setup_lib:sort_vsns(lists:usort(Found), AppStr), ?if_verbose(io:fwrite("Sorted = ~p~n", [Sorted])), match_app_vsn(Sorted, V, AppStr). match_app_vsn(Vsns, latest, App) -> %% element(1, lists:last(Vsns)); case Vsns of [] -> abort("No version of ~s found~n", [App]); [_|_] -> lists:last(Vsns) end; match_app_vsn(Vsns, V, App) when is_list(V) -> case [Pair || {V1, _} = Pair <- Vsns, V == V1] of [FoundV] -> FoundV; [] -> abort("Cannot find version ~s of ~s~n", [V, App]) end. is_app(A, D) -> case re:run(D, A ++ "[^/]*/ebin\$") of {match, _} -> true; nomatch -> false end. %% replace_versions([App|Apps], [H|T]) -> %% A = element(1, App), %% V = element(2, App), %% Res = %% if is_atom(H) -> %% A = H, % assertion %% {A, V}; %% true -> %% A = element(1, H), % assertion %% setelement(2, H, V) %% end, %% [Res | replace_versions(Apps, T)]; %% replace_versions([], []) -> %% []. make_boot(Rel, GenTarget, Roots) -> Path = path(Roots), Vars = if GenTarget -> []; true -> {Vs, _} = lists:mapfoldl( fun(R, N) -> V = var_name(N), {{V, R}, N+1} end, 1, Roots), Vs end, ?if_verbose(io:fwrite("Path = ~p~n", [Path])), Opts = if GenTarget -> [no_module_tests]; true -> [no_module_tests, local, {variables, Vars}] end, Res = systools:make_script(Rel, [{path, path(Roots)}|Opts]), ?if_verbose(io:fwrite("make_script() -> ~p~n", [Res])). make_install_rel({release, R, Erts, Apps}) -> Apps1 = lists:map( fun({setup,V,load}) -> {setup, V}; (A) -> case lists:member(element(1,A), [stdlib,kernel,sasl]) of true -> A; false -> case A of {Nm,Vsn} -> {Nm,Vsn,load}; {Nm,Vsn,Inc} when is_list(Inc) -> {Nm,Vsn,load,Inc}; _ -> A end end end, Apps), %% Apps2 = case app_vsn(setup) of %% undefined -> %% Apps1; %% V -> %% Apps1 ++ [{setup, V}] %% end, {release, R, Erts, Apps1}. path(Roots) -> [filename:join(R, "lib/*/ebin") || R <- Roots]. var_name(N) -> "V" ++ integer_to_list(N).