diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/rlx_app_discovery.erl | 15 | ||||
-rw-r--r-- | src/rlx_app_info.erl | 14 | ||||
-rw-r--r-- | src/rlx_cmd_args.erl | 3 | ||||
-rw-r--r-- | src/rlx_config.erl | 18 | ||||
-rw-r--r-- | src/rlx_depsolver.erl | 2 | ||||
-rw-r--r-- | src/rlx_goal.erl | 2 | ||||
-rw-r--r-- | src/rlx_prv_assembler.erl | 263 | ||||
-rw-r--r-- | src/rlx_prv_overlay.erl | 30 | ||||
-rw-r--r-- | src/rlx_prv_relup.erl | 37 | ||||
-rw-r--r-- | src/rlx_release.erl | 11 | ||||
-rw-r--r-- | src/rlx_state.erl | 28 | ||||
-rw-r--r-- | src/rlx_topo.erl | 190 |
12 files changed, 544 insertions, 69 deletions
diff --git a/src/rlx_app_discovery.erl b/src/rlx_app_discovery.erl index dcd2604..0414a0a 100644 --- a/src/rlx_app_discovery.erl +++ b/src/rlx_app_discovery.erl @@ -290,13 +290,24 @@ get_vsn(AppDir, AppName, AppDetail) -> end end. --spec get_deps(file:name(), atom(), string(), proplists:proplist()) -> +-spec get_deps(binary(), atom(), string(), proplists:proplist()) -> {ok, rlx_app_info:t()} | {error, Reason::term()}. get_deps(AppDir, AppName, AppVsn, AppDetail) -> - ActiveApps = proplists:get_value(applications, AppDetail, []), + %% ensure that at least stdlib and kernel are defined as application deps + ActiveApps = ensure_stdlib_kernel(AppName, + proplists:get_value(applications, AppDetail, [])), LibraryApps = proplists:get_value(included_applications, AppDetail, []), rlx_app_info:new(AppName, AppVsn, AppDir, ActiveApps, LibraryApps). +-spec ensure_stdlib_kernel(AppName :: atom(), + Apps :: list(atom())) -> list(atom()). +ensure_stdlib_kernel(kernel, Deps) -> Deps; +ensure_stdlib_kernel(stdlib, Deps) -> Deps; +ensure_stdlib_kernel(_AppName, []) -> + %% minimum required deps are kernel and stdlib + [kernel, stdlib]; +ensure_stdlib_kernel(_AppName, Deps) -> Deps. + %%%=================================================================== %%% Test Functions %%%=================================================================== diff --git a/src/rlx_app_info.erl b/src/rlx_app_info.erl index b3402d0..f44dbb5 100644 --- a/src/rlx_app_info.erl +++ b/src/rlx_app_info.erl @@ -61,9 +61,9 @@ -include("relx.hrl"). -record(app_info_t, {name :: atom(), - original_vsn :: string(), - vsn :: ec_semver:semver(), - dir :: binary(), + original_vsn :: undefined | string(), + vsn :: undefined | ec_semver:semver(), + dir :: undefined | binary(), link=false :: boolean(), active_deps=[]:: [atom()], library_deps=[] :: [atom()]}). @@ -83,13 +83,13 @@ new() -> {ok, #app_info_t{}}. %% @doc build a complete version of the app info with all fields set. --spec new(atom(), string(), file:name(), [atom()], [atom()]) -> +-spec new(atom(), string(), binary(), [atom()], [atom()]) -> {ok, t()} | relx:error(). new(AppName, Vsn, Dir, ActiveDeps, LibraryDeps) -> new(AppName, Vsn, Dir, ActiveDeps, LibraryDeps, false). %% @doc build a complete version of the app info with all fields set. --spec new(atom(), string(), file:name(), [atom()], [atom()], boolean()) -> +-spec new(atom(), string(), binary(), [atom()], [atom()], boolean()) -> {ok, t()} | relx:error(). new(AppName, Vsn, Dir, ActiveDeps, LibraryDeps, Link) when erlang:is_atom(AppName), @@ -138,10 +138,10 @@ vsn(AppInfo=#app_info_t{name=AppName}, AppVsn) {ok, AppInfo#app_info_t{vsn=ParsedVsn}} end. --spec dir(t()) -> file:name(). +-spec dir(t()) -> binary(). dir(#app_info_t{dir=Dir}) -> Dir. --spec dir(t(), file:name()) -> t(). +-spec dir(t(), binary()) -> t(). dir(AppInfo=#app_info_t{}, Dir) -> AppInfo#app_info_t{dir=Dir}. diff --git a/src/rlx_cmd_args.erl b/src/rlx_cmd_args.erl index 7f3f39b..b20344c 100644 --- a/src/rlx_cmd_args.erl +++ b/src/rlx_cmd_args.erl @@ -282,6 +282,9 @@ create(include_erts, Opts) -> Erts when is_list(Erts) -> {include_erts, Erts} end; +create(warnings_as_errors, Opts) -> + WarningsAsErrors = proplists:get_value(warnings_as_errors, Opts, false), + {warnings_as_errors, WarningsAsErrors}; create(_, _) -> []. diff --git a/src/rlx_config.erl b/src/rlx_config.erl index d18f5f1..b5ef51b 100644 --- a/src/rlx_config.erl +++ b/src/rlx_config.erl @@ -173,6 +173,8 @@ load_terms({skip_apps, SkipApps0}, {ok, State0}) -> {ok, rlx_state:skip_apps(State0, SkipApps0)}; load_terms({exclude_apps, ExcludeApps0}, {ok, State0}) -> {ok, rlx_state:exclude_apps(State0, ExcludeApps0)}; +load_terms({exclude_modules, ExcludeModules0}, {ok, State0}) -> + {ok, rlx_state:exclude_modules(State0, ExcludeModules0)}; load_terms({debug_info, DebugInfo}, {ok, State0}) -> {ok, rlx_state:debug_info(State0, DebugInfo)}; load_terms({overrides, Overrides0}, {ok, State0}) -> @@ -265,8 +267,10 @@ load_terms({output_dir, OutputDir}, {ok, State}) -> load_terms({overlay_vars, OverlayVars}, {ok, State}) -> CurrentOverlayVars = rlx_state:get(State, overlay_vars), NewOverlayVars0 = list_of_overlay_vars_files(OverlayVars), - NewOverlayVars1 = lists:umerge(lists:usort(NewOverlayVars0), lists:usort(CurrentOverlayVars)), + NewOverlayVars1 = CurrentOverlayVars ++ NewOverlayVars0, {ok, rlx_state:put(State, overlay_vars, NewOverlayVars1)}; +load_terms({warnings_as_errors, WarningsAsErrors}, {ok, State}) -> + {ok, rlx_state:warnings_as_errors(State, WarningsAsErrors)}; load_terms({Name, Value}, {ok, State}) when erlang:is_atom(Name) -> {ok, rlx_state:put(State, Name, Value)}; @@ -310,7 +314,6 @@ merge_configs([{Key, Value} | CliTerms], ConfigTerms) -> case Key of X when X =:= lib_dirs ; X =:= goals - ; X =:= overlay_vars ; X =:= overrides -> case lists:keyfind(Key, 1, ConfigTerms) of {Key, Value2} -> @@ -319,6 +322,17 @@ merge_configs([{Key, Value} | CliTerms], ConfigTerms) -> false -> merge_configs(CliTerms, ConfigTerms++[{Key, Value}]) end; + overlay_vars -> + case lists:keyfind(overlay_vars, 1, ConfigTerms) of + {_, [H | _] = Vars} when is_list(H) -> + MergedValue = Vars ++ Value, + merge_configs(CliTerms, lists:keyreplace(overlay_vars, 1, ConfigTerms, {Key, MergedValue})); + {_, Vars} when is_list(Vars) -> + MergedValue = [Vars | Value], + merge_configs(CliTerms, lists:keyreplace(overlay_vars, 1, ConfigTerms, {Key, MergedValue})); + false -> + merge_configs(CliTerms, ConfigTerms++[{Key, Value}]) + end; _ -> merge_configs(CliTerms, lists:reverse(lists:keystore(Key, 1, lists:reverse(ConfigTerms), {Key, Value}))) end. diff --git a/src/rlx_depsolver.erl b/src/rlx_depsolver.erl index fd26145..9e34a2c 100644 --- a/src/rlx_depsolver.erl +++ b/src/rlx_depsolver.erl @@ -113,7 +113,7 @@ %% type %%============================================================================ -ifdef(namespaced_types). --type dep_graph() :: gb_tree:tree(). +-type dep_graph() :: gb_trees:tree(). -else. -type dep_graph() :: gb_tree(). -endif. diff --git a/src/rlx_goal.erl b/src/rlx_goal.erl index 354aa48..07126d5 100644 --- a/src/rlx_goal.erl +++ b/src/rlx_goal.erl @@ -10,8 +10,6 @@ -define(p_seq,true). -define(p_string,true). - --compile(export_all). -spec file(file:name()) -> any(). file(Filename) -> case file:read_file(Filename) of {ok,Bin} -> parse(Bin); Err -> Err end. diff --git a/src/rlx_prv_assembler.erl b/src/rlx_prv_assembler.erl index e942b64..2088de6 100644 --- a/src/rlx_prv_assembler.erl +++ b/src/rlx_prv_assembler.erl @@ -121,7 +121,10 @@ format_error({start_clean_script_generation_error, Module, Errors}) -> rlx_util:indent(2), Module:format_error(Errors)]; format_error({strip_release, Reason}) -> io_lib:format("Stripping debug info from release beam files failed becuase ~s", - [beam_lib:format_error(Reason)]). + [beam_lib:format_error(Reason)]); +format_error({rewrite_app_file, AppFile, Error}) -> + io_lib:format("Unable to rewrite .app file ~s due to ~p", + [AppFile, Error]). %%%=================================================================== %%% Internal Functions @@ -162,7 +165,7 @@ copy_app_directories_to_output(State, Release, OutputDir) -> false end, lists:flatten(ec_plists:map(fun(App) -> - copy_app(LibDir, App, IncludeSrc, IncludeErts) + copy_app(State, LibDir, App, IncludeSrc, IncludeErts) end, Apps))), case Result of [E | _] -> @@ -179,7 +182,7 @@ prepare_applications(State, Apps) -> Apps end. -copy_app(LibDir, App, IncludeSrc, IncludeErts) -> +copy_app(State, LibDir, App, IncludeSrc, IncludeErts) -> AppName = erlang:atom_to_list(rlx_app_info:name(App)), AppVsn = rlx_app_info:original_vsn(App), AppDir = rlx_app_info:dir(App), @@ -196,57 +199,75 @@ copy_app(LibDir, App, IncludeSrc, IncludeErts) -> true -> []; false -> - copy_app_(App, AppDir, TargetDir, IncludeSrc) + copy_app_(State, App, AppDir, TargetDir, IncludeSrc) end; _ -> - copy_app_(App, AppDir, TargetDir, IncludeSrc) + copy_app_(State, App, AppDir, TargetDir, IncludeSrc) end end. is_erts_lib(Dir) -> lists:prefix(filename:split(list_to_binary(code:lib_dir())), filename:split(Dir)). -copy_app_(App, AppDir, TargetDir, IncludeSrc) -> +copy_app_(State, App, AppDir, TargetDir, IncludeSrc) -> remove_symlink_or_directory(TargetDir), case rlx_app_info:link(App) of true -> link_directory(AppDir, TargetDir), - rewrite_app_file(App, AppDir); + rewrite_app_file(State, App, AppDir); false -> - copy_directory(AppDir, TargetDir, IncludeSrc), - rewrite_app_file(App, TargetDir) + copy_directory(State, App, AppDir, TargetDir, IncludeSrc), + rewrite_app_file(State, App, TargetDir) end. %% If excluded apps exist in this App's applications list we must write a new .app -rewrite_app_file(App, TargetDir) -> +rewrite_app_file(State, App, TargetDir) -> Name = rlx_app_info:name(App), ActiveDeps = rlx_app_info:active_deps(App), IncludedDeps = rlx_app_info:library_deps(App), AppFile = filename:join([TargetDir, "ebin", ec_cnv:to_list(Name) ++ ".app"]), - {ok, [{application, AppName, AppData}]} = file:consult(AppFile), - OldActiveDeps = proplists:get_value(applications, AppData, []), - OldIncludedDeps = proplists:get_value(included_applications, AppData, []), - - case {OldActiveDeps, OldIncludedDeps} of - {ActiveDeps, IncludedDeps} -> - ok; - _ -> - AppData1 = lists:keyreplace(applications - ,1 - ,AppData - ,{applications, ActiveDeps}), - AppData2 = lists:keyreplace(included_applications - ,1 - ,AppData1 - ,{included_applications, IncludedDeps}), - Spec = io_lib:format("~p.\n", [{application, AppName, AppData2}]), - write_file_if_contents_differ(AppFile, Spec) + {ok, [{application, AppName, AppData0}]} = file:consult(AppFile), + OldActiveDeps = proplists:get_value(applications, AppData0, []), + OldIncludedDeps = proplists:get_value(included_applications, AppData0, []), + OldModules = proplists:get_value(modules, AppData0, []), + ExcludedModules = proplists:get_value(Name, + rlx_state:exclude_modules(State), []), + + %% maybe replace excluded apps + AppData2 = + case {OldActiveDeps, OldIncludedDeps} of + {ActiveDeps, IncludedDeps} -> + AppData0; + _ -> + AppData1 = lists:keyreplace(applications + ,1 + ,AppData0 + ,{applications, ActiveDeps}), + lists:keyreplace(included_applications + ,1 + ,AppData1 + ,{included_applications, IncludedDeps}) + end, + %% maybe replace excluded modules + AppData3 = + case ExcludedModules of + [] -> AppData2; + _ -> + lists:keyreplace(modules + ,1 + ,AppData2 + ,{modules, OldModules -- ExcludedModules}) + end, + Spec = [{application, AppName, AppData3}], + case write_file_if_contents_differ(AppFile, Spec) of + ok -> ok; + Error -> ?RLX_ERROR({rewrite_app_file, AppFile, Error}) end. -write_file_if_contents_differ(Filename, Bytes) -> - ToWrite = iolist_to_binary(Bytes), - case file:read_file(Filename) of - {ok, ToWrite} -> +write_file_if_contents_differ(Filename, Spec) -> + ToWrite = io_lib:format("~p.\n", Spec), + case file:consult(Filename) of + {ok, Spec} -> ok; {ok, _} -> file:write_file(Filename, ToWrite); @@ -275,8 +296,8 @@ link_directory(AppDir, TargetDir) -> ok end. -copy_directory(AppDir, TargetDir, IncludeSrc) -> - [copy_dir(AppDir, TargetDir, SubDir) +copy_directory(State, App, AppDir, TargetDir, IncludeSrc) -> + [copy_dir(State, App, AppDir, TargetDir, SubDir) || SubDir <- ["ebin", "include", "priv", @@ -289,13 +310,20 @@ copy_directory(AppDir, TargetDir, IncludeSrc) -> [] end]]. -copy_dir(AppDir, TargetDir, SubDir) -> +copy_dir(State, App, AppDir, TargetDir, SubDir) -> SubSource = filename:join(AppDir, SubDir), SubTarget = filename:join(TargetDir, SubDir), case ec_file:is_dir(SubSource) of true -> ok = rlx_util:mkdir_p(SubTarget), - case ec_file:copy(SubSource, SubTarget, [recursive]) of + %% get a list of the modules to be excluded from this app + AppName = rlx_app_info:name(App), + ExcludedModules = proplists:get_value(AppName, rlx_state:exclude_modules(State), + []), + ExcludedFiles = [filename:join([binary_to_list(SubSource), + atom_to_list(M) ++ ".beam"]) || + M <- ExcludedModules], + case copy_dir(SubSource, SubTarget, ExcludedFiles) of {error, E} -> ?RLX_ERROR({ec_file_error, AppDir, SubTarget, E}); ok -> @@ -305,6 +333,22 @@ copy_dir(AppDir, TargetDir, SubDir) -> ok end. +%% no files are excluded, just copy the whole dir +copy_dir(SourceDir, TargetDir, []) -> + case ec_file:copy(SourceDir, TargetDir, [recursive]) of + {error, E} -> {error, E}; + ok -> + ok + end; +copy_dir(SourceDir, TargetDir, ExcludeFiles) -> + SourceFiles = filelib:wildcard( + filename:join([binary_to_list(SourceDir), "*"])), + lists:foreach(fun(F) -> + ok = ec_file:copy(F, + filename:join([TargetDir, + filename:basename(F)])) + end, SourceFiles -- ExcludeFiles). + create_release_info(State0, Release0, OutputDir) -> RelName = atom_to_list(rlx_release:name(Release0)), ReleaseDir = rlx_util:release_output_dir(State0, Release0), @@ -349,13 +393,17 @@ write_bin_file(State, Release, OutputDir, RelDir) -> rlx_release:erts(Release), ErlOpts); true -> - case rlx_state:get(State, extended_start_script, false) of - true -> - include_nodetool(BinDir); - false -> - ok - end, - extended_bin_file_contents(OsFamily, RelName, RelVsn, rlx_release:erts(Release), ErlOpts) + %% extended start script needs nodetool so it's + %% always included + include_nodetool(BinDir), + Hooks = expand_hooks(BinDir, + rlx_state:get(State, + extended_start_script_hooks, + []), + State), + extended_bin_file_contents(OsFamily, RelName, RelVsn, + rlx_release:erts(Release), ErlOpts, + Hooks) end, %% We generate the start script by default, unless the user %% tells us not too @@ -386,6 +434,98 @@ write_bin_file(State, Release, OutputDir, RelDir) -> E end. +expand_hooks(_Bindir, [], _State) -> []; +expand_hooks(BinDir, Hooks, _State) -> + expand_hooks(BinDir, Hooks, [], _State). + +expand_hooks(_BinDir, [], Acc, _State) -> Acc; +expand_hooks(BinDir, [{Phase, Hooks0} | Rest], Acc, State) -> + %% filter and expand hooks to their respective shell scripts + Hooks = + lists:foldl( + fun(Hook, Acc0) -> + case validate_hook(Phase, Hook) of + true -> + %% all hooks are relative to the bin dir + HookScriptFilename = filename:join([BinDir, + hook_filename(Hook)]), + %% write the hook script file to it's proper location + ok = render_hook(hook_template(Hook), HookScriptFilename, State), + %% and return the invocation that's to be templated in the + %% extended script + Acc0 ++ [hook_invocation(Hook)]; + false -> + ec_cmd_log:error( + rlx_state:log(State), + io_lib:format("~p hook is not allowed in the ~p phase, ignoring it", [Hook, Phase]) + ), + + Acc0 + end + end, [], Hooks0), + expand_hooks(BinDir, Rest, Acc ++ [{Phase, Hooks}], State). + +%% the pid script hook is only allowed in the +%% post_start phase +%% with args +validate_hook(post_start, {pid, _}) -> true; +%% and without args +validate_hook(post_start, pid) -> true; +%% same for wait_for_vm_start, wait_for_process script +validate_hook(post_start, wait_for_vm_start) -> true; +validate_hook(post_start, {wait_for_process, _}) -> true; +%% custom hooks are allowed in all phases +validate_hook(_Phase, {custom, _}) -> true; +%% as well as status hooks +validate_hook(status, _) -> true; +%% deny all others +validate_hook(_, _) -> false. + +hook_filename({custom, CustomScript}) -> CustomScript; +hook_filename(pid) -> "hooks/builtin/pid"; +hook_filename({pid, _}) -> "hooks/builtin/pid"; +hook_filename(wait_for_vm_start) -> "hooks/builtin/wait_for_vm_start"; +hook_filename({wait_for_process, _}) -> "hooks/builtin/wait_for_process"; +hook_filename(builtin_status) -> "hooks/builtin/status". + +hook_invocation({custom, CustomScript}) -> CustomScript; +%% the pid builtin hook with no arguments writes to pid file +%% at /var/run/{{ rel_name }}.pid +hook_invocation(pid) -> string:join(["hooks/builtin/pid", + "/var/run/$REL_NAME.pid"], "|"); +hook_invocation({pid, PidFile}) -> string:join(["hooks/builtin/pid", + PidFile], "|"); +hook_invocation(wait_for_vm_start) -> "hooks/builtin/wait_for_vm_start"; +hook_invocation({wait_for_process, Name}) -> + %% wait_for_process takes an atom as argument + %% which is the process name to wait for + string:join(["hooks/builtin/wait_for_process", + atom_to_list(Name)], "|"); +hook_invocation(builtin_status) -> "hooks/builtin/status". + +hook_template({custom, _}) -> custom; +hook_template(pid) -> builtin_hook_pid; +hook_template({pid, _}) -> builtin_hook_pid; +hook_template(wait_for_vm_start) -> builtin_hook_wait_for_vm_start; +hook_template({wait_for_process, _}) -> builtin_hook_wait_for_process; +hook_template(builtin_status) -> builtin_hook_status. + +%% custom hooks are not rendered, they should +%% be copied by the release overlays +render_hook(custom, _, _) -> ok; +render_hook(TemplateName, Script, State) -> + ec_cmd_log:info( + rlx_state:log(State), + "rendering ~p hook to ~p~n", + [TemplateName, Script] + ), + + Template = render(TemplateName), + ok = filelib:ensure_dir(Script), + _ = ec_file:remove(Script), + ok = file:write_file(Script, Template), + ok = file:change_mode(Script, 8#755). + include_nodetool(BinDir) -> NodeToolFile = nodetool_contents(), InstallUpgradeFile = install_upgrade_escript_contents(), @@ -456,7 +596,7 @@ copy_or_symlink_config_file(State, ConfigPath, RelConfPath) -> ensure_not_exist(RelConfPath), case rlx_state:dev_mode(State) of true -> - ok = rlx_util:symlink_or_copy(ConfigPath, RelConfPath ++ ".orig"); + ok = rlx_util:symlink_or_copy(ConfigPath, RelConfPath); _ -> ok = ec_file:copy(ConfigPath, RelConfPath) end. @@ -502,6 +642,18 @@ include_erts(State, Release, OutputDir, RelDir) -> ok = file:write_file(ErlIni, erl_ini(OutputDir, ErtsVersion)) end, + %% delete erts src if the user requested it not be included + case rlx_state:include_src(State) of + true -> ok; + false -> + SrcDir = filename:join([LocalErts, "src"]), + %% ensure the src folder exists before deletion + case ec_file:exists(SrcDir) of + true -> ok = ec_file:remove(SrcDir, [recursive]); + false -> ok + end + end, + case rlx_state:get(State, extended_start_script, false) of true -> @@ -559,7 +711,7 @@ make_boot_script_variables(State) -> % (dictated by erl.ini [erlang] Rootdir=) and so a boot variable is made % pointing to the release directory % On non-Windows, $ROOT is set by the ROOTDIR environment variable as the - % release directory, so a boot variable is made pointing to the erts + % release directory, so a boot variable is made pointing to the erts % directory. % NOTE the boot variable can point to either the release/erts root directory % or the release/erts lib directory, as long as the usage here matches the @@ -635,13 +787,30 @@ bin_file_contents(OsFamily, RelName, RelVsn, ErtsVsn, ErlOpts) -> render(Template, [{rel_name, RelName}, {rel_vsn, RelVsn}, {erts_vsn, ErtsVsn}, {erl_opts, ErlOpts}]). -extended_bin_file_contents(OsFamily, RelName, RelVsn, ErtsVsn, ErlOpts) -> +extended_bin_file_contents(OsFamily, RelName, RelVsn, ErtsVsn, ErlOpts, Hooks) -> Template = case OsFamily of unix -> extended_bin; win32 -> extended_bin_windows end, + %% turn all the hook lists into space separated strings + PreStartHooks = string:join(proplists:get_value(pre_start, Hooks, []), " "), + PostStartHooks = string:join(proplists:get_value(post_start, Hooks, []), " "), + PreStopHooks = string:join(proplists:get_value(pre_stop, Hooks, []), " "), + PostStopHooks = string:join(proplists:get_value(post_stop, Hooks, []), " "), + PreInstallUpgradeHooks = string:join(proplists:get_value(pre_install_upgrade, + Hooks, []), " "), + PostInstallUpgradeHooks = string:join(proplists:get_value(post_install_upgrade, + Hooks, []), " "), + StatusHook = string:join(proplists:get_value(status, Hooks, []), " "), render(Template, [{rel_name, RelName}, {rel_vsn, RelVsn}, - {erts_vsn, ErtsVsn}, {erl_opts, ErlOpts}]). + {erts_vsn, ErtsVsn}, {erl_opts, ErlOpts}, + {pre_start_hooks, PreStartHooks}, + {post_start_hooks, PostStartHooks}, + {pre_stop_hooks, PreStopHooks}, + {post_stop_hooks, PostStopHooks}, + {pre_install_upgrade_hooks, PreInstallUpgradeHooks}, + {post_install_upgrade_hooks, PostInstallUpgradeHooks}, + {status_hook, StatusHook}]). erl_ini(OutputDir, ErtsVsn) -> ErtsDirName = string:concat("erts-", ErtsVsn), diff --git a/src/rlx_prv_overlay.erl b/src/rlx_prv_overlay.erl index 71aca97..dc57326 100644 --- a/src/rlx_prv_overlay.erl +++ b/src/rlx_prv_overlay.erl @@ -308,6 +308,28 @@ handle_errors(State, Result) -> -spec do_individual_overlay(rlx_state:t(), list(), proplists:proplist(), OverlayDirective::term()) -> {ok, rlx_state:t()} | relx:error(). +do_individual_overlay(State, _Files, OverlayVars, {chmod, Mode, Path}) -> + % mode can be specified directly as an integer value, or if it is + % not an integer we assume it's a template, which we render and convert + % blindly to an integer. So this will crash with an exception if for + % some reason something other than an integer is used + NewMode = + case is_integer(Mode) of + true -> Mode; + false -> erlang:list_to_integer(erlang:binary_to_list(render_string (OverlayVars, Mode))) + end, + + Root = rlx_state:output_dir(State), + file_render_do(OverlayVars, Path, + fun(NewPath) -> + Absolute = absolutize(State, + filename:join(Root,erlang:iolist_to_binary (NewPath))), + case file:change_mode(Absolute, NewMode) of + {error, Error} -> + ?RLX_ERROR({unable_to_chmod, NewMode, NewPath, Error}); + ok -> ok + end + end); do_individual_overlay(State, _Files, OverlayVars, {mkdir, Dir}) -> case rlx_util:render(erlang:iolist_to_binary(Dir), OverlayVars) of {ok, IoList} -> @@ -461,6 +483,14 @@ write_template(OverlayVars, FromFile, ToFile) -> {ok, IoData} -> case filelib:ensure_dir(ToFile) of ok -> + %% we were asked to render a template + %% onto a symlink, this would cause an overwrite + %% of the original file, so we delete the symlink + %% and go ahead with the template render + case ec_file:is_symlink(ToFile) of + true -> ec_file:remove(ToFile); + false -> ok + end, case file:write_file(ToFile, IoData) of ok -> {ok, FileInfo} = file:read_file_info(FromFile), diff --git a/src/rlx_prv_relup.erl b/src/rlx_prv_relup.erl index 9ac2135..1f8a950 100644 --- a/src/rlx_prv_relup.erl +++ b/src/rlx_prv_relup.erl @@ -65,6 +65,15 @@ format_error({relup_script_generation_error, {missing_sasl, _}}}) -> "Unfortunately, due to requirements in systools, you need to have the sasl application " "in both the current release and the release to upgrade from."; +format_error({relup_script_generation_warn, systools_relup, + [{erts_vsn_changed, _}, + {erts_vsn_changed, _}]}) -> + "It has been detected that the ERTS version changed while generating the relup between versions, " + "please be aware that an instruction that will automatically restart the VM will be inserted in " + "this case"; +format_error({relup_script_generation_warn, Module, Warnings}) -> + ["Warnings generating relup \n", + rlx_util:indent(2), Module:format_warning(Warnings)]; format_error({relup_script_generation_error, Module, Errors}) -> ["Errors generating relup \n", rlx_util:indent(2), Module:format_error(Errors)]. @@ -119,10 +128,20 @@ get_up_release(State, Release, Vsn) -> make_upfrom_script(State, Release, UpFrom) -> OutputDir = rlx_state:output_dir(State), + WarningsAsErrors = rlx_state:warnings_as_errors(State), Options = [{outdir, OutputDir}, {path, rlx_util:get_code_paths(Release, OutputDir) ++ rlx_util:get_code_paths(UpFrom, OutputDir)}, silent], + %% the following block can be uncommented + %% when systools:make_relup/4 returns + %% {error,Module,Errors} instead of error + %% when taking the warnings_as_errors option + %% ++ + %% case WarningsAsErrors of + %% true -> [warnings_as_errors]; + %% false -> [] + % end, CurrentRel = strip_rel(rlx_release:relfile(Release)), UpFromRel = strip_rel(rlx_release:relfile(UpFrom)), ec_cmd_log:debug(rlx_state:log(State), @@ -138,14 +157,26 @@ make_upfrom_script(State, Release, UpFrom) -> [UpFromRel, CurrentRel]), {ok, State}; error -> - ?RLX_ERROR({relup_script_generation_error, CurrentRel, UpFromRel}); + ?RLX_ERROR({relup_generation_error, CurrentRel, UpFromRel}); {ok, RelUp, _, []} -> write_relup_file(State, Release, RelUp), ec_cmd_log:info(rlx_state:log(State), "relup successfully created!"), {ok, State}; - {ok,_, Module,Warnings} -> - ?RLX_ERROR({relup_script_generation_warn, Module, Warnings}); + {ok, RelUp, Module,Warnings} -> + case WarningsAsErrors of + true -> + %% since we don't pass the warnings_as_errors option + %% the relup file gets generated anyway, we need to delete + %% it + file:delete(filename:join([OutputDir, "relup"])), + ?RLX_ERROR({relup_script_generation_warn, Module, Warnings}); + false -> + write_relup_file(State, Release, RelUp), + ec_cmd_log:warn(rlx_state:log(State), + format_error({relup_script_generation_warn, Module, Warnings})), + {ok, State} + end; {error,Module,Errors} -> ?RLX_ERROR({relup_script_generation_error, Module, Errors}) end. diff --git a/src/rlx_release.erl b/src/rlx_release.erl index dc39e34..5765079 100644 --- a/src/rlx_release.erl +++ b/src/rlx_release.erl @@ -59,7 +59,7 @@ -record(release_t, {name :: atom(), vsn :: ec_semver:any_version(), - erts :: ec_semver:any_version(), + erts :: undefined | ec_semver:any_version(), goals = [] :: [rlx_depsolver:constraint()], realized = false :: boolean(), annotations = undefined :: annotations(), @@ -144,7 +144,12 @@ goals(#release_t{goals=Goals}) -> {ok, t()}. realize(Rel, Pkgs0, World0) -> World1 = subset_world(Pkgs0, World0), - process_specs(realize_erts(Rel), World1). + case rlx_topo:sort_apps(World1) of + {ok, Pkgs1} -> + process_specs(realize_erts(Rel), Pkgs1); + Error={error, _} -> + Error + end. %% @doc this gives the application specs for the release. This can only be %% populated by the 'realize' call in this module. @@ -239,6 +244,8 @@ format_goal(Constraint) -> rlx_depsolver:format_constraint(Constraint). -spec format_error(Reason::term()) -> iolist(). +format_error({topo_error, E}) -> + rlx_topo:format_error(E); format_error({failed_to_parse, Con}) -> io_lib:format("Failed to parse constraint ~p", [Con]); format_error({invalid_constraint, _, Con}) -> diff --git a/src/rlx_state.erl b/src/rlx_state.erl index 6974d52..5032628 100644 --- a/src/rlx_state.erl +++ b/src/rlx_state.erl @@ -82,8 +82,11 @@ upfrom/1, upfrom/2, format/1, - format/2]). - + format/2, + exclude_modules/1, + exclude_modules/2, + warnings_as_errors/1, + warnings_as_errors/2]). -export_type([t/0, releases/0, @@ -107,6 +110,7 @@ overrides=[] :: [{AppName::atom(), Directory::file:filename()}], skip_apps=[] :: [AppName::atom()], exclude_apps=[] :: [AppName::atom()], + exclude_modules=[] :: [{App::atom(), [Module::atom()]}], debug_info=keep :: keep | strip, configured_releases :: releases(), realized_releases :: releases(), @@ -114,7 +118,8 @@ include_src=true :: boolean(), upfrom :: string() | binary() | undefined, config_values :: ec_dictionary:dictionary(Key::atom(), - Value::term())}). + Value::term()), + warnings_as_errors=false :: boolean()}). %%============================================================================ %% types @@ -200,6 +205,15 @@ exclude_apps(#state_t{exclude_apps=Apps}) -> exclude_apps(State, SkipApps) -> State#state_t{exclude_apps=SkipApps}. +-spec exclude_modules(t()) -> [{App::atom(), [Module::atom()]}]. +exclude_modules(#state_t{exclude_modules=Modules}) -> + Modules. + +%% @doc modules to be excluded from the release +-spec exclude_modules(t(), [{App::atom(), [Module::atom()]}]) -> t(). +exclude_modules(State, SkipModules) -> + State#state_t{exclude_modules=SkipModules}. + -spec debug_info(t()) -> keep | strip. debug_info(#state_t{debug_info=DebugInfo}) -> DebugInfo. @@ -442,6 +456,14 @@ hooks(_State=#state_t{providers=Providers}, Target) -> Provider = providers:get_provider(Target, Providers), providers:hooks(Provider). +-spec warnings_as_errors(t()) -> boolean(). +warnings_as_errors(#state_t{warnings_as_errors=WarningsAsErrors}) -> + WarningsAsErrors. + +-spec warnings_as_errors(t(), boolean()) -> t(). +warnings_as_errors(State, WarningsAsErrors) -> + State#state_t{warnings_as_errors=WarningsAsErrors}. + %% =================================================================== %% Internal functions %% =================================================================== diff --git a/src/rlx_topo.erl b/src/rlx_topo.erl new file mode 100644 index 0000000..f8fc5ad --- /dev/null +++ b/src/rlx_topo.erl @@ -0,0 +1,190 @@ +%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*- +%%% Copyright 2012 Erlware, LLC. All Rights Reserved. +%%% +%%% This file is provided to you 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. +%%%------------------------------------------------------------------- +%%% @author Joe Armstrong +%%% @author Eric Merritt +%%% @author Konstantin Tcepliaev +%%% @doc +%%% This is a pretty simple topological sort for erlang. It was +%%% originally written for ermake by Joe Armstrong back in '98. It +%%% has been pretty heavily modified by Eric Merritt since '06 and modified again for Relx. +%%% Konstantin Tcepliaev rewrote the algorithm in 2017. +%%% +%%% A partial order on the set S is a set of pairs {Xi,Xj} such that +%%% some relation between Xi and Xj is obeyed. +%%% +%%% A topological sort of a partial order is a sequence of elements +%%% [X1, X2, X3 ...] such that if whenever {Xi, Xj} is in the partial +%%% order i < j +%%% +%%% This particular implementation guarantees that nodes closer to +%%% the top level of the graph will be put as close as possible to +%%% the beginning of the resulting list - this ensures that dependencies +%%% are started as late as possible, and top-level apps are started +%%% as early as possible. +%%% @end +%%%------------------------------------------------------------------- +-module(rlx_topo). + +-export([sort_apps/1, + format_error/1]). + +-include("relx.hrl"). + +%%==================================================================== +%% API +%%==================================================================== + +%% @doc This only does a topo sort on the list of applications and +%% assumes that there is only *one* version of each app in the list of +%% applications. This implies that you have already done the +%% constraint solve before you pass the list of apps here to be +%% sorted. +-spec sort_apps([rlx_app_info:t()]) -> + {ok, [rlx_app_info:t()]} | + relx:error(). +sort_apps(Apps) -> + AppDeps = [{rlx_app_info:name(App), + rlx_app_info:active_deps(App) ++ rlx_app_info:library_deps(App)} + || App <- Apps], + {AppNames, _} = lists:unzip(AppDeps), + case lists:foldl(fun iterator/2, {ok, [], AppDeps, []}, AppNames) of + {ok, Names, _, _} -> + {ok, names_to_apps(lists:reverse(Names), Apps)}; + E -> + E + end. + +%% @doc nicely format the error from the sort. +-spec format_error(Reason::term()) -> iolist(). +format_error({cycle, App, Path}) -> + ["Cycle detected in dependency graph, this must be resolved " + "before we can continue:\n", + rlx_util:indent(2), + [[erlang:atom_to_list(A), " -> "] || A <- lists:reverse(Path)], + erlang:atom_to_list(App)]. + +%%==================================================================== +%% Internal Functions +%%==================================================================== + +-type name() :: AppName::atom(). +-type app_dep() :: {AppName::name(), [DepName::name()]}. +-type iterator_state() :: {ok, [Acc::name()], + [Apps::app_dep()], + [Path::name()]}. + +-spec iterator(name(), iterator_state() | relx:error()) -> + iterator_state() | relx:error(). +iterator(App, {ok, Acc, Apps, Path}) -> + case lists:member(App, Acc) of + false -> + %% haven't seen this app yet + case lists:keytake(App, 1, Apps) of + {value, {App, Deps}, NewApps} -> + DepInit = {ok, Acc, NewApps, [App | Path]}, + %% recurse over deps + case lists:foldl(fun iterator/2, DepInit, Deps) of + {ok, DepAcc, DepApps, _} -> + {ok, [App | DepAcc], DepApps, Path}; + Error -> + Error + end; + false -> + %% we have visited this app before, + %% that means there's a cycle + ?RLX_ERROR({cycle, App, Path}) + end; + true -> + %% this app and its deps were already processed + {ok, Acc, Apps, Path} + end; +iterator(_, Error) -> + Error. + +-spec names_to_apps([atom()], [rlx_app_info:t()]) -> [rlx_app_info:t()]. +names_to_apps(Names, Apps) -> + [find_app_by_name(Name, Apps) || Name <- Names]. + +-spec find_app_by_name(atom(), [rlx_app_info:t()]) -> rlx_app_info:t(). +find_app_by_name(Name, Apps) -> + {ok, App1} = + ec_lists:find(fun(App) -> + rlx_app_info:name(App) =:= Name + end, Apps), + App1. + +%%==================================================================== +%% Tests +%%==================================================================== +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +topo_apps_cycle_test() -> + {ok, App1} = rlx_app_info:new(app1, "0.1", "/no-dir", [app2], [stdlib]), + {ok, App2} = rlx_app_info:new(app2, "0.1", "/no-dir", [app1], []), + Apps = [App1, App2], + ?assertMatch({error, {_, {cycle, app1, [app2, app1]}}}, + sort_apps(Apps)). + +topo_apps_good_test() -> + Apps = [App || + {ok, App} <- + [rlx_app_info:new(app1, "0.1", "/no-dir", [app2, zapp1], [stdlib, kernel]), + rlx_app_info:new(app2, "0.1", "/no-dir", [app3], []), + rlx_app_info:new(app3, "0.1", "/no-dir", [kernel], []), + rlx_app_info:new(zapp1, "0.1", "/no-dir", [app2,app3,zapp2], []), + rlx_app_info:new(stdlib, "0.1", "/no-dir", [], []), + rlx_app_info:new(kernel, "0.1", "/no-dir", [], []), + rlx_app_info:new(zapp2, "0.1", "/no-dir", [], [])]], + {ok, Sorted} = sort_apps(Apps), + ?assertMatch([kernel, app3, app2, zapp2, zapp1, stdlib, app1], + [rlx_app_info:name(App) || App <- Sorted]). + +topo_apps_1_test() -> + Apps = [App || + {ok, App} <- + [rlx_app_info:new(app0, "0.1", "/no-dir", [], [stdlib, dep1, dep2, dep3]), + rlx_app_info:new(app1, "0.1", "/no-dir", [], [stdlib, kernel]), + rlx_app_info:new(dep1, "0.1", "/no-dir", [], []), + rlx_app_info:new(dep2, "0.1", "/no-dir", [], []), + rlx_app_info:new(dep3, "0.1", "/no-dir", [], []), + rlx_app_info:new(stdlib, "0.1", "/no-dir", [], []), + rlx_app_info:new(kernel, "0.1", "/no-dir", [], [])]], + {ok, Sorted} = sort_apps(Apps), + ?assertMatch([stdlib, dep1, dep2, dep3, app0, kernel, app1], + [rlx_app_info:name(App) || App <- Sorted]). + +topo_apps_2_test() -> + Apps = [App || + {ok, App} <- + [rlx_app_info:new(app1, "0.1", "/no-dir", [app2, app3, app4, app5, + stdlib, kernel], + []), + rlx_app_info:new(app2, "0.1", "/no-dir", [stdlib, kernel], []), + rlx_app_info:new(app3, "0.1", "/no-dir", [stdlib, kernel], []), + rlx_app_info:new(app4, "0.1", "/no-dir", [stdlib, kernel], []), + rlx_app_info:new(app5, "0.1", "/no-dir", [stdlib, kernel], []), + rlx_app_info:new(stdlib, "0.1", "/no-dir", [], []), + rlx_app_info:new(kernel, "0.1", "/no-dir", [], []) + ]], + {ok, Sorted} = sort_apps(Apps), + ?assertMatch([stdlib, kernel, app2, + app3, app4, app5, app1], + [rlx_app_info:name(App) || App <- Sorted]). + +-endif. |