1%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
2%% ex: ts=4 sw=4 et
3
4-module(rebar_relx).
5
6-export([do/2,
7         opt_spec_list/0,
8         format_error/1]).
9
10-ifdef(TEST).
11-export([merge_overlays/1]).
12-endif.
13
14-include_lib("providers/include/providers.hrl").
15-include("rebar.hrl").
16
17%% ===================================================================
18%% Public API
19%% ===================================================================
20
21-spec do(atom(), rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
22do(Provider, State) ->
23    {Opts, _} = rebar_state:command_parsed_args(State),
24    RelxConfig = read_relx_config(State, Opts),
25
26    ProfileString = rebar_dir:profile_dir_name(State),
27    ExtraOverlays = [{profile_string, ProfileString}],
28
29    CurrentProfiles = rebar_state:current_profiles(State),
30    RelxMode = case lists:member(prod, CurrentProfiles) of
31                   true ->
32                       [{mode, prod}];
33                   false ->
34                       []
35               end,
36    DefaultOutputDir = filename:join(rebar_dir:base_dir(State), ?DEFAULT_RELEASE_DIR),
37    RelxConfig1 = RelxMode ++ [output_dir(DefaultOutputDir, Opts),
38                               {overlay_vars_values, ExtraOverlays},
39                               {overlay_vars, [{base_dir, rebar_dir:base_dir(State)} | overlay_vars(Opts)]}
40                               | merge_overlays(RelxConfig)],
41
42    Args = [include_erts, system_libs, vm_args, sys_config],
43    RelxConfig2 = maybe_obey_command_args(RelxConfig1, Opts, Args),
44
45    {ok, RelxState_0} = rlx_config:to_state(RelxConfig2),
46    XrefIgnores = rebar_state:get(State, xref_ignores, []),
47    RelxState = rlx_state:filter_xref_warning(RelxState_0,
48        fun(Warnings) ->
49            rebar_prv_xref:filter_xref_results(undefined_function_calls, XrefIgnores, Warnings) end),
50
51    Providers = rebar_state:providers(State),
52    Cwd = rebar_state:dir(State),
53    rebar_hooks:run_project_and_app_hooks(Cwd, pre, Provider, Providers, State),
54
55    Releases = releases_to_build(Provider, Opts, RelxState),
56
57    case Provider of
58        relup ->
59            {Release, ToVsn} =
60                %% hd/1 can't fail because --all is not a valid option to relup
61                case Releases of
62                    [{Rel,Vsn}|_] when is_atom(Rel) ->
63                        %% This is returned if --relvsn and --relname are given
64                        {Rel, Vsn};
65                    [undefined|_] ->
66                        erlang:error(?PRV_ERROR(unknown_release));
67                    [Rel|_] when is_atom(Rel) ->
68                        erlang:error(?PRV_ERROR(unknown_vsn))
69                end,
70
71            UpFromVsn = proplists:get_value(upfrom, Opts, undefined),
72
73            relx:build_relup(Release, ToVsn, UpFromVsn, RelxState);
74        _ ->
75            parallel_run(Provider, Releases, all_apps(State), RelxState)
76    end,
77
78    rebar_hooks:run_project_and_app_hooks(Cwd, post, Provider, Providers, State),
79
80    {ok, State}.
81
82read_relx_config(State, Options) ->
83    ConfigFile = proplists:get_value(config, Options, []),
84    case ConfigFile of
85        "" ->
86            ConfigPath = filename:join([rebar_dir:root_dir(State), "relx.config"]),
87            case {rebar_state:get(State, relx, []), file:consult(ConfigPath)} of
88                {[], {ok, Config}} ->
89                    ?DEBUG("Configuring releases with relx.config", []),
90                    Config;
91                {Config, {error, enoent}} ->
92                    ?DEBUG("Configuring releases the {relx, ...} entry"
93                           " from rebar.config", []),
94                    Config;
95                {_, {error, Reason}} ->
96                    erlang:error(?PRV_ERROR({config_file, "relx.config", Reason}));
97                {RebarConfig, {ok, _RelxConfig}} ->
98                    ?WARN("Found conflicting relx configs, configuring releases"
99                          " with rebar.config", []),
100                    RebarConfig
101            end;
102        ConfigFile ->
103            case file:consult(ConfigFile) of
104                {ok, Config} ->
105                    ?DEBUG("Configuring releases with: ~ts", [ConfigFile]),
106                    Config;
107                {error, Reason} ->
108                    erlang:error(?PRV_ERROR({config_file, ConfigFile, Reason}))
109            end
110    end.
111
112-spec format_error(any()) -> iolist().
113format_error(unknown_release) ->
114    "Option --relname is missing";
115format_error(unknown_vsn) ->
116    "Option --relvsn is missing";
117format_error(all_relup) ->
118    "Option --all can not be applied to `relup` command";
119format_error({config_file, Filename, Error}) ->
120    io_lib:format("Failed to read config file ~ts: ~p", [Filename, Error]);
121format_error(Error) ->
122    io_lib:format("~p", [Error]).
123
124%%
125
126parallel_run(release, [Release], AllApps, RelxState) ->
127    relx:build_release(Release, AllApps, RelxState);
128parallel_run(tar, [Release], AllApps, RelxState) ->
129    relx:build_tar(Release, AllApps, RelxState);
130parallel_run(Provider, Releases, AllApps, RelxState) ->
131    rebar_parallel:queue(Releases, fun rel_worker/2, [Provider, AllApps, RelxState], fun rel_handler/2, []).
132
133rel_worker(Release, [Provider, Apps, RelxState]) ->
134    try
135        case Provider of
136            release ->
137                relx:build_release(Release, Apps, RelxState);
138            tar ->
139                relx:build_tar(Release, Apps, RelxState)
140        end
141    catch
142        error:Error ->
143            {Release, Error}
144    end.
145
146rel_handler({{Name, Vsn}, {error, {Module, Reason}}}, _Args) ->
147    ?ERROR("Error building release ~ts-~ts:~n~ts~ts", [Name, Vsn, rebar_utils:indent(1),
148                                                       Module:format_error(Reason)]),
149    ok;
150rel_handler({{Name, Vsn}, Other}, _Args) ->
151    ?ERROR("Error building release ~ts-~ts:~nUnknown return value: ~p", [Name, Vsn, Other]),
152    ok;
153rel_handler({ok, _}, _) ->
154    ok.
155
156releases_to_build(Provider, Opts, RelxState)->
157    case proplists:get_value(all, Opts, undefined) of
158        undefined ->
159            case proplists:get_value(relname, Opts, undefined) of
160                undefined ->
161                    [undefined];
162                R ->
163                    case proplists:get_value(relvsn, Opts, undefined) of
164                        undefined ->
165                            [list_to_atom(R)];
166                        RelVsn ->
167                            [{list_to_atom(R), RelVsn}]
168                    end
169            end;
170        true when Provider =:= relup ->
171            erlang:error(?PRV_ERROR(all_relup));
172        true ->
173            highest_unique_releases(rlx_state:configured_releases(RelxState))
174    end.
175
176%% takes a map of relx configured releases and returns a list of the highest
177%% version for each unique release name
178-spec highest_unique_releases(rlx_state:releases()) -> [{atom(), string() | undefined}].
179highest_unique_releases(Releases) ->
180    Unique = maps:fold(fun({Name, Vsn}, _, Acc) ->
181                               update_map_if_higher(Name, Vsn, Acc)
182                       end, #{}, Releases),
183    maps:to_list(Unique).
184
185update_map_if_higher(Name, Vsn, Acc) ->
186    maps:update_with(Name, fun(Vsn1) ->
187                                   case rlx_util:parsed_vsn_lte(rlx_util:parse_vsn(Vsn1),
188                                                                rlx_util:parse_vsn(Vsn)) of
189                                       true ->
190                                           Vsn;
191                                       false ->
192                                           Vsn1
193                                   end
194                           end, Vsn, Acc).
195
196%% Don't override output_dir if the user passed one on the command line
197output_dir(DefaultOutputDir, Options) ->
198    {output_dir, proplists:get_value(output_dir, Options, DefaultOutputDir)}.
199
200merge_overlays(Config) ->
201    {Overlays, Others} =
202        lists:partition(fun(C) when element(1, C) =:= overlay -> true;
203                           (_) -> false
204                        end, Config),
205    %% Have profile overlay entries come before others to match how profiles work elsewhere
206    NewOverlay = lists:flatmap(fun({overlay, Overlay}) -> Overlay end, lists:reverse(Overlays)),
207    [{overlay, NewOverlay} | Others].
208
209overlay_vars(Opts) ->
210    case proplists:get_value(overlay_vars, Opts) of
211        undefined ->
212            [];
213        [] ->
214            [];
215        FileName when is_list(FileName) ->
216            [FileName]
217    end.
218
219maybe_obey_command_args(RelxConfig, Opts, Args) ->
220    lists:foldl(
221        fun(Opt, Acc) ->
222                 case proplists:get_value(Opt, Opts) of
223                     undefined ->
224                         Acc;
225                     V ->
226                         lists:keystore(Opt, 1, Acc, {Opt, V})
227                 end
228        end, RelxConfig, Args).
229
230%%
231
232%% Returns a map of all apps that are part of the rebar3 project.
233%% This means the project apps and dependencies but not OTP libraries.
234-spec all_apps(rebar_state:t()) -> #{atom() => rlx_app_info:t()}.
235all_apps(State) ->
236    maps:merge(app_infos_to_relx(rebar_state:project_apps(State), project),
237               app_infos_to_relx(rebar_state:all_deps(State), dep)).
238
239%%
240
241-spec app_infos_to_relx([rlx_app_info:t()], rlx_app_info:app_type()) -> #{atom() => rlx_app_info:t()}.
242app_infos_to_relx(AppInfos, AppType) ->
243    lists:foldl(fun(AppInfo, Acc) ->
244                        Acc#{binary_to_atom(rebar_app_info:name(AppInfo), utf8)
245                             => app_info_to_relx(rebar_app_info:app_to_map(AppInfo), AppType)}
246                end, #{}, AppInfos).
247
248app_info_to_relx(#{name := Name,
249                   vsn := Vsn,
250                   applications := Applications,
251                   included_applications := IncludedApplications,
252                   dir := Dir,
253                   link := false}, AppType) ->
254    rlx_app_info:new(Name, Vsn, Dir, Applications, IncludedApplications, AppType).
255
256-spec opt_spec_list() -> [getopt:option_spec()].
257opt_spec_list() ->
258    [{all, undefined, "all",  boolean,
259      "If true runs the command against all configured  releases"},
260    {relname,  $n, "relname",  string,
261      "Specify the name for the release that will be generated"},
262     {relvsn, $v, "relvsn", string, "Specify the version for the release"},
263     {upfrom, $u, "upfrom", string,
264      "Only valid with relup target, specify the release to upgrade from"},
265     {output_dir, $o, "output-dir", string,
266      "The output directory for the release. This is `./` by default."},
267     {help, $h, "help", undefined,
268      "Print usage"},
269     {lib_dir, $l, "lib-dir", string,
270      "Additional dir that should be searched for OTP Apps"},
271     {dev_mode, $d, "dev-mode", boolean,
272      "Symlink the applications and configuration into the release instead of copying"},
273     {include_erts, $i, "include-erts", string,
274      "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"},
275     {override, $a, "override", string,
276      "Provide an app name and a directory to override in the form <appname>:<app directory>"},
277     {config, $c, "config", {string, ""}, "The path to a config file"},
278     {overlay_vars, undefined, "overlay_vars", string, "Path to a file of overlay variables"},
279     {vm_args, undefined, "vm_args", string, "Path to a file to use for vm.args"},
280     {sys_config, undefined, "sys_config", string, "Path to a file to use for sys.config"},
281     {system_libs, undefined, "system_libs", string, "Boolean or path to dir of Erlang system libs"},
282     {version, undefined, "version", undefined, "Print relx version"},
283     {root_dir, $r, "root", string, "The project root directory"}].
284