aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/rlx_app_discovery.erl15
-rw-r--r--src/rlx_app_info.erl14
-rw-r--r--src/rlx_cmd_args.erl3
-rw-r--r--src/rlx_config.erl18
-rw-r--r--src/rlx_depsolver.erl2
-rw-r--r--src/rlx_goal.erl2
-rw-r--r--src/rlx_prv_assembler.erl263
-rw-r--r--src/rlx_prv_overlay.erl30
-rw-r--r--src/rlx_prv_relup.erl37
-rw-r--r--src/rlx_release.erl11
-rw-r--r--src/rlx_state.erl28
-rw-r--r--src/rlx_topo.erl190
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 &lt; 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.