%% -*- 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 Eric Merritt %%% @copyright (C) 2012 Erlware, LLC. %%% %%% @doc Given a complete built release this provider assembles that release %%% into a release directory. -module(rlx_prv_assembler). -behaviour(provider). -export([init/1, do/1, format_error/1]). -include("relx.hrl"). -define(PROVIDER, release). -define(DEPS, [resolve_release]). %%============================================================================ %% API %%============================================================================ -spec init(rlx_state:t()) -> {ok, rlx_state:t()}. init(State) -> State1 = rlx_state:add_provider(State, providers:create([{name, ?PROVIDER}, {module, ?MODULE}, {deps, ?DEPS}, {hooks, {[], [overlay]}}])), {ok, State1}. %% @doc recursively dig down into the library directories specified in the state %% looking for OTP Applications -spec do(rlx_state:t()) -> {ok, rlx_state:t()} | relx:error(). do(State) -> print_dev_mode(State), {RelName, RelVsn} = rlx_state:default_configured_release(State), Release = rlx_state:get_realized_release(State, RelName, RelVsn), OutputDir = rlx_state:output_dir(State), case create_output_dir(OutputDir) of ok -> case rlx_release:realized(Release) of true -> case copy_app_directories_to_output(State, Release, OutputDir) of {ok, State1} -> case rlx_state:debug_info(State1) =:= strip andalso rlx_state:dev_mode(State1) =:= false of true -> case beam_lib:strip_release(OutputDir) of {ok, _} -> {ok, State1}; {error, _, Reason} -> ?RLX_ERROR({strip_release, Reason}) end; false -> {ok, State1} end; E -> E end; false -> ?RLX_ERROR({unresolved_release, RelName, RelVsn}) end; Error -> Error end. -spec format_error(ErrorDetail::term()) -> iolist(). format_error({unresolved_release, RelName, RelVsn}) -> io_lib:format("The release has not been resolved ~p-~s", [RelName, RelVsn]); format_error({ec_file_error, AppDir, TargetDir, E}) -> io_lib:format("Unable to copy OTP App from ~s to ~s due to ~p", [AppDir, TargetDir, E]); format_error({vmargs_does_not_exist, Path}) -> io_lib:format("The vm.args file specified for this release (~s) does not exist!", [Path]); format_error({vmargs_src_does_not_exist, Path}) -> io_lib:format("The vm.args.src file specified for this release (~s) does not exist!", [Path]); format_error({config_does_not_exist, Path}) -> io_lib:format("The sys.config file specified for this release (~s) does not exist!", [Path]); format_error({config_src_does_not_exist, Path}) -> io_lib:format("The sys.config.src file specified for this release (~s) does not exist!", [Path]); format_error({sys_config_parse_error, ConfigPath, Reason}) -> io_lib:format("The config file (~s) specified for this release could not be opened or parsed: ~s", [ConfigPath, file:format_error(Reason)]); format_error({specified_erts_does_not_exist, ErtsVersion}) -> io_lib:format("Specified version of erts (~s) does not exist", [ErtsVersion]); format_error({release_script_generation_error, RelFile}) -> io_lib:format("Unknown internal release error generating the release file to ~s", [RelFile]); format_error({release_script_generation_warning, Module, Warnings}) -> ["Warnings generating release \s", rlx_util:indent(2), Module:format_warning(Warnings)]; format_error({unable_to_create_output_dir, OutputDir}) -> io_lib:format("Unable to create output directory (possible permissions issue): ~s", [OutputDir]); format_error({release_script_generation_error, Module, Errors}) -> ["Errors generating release \n", rlx_util:indent(2), Module:format_error(Errors)]; format_error({unable_to_make_symlink, AppDir, TargetDir, Reason}) -> io_lib:format("Unable to symlink directory ~s to ~s because \n~s~s", [AppDir, TargetDir, rlx_util:indent(2), file:format_error(Reason)]); format_error(boot_script_generation_error) -> "Unknown internal release error generating start_clean.boot"; format_error({boot_script_generation_warning, Module, Warnings}) -> ["Warnings generating start_clean.boot \s", rlx_util:indent(2), Module:format_warning(Warnings)]; format_error({boot_script_generation_error, Module, Errors}) -> ["Errors generating start_clean.boot \n", 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)]); format_error({rewrite_app_file, AppFile, Error}) -> io_lib:format("Unable to rewrite .app file ~s due to ~p", [AppFile, Error]). %%%=================================================================== %%% Internal Functions %%%=================================================================== print_dev_mode(State) -> case rlx_state:dev_mode(State) of true -> ec_cmd_log:info(rlx_state:log(State), "Dev mode enabled, release will be symlinked"); false -> ok end. -spec create_output_dir(file:name()) -> ok | {error, Reason::term()}. create_output_dir(OutputDir) -> case ec_file:is_dir(OutputDir) of false -> case rlx_util:mkdir_p(OutputDir) of ok -> ok; {error, _} -> ?RLX_ERROR({unable_to_create_output_dir, OutputDir}) end; true -> ok end. copy_app_directories_to_output(State, Release, OutputDir) -> LibDir = filename:join([OutputDir, "lib"]), ok = ec_file:mkdir_p(LibDir), IncludeSrc = rlx_state:include_src(State), IncludeErts = rlx_state:get(State, include_erts, true), Apps = prepare_applications(State, rlx_release:application_details(Release)), Result = lists:filter(fun({error, _}) -> true; (_) -> false end, lists:flatten(ec_plists:map(fun(App) -> copy_app(State, LibDir, App, IncludeSrc, IncludeErts) end, Apps))), case Result of [E | _] -> E; [] -> create_release_info(State, Release, OutputDir) end. prepare_applications(State, Apps) -> case rlx_state:dev_mode(State) of true -> [rlx_app_info:link(App, true) || App <- Apps]; false -> Apps end. 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), TargetDir = filename:join([LibDir, AppName ++ "-" ++ AppVsn]), case AppDir == ec_cnv:to_binary(TargetDir) of true -> %% No need to do anything here, discover found something already in %% a release dir ok; false -> case IncludeErts of false -> case is_erts_lib(AppDir) of true -> []; false -> copy_app_(State, App, AppDir, TargetDir, IncludeSrc) end; _ -> 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_(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(State, App, AppDir); false -> 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(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, 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, Spec) -> ToWrite = io_lib:format("~p.\n", Spec), case file:consult(Filename) of {ok, Spec} -> ok; {ok, _} -> file:write_file(Filename, ToWrite); {error, _} -> file:write_file(Filename, ToWrite) end. remove_symlink_or_directory(TargetDir) -> case ec_file:is_symlink(TargetDir) of true -> ec_file:remove(TargetDir); false -> case ec_file:is_dir(TargetDir) of true -> ok = ec_file:remove(TargetDir, [recursive]); false -> ok end end. link_directory(AppDir, TargetDir) -> case rlx_util:symlink_or_copy(AppDir, TargetDir) of {error, Reason} -> ?RLX_ERROR({unable_to_make_symlink, AppDir, TargetDir, Reason}); ok -> ok end. copy_directory(State, App, AppDir, TargetDir, IncludeSrc) -> [copy_dir(State, App, AppDir, TargetDir, SubDir) || SubDir <- ["ebin", "include", "priv", "lib" | case IncludeSrc of true -> ["src", "c_src"]; false -> [] end]]. 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), %% 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 -> ok end; false -> ok end. %% no files are excluded, just copy the whole dir copy_dir(SourceDir, TargetDir, []) -> case ec_file:copy(SourceDir, TargetDir, [recursive, {file_info, [mode, time]}]) 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)]), [{file_info, [mode, time]}]) 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), ReleaseFile = filename:join([ReleaseDir, RelName ++ ".rel"]), StartCleanFile = filename:join([ReleaseDir, "start_clean.rel"]), NoDotErlFile = filename:join([ReleaseDir, "no_dot_erlang.rel"]), ok = ec_file:mkdir_p(ReleaseDir), Release1 = rlx_release:relfile(Release0, ReleaseFile), State1 = rlx_state:update_realized_release(State0, Release1), case rlx_release:metadata(Release1) of {ok, Meta} -> case {rlx_release:start_clean_metadata(Release1), rlx_release:no_dot_erlang_metadata(Release1)} of {{ok, StartCleanMeta}, {ok, NoDotErlMeta}} -> ok = ec_file:write_term(ReleaseFile, Meta), ok = ec_file:write_term(StartCleanFile, StartCleanMeta), ok = ec_file:write_term(NoDotErlFile, NoDotErlMeta), write_bin_file(State1, Release1, OutputDir, ReleaseDir); {{ok, _}, E} -> E; {_, E} -> E end; E -> E end. write_bin_file(State, Release, OutputDir, RelDir) -> RelName = erlang:atom_to_list(rlx_release:name(Release)), RelVsn = rlx_release:vsn(Release), BinDir = filename:join([OutputDir, "bin"]), ok = ec_file:mkdir_p(BinDir), VsnRel = filename:join(BinDir, rlx_release:canonical_name(Release)), BareRel = filename:join(BinDir, RelName), ErlOpts = rlx_state:get(State, erl_opts, ""), {OsFamily, _OsName} = rlx_util:os_type(State), StartFile = case rlx_state:get(State, extended_start_script, false) of false -> case rlx_state:get(State, include_nodetool, false) of true -> include_nodetool(BinDir); false -> ok end, bin_file_contents(OsFamily, RelName, RelVsn, rlx_release:erts(Release), ErlOpts); true -> %% 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), Extensions = rlx_state:get(State, extended_start_script_extensions, []), extended_bin_file_contents(OsFamily, RelName, RelVsn, rlx_release:erts(Release), ErlOpts, Hooks, Extensions) end, %% We generate the start script by default, unless the user %% tells us not too case rlx_state:get(State, generate_start_script, true) of false -> ok; _ -> VsnRelStartFile = case OsFamily of unix -> VsnRel; win32 -> rlx_string:concat(VsnRel, ".cmd") end, ok = file:write_file(VsnRelStartFile, StartFile), ok = file:change_mode(VsnRelStartFile, 8#755), BareRelStartFile = case OsFamily of unix -> BareRel; win32 -> rlx_string:concat(BareRel, ".cmd") end, ok = file:write_file(BareRelStartFile, StartFile), ok = file:change_mode(BareRelStartFile, 8#755) end, ReleasesDir = filename:join(OutputDir, "releases"), generate_start_erl_data_file(Release, ReleasesDir), copy_or_generate_vmargs_file(State, Release, RelDir), case copy_or_generate_sys_config_file(State, RelDir) of ok -> include_erts(State, Release, OutputDir, RelDir); E -> 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) -> rlx_string:join(["hooks/builtin/pid", "/var/run/$REL_NAME.pid"], "|"); hook_invocation({pid, PidFile}) -> rlx_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 rlx_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(), NodeTool = filename:join([BinDir, "nodetool"]), InstallUpgrade = filename:join([BinDir, "install_upgrade.escript"]), ok = file:write_file(NodeTool, NodeToolFile), ok = file:write_file(InstallUpgrade, InstallUpgradeFile). %% @doc generate a start_erl.data file -spec generate_start_erl_data_file(rlx_release:t(), file:name()) -> ok | relx:error(). generate_start_erl_data_file(Release, ReleasesDir) -> ErtsVersion = rlx_release:erts(Release), ReleaseVersion = rlx_release:vsn(Release), Data = ErtsVersion ++ " " ++ ReleaseVersion, ok = file:write_file(filename:join(ReleasesDir, "start_erl.data"), Data). %% @doc copy vm.args or generate one to releases/VSN/vm.args -spec copy_or_generate_vmargs_file(rlx_state:t(), rlx_release:t(), file:name()) -> {ok, rlx_state:t()} | relx:error(). copy_or_generate_vmargs_file(State, Release, RelDir) -> RelVmargsPath = filename:join([RelDir, "vm.args"]), RelVmargsSrcPath = filename:join([RelDir, "vm.args.src"]), case rlx_state:vm_args_src(State) of undefined -> case rlx_state:vm_args(State) of false -> ok; undefined -> RelName = erlang:atom_to_list(rlx_release:name(Release)), unless_exists_write_default(RelVmargsPath, vm_args_file(RelName)); ArgsPath -> case filelib:is_regular(ArgsPath) of false -> ?RLX_ERROR({vmargs_does_not_exist, ArgsPath}); true -> copy_or_symlink_config_file(State, ArgsPath, RelVmargsPath) end end; ArgsSrcPath -> %% print a warning if vm_args is also set case rlx_state:vm_args(State) of undefined -> ok; _-> ec_cmd_log:warn(rlx_state:log(State), "Both vm_args_src and vm_args are set, vm_args will be ignored~n", []) end, case filelib:is_regular(ArgsSrcPath) of false -> ?RLX_ERROR({vmargs_src_does_not_exist, ArgsSrcPath}); true -> copy_or_symlink_config_file(State, ArgsSrcPath, RelVmargsSrcPath) end end. %% @doc copy config/sys.config or generate one to releases/VSN/sys.config -spec copy_or_generate_sys_config_file(rlx_state:t(), file:name()) -> {ok, rlx_state:t()} | relx:error(). copy_or_generate_sys_config_file(State, RelDir) -> RelSysConfPath = filename:join([RelDir, "sys.config"]), RelSysConfSrcPath = filename:join([RelDir, "sys.config.src"]), case rlx_state:sys_config_src(State) of undefined -> case rlx_state:sys_config(State) of false -> ok; undefined -> unless_exists_write_default(RelSysConfPath, sys_config_file()); ConfigPath -> case filelib:is_regular(ConfigPath) of false -> ?RLX_ERROR({config_does_not_exist, ConfigPath}); true -> %% validate sys.config is valid Erlang terms case file:consult(ConfigPath) of {ok, _} -> copy_or_symlink_config_file(State, ConfigPath, RelSysConfPath); {error, Reason} -> ?RLX_ERROR({sys_config_parse_error, ConfigPath, Reason}) end end end; ConfigSrcPath -> %% print a warning if sys_config is also set case rlx_state:sys_config(State) of P when P =:= false orelse P =:= undefined -> ok; _-> ec_cmd_log:warn(rlx_state:log(State), "Both sys_config_src and sys_config are set, sys_config will be ignored~n", []) end, case filelib:is_regular(ConfigSrcPath) of false -> ?RLX_ERROR({config_src_does_not_exist, ConfigSrcPath}); true -> copy_or_symlink_config_file(State, ConfigSrcPath, RelSysConfSrcPath) end end. %% @doc copy config/sys.config or generate one to releases/VSN/sys.config -spec copy_or_symlink_config_file(rlx_state:t(), file:name(), file:name()) -> ok. 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); _ -> ok = ec_file:copy(ConfigPath, RelConfPath, [{file_info, [mode, time]}]) end. %% @doc Optionally add erts directory to release, if defined. -spec include_erts(rlx_state:t(), rlx_release:t(), file:name(), file:name()) -> {ok, rlx_state:t()} | relx:error(). include_erts(State, Release, OutputDir, RelDir) -> Prefix = case rlx_state:get(State, include_erts, true) of false -> false; true -> code:root_dir(); P -> filename:absname(P) end, case Prefix of false -> make_boot_script(State, Release, OutputDir, RelDir); _ -> ec_cmd_log:info(rlx_state:log(State), "Including Erts from ~s~n", [Prefix]), ErtsVersion = rlx_release:erts(Release), ErtsDir = filename:join([Prefix, "erts-" ++ ErtsVersion]), LocalErts = filename:join([OutputDir, "erts-" ++ ErtsVersion]), {OsFamily, _OsName} = rlx_util:os_type(State), case ec_file:is_dir(ErtsDir) of false -> ?RLX_ERROR({specified_erts_does_not_exist, ErtsVersion}); true -> ok = ec_file:mkdir_p(LocalErts), ok = ec_file:copy(ErtsDir, LocalErts, [recursive, {file_info, [mode, time]}]), case OsFamily of unix -> Erl = filename:join([LocalErts, "bin", "erl"]), ok = ec_file:remove(Erl), ok = file:write_file(Erl, erl_script(ErtsVersion)), ok = file:change_mode(Erl, 8#755); win32 -> ErlIni = filename:join([LocalErts, "bin", "erl.ini"]), ok = ec_file:remove(ErlIni), 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 -> NodeToolFile = nodetool_contents(), InstallUpgradeFile = install_upgrade_escript_contents(), NodeTool = filename:join([LocalErts, "bin", "nodetool"]), InstallUpgrade = filename:join([LocalErts, "bin", "install_upgrade.escript"]), ok = file:write_file(NodeTool, NodeToolFile), ok = file:write_file(InstallUpgrade, InstallUpgradeFile), ok = file:change_mode(NodeTool, 8#755), ok = file:change_mode(InstallUpgrade, 8#755); false -> ok end, make_boot_script(State, Release, OutputDir, RelDir) end end. -spec make_boot_script(rlx_state:t(), rlx_release:t(), file:name(), file:name()) -> {ok, rlx_state:t()} | relx:error(). make_boot_script(State, Release, OutputDir, RelDir) -> Options = [{path, [RelDir | rlx_util:get_code_paths(Release, OutputDir)]}, {outdir, RelDir}, {variables, make_boot_script_variables(State)}, no_module_tests, silent], Name = erlang:atom_to_list(rlx_release:name(Release)), ReleaseFile = filename:join([RelDir, Name ++ ".rel"]), case rlx_util:make_script(Options, fun(CorrectedOptions) -> systools:make_script(Name, CorrectedOptions) end) of ok -> ec_cmd_log:info(rlx_state:log(State), "release successfully created!"), create_RELEASES(OutputDir, ReleaseFile), create_no_dot_erlang(RelDir, OutputDir, Options, State), create_start_clean(RelDir, OutputDir, Options, State); error -> ?RLX_ERROR({release_script_generation_error, ReleaseFile}); {ok, _, []} -> ec_cmd_log:info(rlx_state:log(State), "release successfully created!"), create_RELEASES(OutputDir, ReleaseFile), create_no_dot_erlang(RelDir, OutputDir, Options, State), create_start_clean(RelDir, OutputDir, Options, State); {ok,Module,Warnings} -> ?RLX_ERROR({release_script_generation_warn, Module, Warnings}); {error,Module,Error} -> ?RLX_ERROR({release_script_generation_error, Module, Error}) end. make_boot_script_variables(State) -> % A boot variable is needed when {include_erts, false} and the application % directories are split between the release/lib directory and the erts/lib % directory. % The built-in $ROOT variable points to the erts directory on Windows % (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 % 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 % usage used in the start up scripts case {os:type(), rlx_state:get(State, include_erts, true)} of {{win32, _}, false} -> [{"RELEASE_DIR", rlx_state:output_dir(State)}]; {{win32, _}, true} -> []; _ -> [{"ERTS_LIB_DIR", code:lib_dir()}] end. create_no_dot_erlang(RelDir, OutputDir, Options, State) -> create_boot_file(RelDir, OutputDir, Options, State, "no_dot_erlang"). create_start_clean(RelDir, OutputDir, Options, State) -> create_boot_file(RelDir, OutputDir, Options, State, "start_clean"). create_boot_file(RelDir, OutputDir, Options, State, Name) -> case rlx_util:make_script(Options, fun(CorrectedOptions) -> systools:make_script(Name, CorrectedOptions) end) of ok -> ok = ec_file:copy(filename:join([RelDir, Name++".boot"]), filename:join([OutputDir, "bin", Name++".boot"]), [{file_info, [mode, time]}]), ec_file:remove(filename:join([RelDir, Name++".rel"])), ec_file:remove(filename:join([RelDir, Name++".script"])), {ok, State}; error -> ?RLX_ERROR(boot_script_generation_error); {ok, _, []} -> ok = ec_file:copy(filename:join([RelDir, Name++".boot"]), filename:join([OutputDir, "bin", Name++".boot"]), [{file_info, [mode, time]}]), ec_file:remove(filename:join([RelDir, Name++".rel"])), ec_file:remove(filename:join([RelDir, Name++".script"])), {ok, State}; {ok,Module,Warnings} -> ?RLX_ERROR({boot_script_generation_warn, Module, Warnings}); {error,Module,Error} -> ?RLX_ERROR({boot_script_generation_error, Module, Error}) end. create_RELEASES(OutputDir, ReleaseFile) -> {ok, OldCWD} = file:get_cwd(), file:set_cwd(OutputDir), release_handler:create_RELEASES("./", "releases", ReleaseFile, []), file:set_cwd(OldCWD). unless_exists_write_default(Path, File) -> case ec_file:exists(Path) of true -> ok; false -> ok = file:write_file(Path, File) end. -spec ensure_not_exist(file:name()) -> ok. ensure_not_exist(RelConfPath) -> case ec_file:exists(RelConfPath) of false -> ok; _ -> ec_file:remove(RelConfPath) end. erl_script(ErtsVsn) -> render(erl_script, [{erts_vsn, ErtsVsn}]). bin_file_contents(OsFamily, RelName, RelVsn, ErtsVsn, ErlOpts) -> Template = case OsFamily of unix -> bin; win32 -> bin_windows end, render(Template, [{rel_name, RelName}, {rel_vsn, RelVsn}, {erts_vsn, ErtsVsn}, {erl_opts, ErlOpts}]). extended_bin_file_contents(OsFamily, RelName, RelVsn, ErtsVsn, ErlOpts, Hooks, Extensions) -> Template = case OsFamily of unix -> extended_bin; win32 -> extended_bin_windows end, %% turn all the hook lists into space separated strings PreStartHooks = rlx_string:join(proplists:get_value(pre_start, Hooks, []), " "), PostStartHooks = rlx_string:join(proplists:get_value(post_start, Hooks, []), " "), PreStopHooks = rlx_string:join(proplists:get_value(pre_stop, Hooks, []), " "), PostStopHooks = rlx_string:join(proplists:get_value(post_stop, Hooks, []), " "), PreInstallUpgradeHooks = rlx_string:join(proplists:get_value(pre_install_upgrade, Hooks, []), " "), PostInstallUpgradeHooks = rlx_string:join(proplists:get_value(post_install_upgrade, Hooks, []), " "), StatusHook = rlx_string:join(proplists:get_value(status, Hooks, []), " "), {ExtensionsList1, ExtensionDeclarations1} = lists:foldl(fun({Name, Script}, {ExtensionsList0, ExtensionDeclarations0}) -> ExtensionDeclaration = atom_to_list(Name) ++ "_extension=\"" ++ Script ++ "\"", {ExtensionsList0 ++ [atom_to_list(Name)], ExtensionDeclarations0 ++ [ExtensionDeclaration]} end, {[], []}, Extensions), % pipe separated string of extensions, to show on the start script usage % (eg. foo|bar) ExtensionsList = rlx_string:join(ExtensionsList1 ++ ["undefined"], "|"), % command separated string of extension script declarations % (eg. foo_extension="path/to/foo_script") ExtensionDeclarations = rlx_string:join(ExtensionDeclarations1, ";"), render(Template, [{rel_name, RelName}, {rel_vsn, RelVsn}, {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}, {extensions, ExtensionsList}, {extension_declarations, ExtensionDeclarations}]). erl_ini(OutputDir, ErtsVsn) -> ErtsDirName = rlx_string:concat("erts-", ErtsVsn), BinDir = filename:join([OutputDir, ErtsDirName, bin]), render(erl_ini, [{bin_dir, BinDir}, {output_dir, OutputDir}]). install_upgrade_escript_contents() -> render(install_upgrade_escript). nodetool_contents() -> render(nodetool). sys_config_file() -> render(sys_config). vm_args_file(RelName) -> render(vm_args, [{rel_name, RelName}]). render(Template) -> render(Template, []). render(Template, Data) -> Files = rlx_util:template_files(), Tpl = rlx_util:load_file(Files, escript, atom_to_list(Template)), {ok, Content} = rlx_util:render(Tpl, Data), Content.