rebar3/src/rebar_relx.erl

299 lines
12 KiB
Erlang

%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
%% ex: ts=4 sw=4 et
-module(rebar_relx).
-export([do/2,
opt_spec_list/0,
format_error/1]).
-ifdef(TEST).
-export([merge_overlays/1]).
-endif.
-include_lib("providers/include/providers.hrl").
-include("rebar.hrl").
%% ===================================================================
%% Public API
%% ===================================================================
-spec do(atom(), rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(Provider, State) ->
{Opts, _} = rebar_state:command_parsed_args(State),
RelxConfig = read_relx_config(State, Opts),
ProfileString = rebar_dir:profile_dir_name(State),
ExtraOverlays = [{profile_string, ProfileString}],
CurrentProfiles = rebar_state:current_profiles(State),
RelxMode = case lists:member(prod, CurrentProfiles) of
true ->
[{mode, prod}];
false ->
[]
end,
DefaultOutputDir = filename:join(rebar_dir:base_dir(State), ?DEFAULT_RELEASE_DIR),
RelxConfig1 = RelxMode ++ [output_dir(DefaultOutputDir, Opts),
{overlay_vars_values, ExtraOverlays},
{overlay_vars, [{base_dir, rebar_dir:base_dir(State)} | overlay_vars(Opts)]}
| merge_overlays(RelxConfig)],
Args = [include_erts, system_libs, vm_args, sys_config],
RelxConfig2 = maybe_obey_command_args(RelxConfig1, Opts, Args),
{ok, RelxState_0} = rlx_config:to_state(RelxConfig2),
XrefIgnores = rebar_state:get(State, xref_ignores, []),
RelxState = rlx_state:filter_xref_warning(RelxState_0,
fun(Warnings) ->
rebar_prv_xref:filter_xref_results(undefined_function_calls, XrefIgnores, Warnings) end),
Providers = rebar_state:providers(State),
Cwd = rebar_state:dir(State),
rebar_hooks:run_project_and_app_hooks(Cwd, pre, Provider, Providers, State),
Releases = releases_to_build(Provider, Opts, RelxState),
case Provider of
relup ->
{Release, ToVsn} =
%% hd/1 can't fail because --all is not a valid option to relup
case Releases of
[{Rel,Vsn}|_] when is_atom(Rel) ->
%% This is returned if --relvsn and --relname are given
{Rel, Vsn};
[undefined|_] ->
erlang:error(?PRV_ERROR(unknown_release));
[Rel|_] when is_atom(Rel) ->
erlang:error(?PRV_ERROR(unknown_vsn))
end,
UpFromVsn = proplists:get_value(upfrom, Opts, undefined),
relx:build_relup(Release, ToVsn, UpFromVsn, RelxState);
_ ->
parallel_run(Provider, Releases, all_apps(State), RelxState)
end,
rebar_hooks:run_project_and_app_hooks(Cwd, post, Provider, Providers, State),
{ok, State}.
read_relx_config(State, Options) ->
ConfigFile = proplists:get_value(config, Options, []),
case ConfigFile of
"" ->
ConfigPath = filename:join([rebar_dir:root_dir(State), "relx.config"]),
case {rebar_state:get(State, relx, []), file:consult(ConfigPath)} of
{[], {ok, Config}} ->
?DEBUG("Configuring releases with relx.config", []),
Config;
{Config, {error, enoent}} ->
?DEBUG("Configuring releases the {relx, ...} entry"
" from rebar.config", []),
Config;
{_, {error, Reason}} ->
erlang:error(?PRV_ERROR({config_file, "relx.config", Reason}));
{RebarConfig, {ok, _RelxConfig}} ->
?WARN("Found conflicting relx configs, configuring releases"
" with rebar.config", []),
RebarConfig
end;
ConfigFile ->
case file:consult(ConfigFile) of
{ok, Config} ->
?DEBUG("Configuring releases with: ~ts", [ConfigFile]),
Config;
{error, Reason} ->
erlang:error(?PRV_ERROR({config_file, ConfigFile, Reason}))
end
end.
-spec format_error(any()) -> iolist().
format_error(unknown_release) ->
"Option --relname is missing";
format_error(unknown_vsn) ->
"Option --relvsn is missing";
format_error(all_relup) ->
"Option --all can not be applied to `relup` command";
format_error({config_file, Filename, Error}) ->
io_lib:format("Failed to read config file ~ts: ~p", [Filename, Error]);
format_error({release_not_found, Relname}) ->
io_lib:format("No releases exists in the system for ~s!", [Relname]);
format_error(Error) ->
io_lib:format("~p", [Error]).
%%
parallel_run(release, [Release], AllApps, RelxState) ->
relx:build_release(Release, AllApps, RelxState);
parallel_run(tar, [Release], AllApps, RelxState) ->
relx:build_tar(Release, AllApps, RelxState);
parallel_run(Provider, Releases, AllApps, RelxState) ->
rebar_parallel:queue(Releases, fun rel_worker/2, [Provider, AllApps, RelxState], fun rel_handler/2, []).
rel_worker(Release, [Provider, Apps, RelxState]) ->
try
case Provider of
release ->
relx:build_release(Release, Apps, RelxState);
tar ->
relx:build_tar(Release, Apps, RelxState)
end
catch
error:Error ->
{Release, Error}
end.
rel_handler({{Name, Vsn}, {error, {Module, Reason}}}, _Args) ->
?ERROR("Error building release ~ts-~ts:~n~ts~ts", [Name, Vsn, rebar_utils:indent(1),
Module:format_error(Reason)]),
ok;
rel_handler({{Name, Vsn}, Other}, _Args) ->
?ERROR("Error building release ~ts-~ts:~nUnknown return value: ~p", [Name, Vsn, Other]),
ok;
rel_handler({ok, _}, _) ->
ok.
releases_to_build(Provider, Opts, RelxState)->
case {proplists:get_value(all, Opts, undefined),
proplists:get_value(relnames, Opts, undefined)} of
{undefined, undefined} ->
case proplists:get_value(relname, Opts, undefined) of
undefined ->
[undefined];
R ->
case proplists:get_value(relvsn, Opts, undefined) of
undefined ->
[list_to_atom(R)];
RelVsn ->
[{list_to_atom(R), RelVsn}]
end
end;
{true, _} when Provider =:= relup ->
erlang:error(?PRV_ERROR(all_relup));
{true, _} ->
highest_unique_releases(rlx_state:configured_releases(RelxState));
{_, Filter} ->
Releases = highest_unique_releases(rlx_state:configured_releases(RelxState)),
WantReleases = [list_to_atom(Rel) || Rel <- string:split(Filter, ",", all)],
[
case proplists:lookup(Relname, Releases) of
none -> erlang:error(?PRV_ERROR({release_not_found, Relname}));
Rel -> Rel
end
|| Relname <- WantReleases
]
end.
%% takes a map of relx configured releases and returns a list of the highest
%% version for each unique release name
-spec highest_unique_releases(rlx_state:releases()) -> [{atom(), string() | undefined}].
highest_unique_releases(Releases) ->
Unique = maps:fold(fun({Name, Vsn}, _, Acc) ->
update_map_if_higher(Name, Vsn, Acc)
end, #{}, Releases),
maps:to_list(Unique).
update_map_if_higher(Name, Vsn, Acc) ->
maps:update_with(Name, fun(Vsn1) ->
case rlx_util:parsed_vsn_lte(rlx_util:parse_vsn(Vsn1),
rlx_util:parse_vsn(Vsn)) of
true ->
Vsn;
false ->
Vsn1
end
end, Vsn, Acc).
%% Don't override output_dir if the user passed one on the command line
output_dir(DefaultOutputDir, Options) ->
{output_dir, proplists:get_value(output_dir, Options, DefaultOutputDir)}.
merge_overlays(Config) ->
{Overlays, Others} =
lists:partition(fun(C) when element(1, C) =:= overlay -> true;
(_) -> false
end, Config),
%% Have profile overlay entries come before others to match how profiles work elsewhere
NewOverlay = lists:flatmap(fun({overlay, Overlay}) -> Overlay end, lists:reverse(Overlays)),
[{overlay, NewOverlay} | Others].
overlay_vars(Opts) ->
case proplists:get_value(overlay_vars, Opts) of
undefined ->
[];
[] ->
[];
FileName when is_list(FileName) ->
[FileName]
end.
maybe_obey_command_args(RelxConfig, Opts, Args) ->
lists:foldl(
fun(Opt, Acc) ->
case proplists:get_value(Opt, Opts) of
undefined ->
Acc;
V ->
lists:keystore(Opt, 1, Acc, {Opt, V})
end
end, RelxConfig, Args).
%%
%% Returns a map of all apps that are part of the rebar3 project.
%% This means the project apps and dependencies but not OTP libraries.
-spec all_apps(rebar_state:t()) -> #{atom() => rlx_app_info:t()}.
all_apps(State) ->
maps:merge(app_infos_to_relx(rebar_state:project_apps(State), project),
app_infos_to_relx(rebar_state:all_deps(State), dep)).
%%
-spec app_infos_to_relx([rlx_app_info:t()], rlx_app_info:app_type()) -> #{atom() => rlx_app_info:t()}.
app_infos_to_relx(AppInfos, AppType) ->
lists:foldl(fun(AppInfo, Acc) ->
Acc#{binary_to_atom(rebar_app_info:name(AppInfo), utf8)
=> app_info_to_relx(rebar_app_info:app_to_map(AppInfo), AppType)}
end, #{}, AppInfos).
app_info_to_relx(#{name := Name,
vsn := Vsn,
applications := Applications,
included_applications := IncludedApplications,
optional_applications := OptionalApplications,
dir := Dir,
link := false}, AppType) ->
rlx_app_info:new(Name, Vsn, Dir, Applications, IncludedApplications, OptionalApplications, AppType).
-spec opt_spec_list() -> [getopt:option_spec()].
opt_spec_list() ->
[{all, undefined, "all", boolean,
"If true runs the command against all configured releases"},
{relname, $n, "relname", string,
"Specify the name for the release that will be generated"},
{relvsn, $v, "relvsn", string, "Specify the version for the release"},
{upfrom, $u, "upfrom", string,
"Only valid with relup target, specify the release to upgrade from"},
{output_dir, $o, "output-dir", string,
"The output directory for the release. This is `./` by default."},
{help, $h, "help", undefined,
"Print usage"},
{lib_dir, $l, "lib-dir", string,
"Additional dir that should be searched for OTP Apps"},
{dev_mode, $d, "dev-mode", boolean,
"Symlink the applications and configuration into the release instead of copying"},
{include_erts, $i, "include-erts", string,
"If true include a copy of erts used to build with, if a path include erts at that path. If false, do not include erts"},
{override, $a, "override", string,
"Provide an app name and a directory to override in the form <appname>:<app directory>"},
{config, $c, "config", {string, ""}, "The path to a config file"},
{overlay_vars, undefined, "overlay_vars", string, "Path to a file of overlay variables"},
{vm_args, undefined, "vm_args", string, "Path to a file to use for vm.args"},
{sys_config, undefined, "sys_config", string, "Path to a file to use for sys.config"},
{system_libs, undefined, "system_libs", string, "Boolean or path to dir of Erlang system libs"},
{version, undefined, "version", undefined, "Print relx version"},
{root_dir, $r, "root", string, "The project root directory"},
{relnames, $m, "relnames", string, "Like --all, but only build the releases in the list, e.g. --relnames rel1,rel2"}].