1-module(rlx_config).
2
3-export([to_state/1,
4         load/2,
5         format_error/1]).
6
7-include("relx.hrl").
8-include("rlx_log.hrl").
9
10%% TODO: list out each supported config
11-type t() :: [{atom(), term()}].
12
13-export_type([t/0]).
14
15to_state(Config) ->
16    to_state(Config, rlx_state:new()).
17
18to_state(Config, State) ->
19    %% setup warnings_as_errors before loading the rest so we can error on
20    %% any warning during the load
21    State1 = rlx_state:warnings_as_errors(State, proplists:get_bool(warnings_as_errors, Config)),
22    lists:foldl(fun load/2, {ok, State1}, Config).
23
24-spec load(term(), {ok, rlx_state:t()} | relx:error()) -> {ok, rlx_state:t()} | relx:error().
25load({mode, Mode}, {ok, State}) ->
26    State1 = rlx_state:mode(State, Mode),
27    Expanded = expand_mode(Mode),
28    {ok, State2} = lists:foldl(fun load/2, {ok, State1}, Expanded),
29    {ok, State2};
30load({lib_dirs, Dirs}, {ok, State}) ->
31    LibDirs = rlx_file_utils:wildcard_paths(Dirs),
32    State1 = rlx_state:add_lib_dirs(State, LibDirs),
33    {ok, State1};
34load({exclude_apps, ExcludeApps0}, {ok, State0}) ->
35    {ok, rlx_state:exclude_apps(State0, ExcludeApps0)};
36load({exclude_modules, ExcludeModules0}, {ok, State0}) ->
37    {ok, rlx_state:exclude_modules(State0, ExcludeModules0)};
38load({debug_info, DebugInfo}, {ok, State0}) ->
39    {ok, rlx_state:debug_info(State0, DebugInfo)};
40load({overrides, Overrides0}, {ok, State0}) ->
41    {ok, rlx_state:overrides(State0, Overrides0)};
42load({dev_mode, false}, {ok, State0}) ->
43    {ok, rlx_state:dev_mode(State0, false)};
44load({dev_mode, true}, {ok, State0}) ->
45    {ok, State1} = load({mode, dev}, {ok, State0}),
46    {ok, rlx_state:dev_mode(State1, true)};
47load({upfrom, UpFrom}, {ok, State0}) ->
48    {ok, rlx_state:upfrom(State0, UpFrom)};
49load({include_src, IncludeSrc}, {ok, State0}) ->
50    {ok, rlx_state:include_src(State0, IncludeSrc)};
51load({release, {RelName, Vsn, {extend, {RelName2, Vsn2}}}, Applications}, {ok, State0}) ->
52    NewVsn = parse_vsn(Vsn),
53    NewVsn2 = parse_vsn(Vsn2),
54    add_extended_release(RelName, NewVsn, RelName2, NewVsn2, Applications, State0);
55load({release, {RelName, Vsn, {extend, RelName2}}, Applications}, {ok, State0}) ->
56    NewVsn = parse_vsn(Vsn),
57    add_extended_release(RelName, NewVsn, RelName2, NewVsn, Applications, State0);
58load({release, {RelName, Vsn, {extend, {RelName2, Vsn2}}}, Applications, Config}, {ok, State0}) ->
59    NewVsn = parse_vsn(Vsn),
60    NewVsn2 = parse_vsn(Vsn2),
61    add_extended_release(RelName, NewVsn, RelName2, NewVsn2, Applications, Config, State0);
62load({release, {RelName, Vsn, {extend, RelName2}}, Applications, Config}, {ok, State0}) ->
63    NewVsn = parse_vsn(Vsn),
64    add_extended_release(RelName, NewVsn, RelName2, NewVsn, Applications, Config, State0);
65load({release, {RelName, Vsn}, {erts, ErtsVsn}, Applications}, {ok, State0}) ->
66    NewVsn = parse_vsn(Vsn),
67    Release0 = rlx_release:erts(rlx_release:new(RelName, NewVsn), ErtsVsn),
68    Release1 = rlx_release:goals(Release0, Applications),
69    {ok, rlx_state:add_configured_release(State0, Release1)};
70load({release, {RelName, Vsn}, {erts, ErtsVsn}, Applications, Config}, {ok, State0}) ->
71    NewVsn = parse_vsn(Vsn),
72    Release0 = rlx_release:erts(rlx_release:new(RelName, NewVsn), ErtsVsn),
73    Release1 = rlx_release:goals(Release0, Applications),
74    Release2 = rlx_release:config(Release1, Config),
75    {ok, rlx_state:add_configured_release(State0, Release2)};
76load({release, {RelName, Vsn}, Applications}, {ok, State0}) ->
77    NewVsn = parse_vsn(Vsn),
78    Release0 = rlx_release:new(RelName, NewVsn),
79    Release1 = rlx_release:goals(Release0, Applications),
80    {ok, rlx_state:add_configured_release(State0, Release1)};
81load({release, {RelName, Vsn}, Applications, Config}, {ok, State0}) ->
82    NewVsn = parse_vsn(Vsn),
83    Release0 = rlx_release:new(RelName, NewVsn),
84    Release1 = rlx_release:goals(Release0, Applications),
85    Release2 = rlx_release:config(Release1, Config),
86    {ok, rlx_state:add_configured_release(State0, Release2)};
87load({vm_args, false}, {ok, State}) ->
88    {ok, rlx_state:vm_args(State, false)};
89load({vm_args, undefined}, {ok, State}) ->
90    {ok, rlx_state:vm_args(State, undefined)};
91load({vm_args, VmArgs}, {ok, State}) ->
92    {ok, rlx_state:vm_args(State, filename:absname(VmArgs))};
93load({vm_args_src, false}, {ok, State}) ->
94    {ok, rlx_state:vm_args_src(State, false)};
95load({vm_args_src, undefined}, {ok, State}) ->
96    {ok, rlx_state:vm_args_src(State, undefined)};
97load({vm_args_src, VmArgs}, {ok, State}) ->
98    {ok, rlx_state:vm_args_src(State, filename:absname(VmArgs))};
99load({sys_config, false}, {ok, State}) ->
100    {ok, rlx_state:sys_config(State, false)};
101load({sys_config, undefined}, {ok, State}) ->
102    {ok, rlx_state:sys_config(State, undefined)};
103load({sys_config, SysConfig}, {ok, State}) ->
104    {ok, rlx_state:sys_config(State, filename:absname(SysConfig))};
105load({sys_config_src, false}, {ok, State}) ->
106    {ok, rlx_state:sys_config_src(State, false)};
107load({sys_config_src, undefined}, {ok, State}) ->
108    {ok, rlx_state:sys_config_src(State, undefined)};
109load({sys_config_src, SysConfigSrc}, {ok, State}) ->
110    {ok, rlx_state:sys_config_src(State, filename:absname(SysConfigSrc))};
111load({root_dir, Root}, {ok, State}) ->
112    {ok, rlx_state:root_dir(State, filename:absname(Root))};
113load({output_dir, OutputDir}, {ok, State}) ->
114    {ok, rlx_state:base_output_dir(State, filename:absname(OutputDir))};
115load({overlay_vars_values, OverlayVarsValues}, {ok, State}) ->
116    CurrentOverlayVarsValues = rlx_state:overlay_vars_values(State),
117    NewOverlayVarsValues = CurrentOverlayVarsValues ++ OverlayVarsValues,
118    {ok, rlx_state:overlay_vars_values(State, NewOverlayVarsValues)};
119load({overlay_vars, OverlayVars}, {ok, State}) ->
120    CurrentOverlayVars = rlx_state:overlay_vars(State),
121    NewOverlayVars0 = list_of_overlay_vars_files(OverlayVars),
122    NewOverlayVars1 = CurrentOverlayVars ++ NewOverlayVars0,
123    {ok, rlx_state:overlay_vars(State, NewOverlayVars1)};
124load({warnings_as_errors, WarningsAsErrors}, {ok, State}) ->
125    {ok, rlx_state:warnings_as_errors(State, WarningsAsErrors)};
126load({src_tests, SrcTests}, {ok, State}) ->
127    {ok, rlx_state:src_tests(State, SrcTests)};
128load({exref, ExRef}, {ok, State}) ->
129    {ok, rlx_state:exref(State, ExRef)};
130load({check_for_undefined_functions, CheckForUndefinedFunctions}, {ok, State}) ->
131    {ok, rlx_state:check_for_undefined_functions(State, CheckForUndefinedFunctions)};
132load({include_erts, IncludeErts}, {ok, State}) ->
133    {ok, rlx_state:include_erts(State, IncludeErts)};
134load({system_libs, SystemLibs}, {ok, State}) when is_boolean(SystemLibs) ->
135    {ok, rlx_state:system_libs(State, SystemLibs)};
136load({system_libs, SystemLibs}, {ok, State}) when is_list(SystemLibs) ->
137    case filelib:is_dir(SystemLibs) of
138        true ->
139            {ok, rlx_state:system_libs(State, SystemLibs)};
140        false ->
141            erlang:error(?RLX_ERROR({bad_system_libs, SystemLibs}))
142    end;
143load({overlay, Overlay}, {ok, State}) ->
144    {ok, rlx_state:overlay(State, Overlay)};
145load({extended_start_script_hooks, ExtendedStartScriptHooks}, {ok, State}) ->
146    {ok, rlx_state:extended_start_script_hooks(State, ExtendedStartScriptHooks)};
147load({extended_start_script, ExtendedStartScript}, {ok, State}) ->
148    {ok, rlx_state:extended_start_script(State, ExtendedStartScript)};
149load({extended_start_script_extensions, ExtendedStartScriptExtensions}, {ok, State}) ->
150    {ok, rlx_state:extended_start_script_extensions(State, ExtendedStartScriptExtensions)};
151load({include_start_scripts_for, IncludeStartScriptsFor}, {ok, State}) when is_list(IncludeStartScriptsFor) ->
152    {ok, rlx_state:include_start_scripts_for(State, IncludeStartScriptsFor)};
153load({generate_start_script, GenerateStartScript}, {ok, State}) ->
154    {ok, rlx_state:generate_start_script(State, GenerateStartScript)};
155load({include_nodetool, IncludeNodetool}, {ok, State}) ->
156    {ok, rlx_state:include_nodetool(State, IncludeNodetool)};
157load(_, Error={error, _}) ->
158    erlang:error(?RLX_ERROR(Error));
159load(InvalidTerm, {ok, State}) ->
160    Warning = {invalid_term, InvalidTerm},
161    case rlx_state:warnings_as_errors(State) of
162        true ->
163            erlang:error(?RLX_ERROR(Warning));
164        false ->
165            ?log_warn(format_error(Warning)),
166            {ok, State}
167    end.
168
169format_error({bad_system_libs, SetSystemLibs}) ->
170    io_lib:format("Config value for system_libs must be a boolean or directory but found: ~p",
171                  [SetSystemLibs]);
172format_error({invalid_term, InvalidTerm}) ->
173    io_lib:format("Unknown term found in relx configuration: ~p", [InvalidTerm]).
174
175%%
176
177-spec expand_mode(rlx_state:mode()) -> [term()].
178expand_mode(dev) ->
179    [{include_src, true}, {debug_info, keep}, {include_erts, false}];
180expand_mode(prod) ->
181    [{include_src, false}, {debug_info, strip}, {include_erts, true}, {dev_mode, false}];
182expand_mode(minimal) ->
183    %% minimal is prod without the erts
184    [{include_src, false}, {debug_info, strip}, {include_erts, false}, {dev_mode, false}].
185
186parse_vsn(Vsn) when Vsn =:= git ; Vsn =:= "git" ->
187    try_vsn(fun git_tag_vsn/0);
188parse_vsn({git, short}) ->
189    try_vsn(fun() -> git_ref("--short") end);
190parse_vsn({git, long}) ->
191    try_vsn(fun() -> git_ref("") end);
192parse_vsn({file, File}) ->
193    try_vsn(fun() ->
194                    {ok, Vsn} = file:read_file(File),
195                    rlx_util:to_string(rlx_string:trim(rlx_util:to_string(Vsn), both, "\n"))
196            end);
197parse_vsn(Vsn) when Vsn =:= semver ; Vsn =:= "semver" ->
198    try_vsn(fun git_tag_vsn/0);
199parse_vsn({semver, _}) ->
200    try_vsn(fun git_tag_vsn/0);
201parse_vsn({cmd, Command}) ->
202    try_vsn(fun() -> rlx_util:sh(Command) end);
203parse_vsn(Vsn) ->
204    Vsn.
205
206try_vsn(Fun) ->
207    try
208        Fun()
209    catch
210        error:_ ->
211            "0.0.0"
212    end.
213
214git_ref(Arg) ->
215    String = rlx_util:sh("git rev-parse " ++ Arg ++ " HEAD"),
216    Vsn = rlx_string:trim(String, both, "\n"),
217    case length(Vsn) =:= 40 orelse length(Vsn) =:= 7 of
218        true ->
219            Vsn;
220        false ->
221            %% if the result isn't exactly either 40 or 7 characters then
222            %% it must have failed
223            {ok, Dir} = file:get_cwd(),
224            ?log_warn("Getting ref of git repo failed in directory ~ts. Falling back to version 0",
225                      [Dir]),
226            "0.0.0"
227    end.
228
229git_tag_vsn() ->
230    {Vsn, RawRef, RawCount} = collect_default_refcount(),
231    build_vsn_string(Vsn, RawRef, RawCount).
232
233collect_default_refcount() ->
234    %% Get the tag timestamp and minimal ref from the system. The
235    %% timestamp is really important from an ordering perspective.
236    RawRef = rlx_util:sh("git log -n 1 --pretty=format:'%h\n' "),
237
238    {Tag, Vsn} = parse_tags(),
239    RawCount = get_patch_count(Tag),
240    {Vsn, RawRef, RawCount}.
241
242get_patch_count(RawRef) ->
243    Ref = re:replace(RawRef, "\\s", "", [global]),
244    Cmd = io_lib:format("git rev-list --count ~s..HEAD", [Ref]),
245    rlx_util:sh(Cmd).
246
247build_vsn_string(Vsn, RawRef, RawCount) ->
248    %% Cleanup the tag and the Ref information. Basically leading 'v's and
249    %% whitespace needs to go away.
250    RefTag = [".ref", re:replace(RawRef, "\\s", "", [global])],
251    Count = re:replace(RawCount, "\\D", "", [global]),
252
253    %% Create the valid [semver](http://semver.org) version from the tag
254    case iolist_to_binary(Count) of
255        <<"0">> ->
256            lists:flatten(Vsn);
257        CountBin ->
258            binary_to_list(iolist_to_binary([Vsn, "+build.", CountBin, RefTag]))
259    end.
260
261parse_tags() ->
262    Tag = rlx_util:sh("git describe --abbrev=0 --tags"),
263    Vsn = rlx_string:trim(rlx_string:trim(Tag, leading, "v"), trailing, "\n"),
264    {Tag, Vsn}.
265
266
267add_extended_release(RelName, NewVsn, RelName2, NewVsn2, Applications, State0) ->
268    Release0 = rlx_release:new(RelName, NewVsn),
269    ExtendRelease = rlx_state:get_configured_release(State0, RelName2, NewVsn2),
270    Applications1 = rlx_release:goals(ExtendRelease),
271    Release1 = rlx_release:parsed_goals(Release0, merge_application_goals(Applications, Applications1)),
272    {ok, rlx_state:add_configured_release(State0, Release1)}.
273
274add_extended_release(RelName, NewVsn, RelName2, NewVsn2, Applications, Config, State0) ->
275    Release0 = rlx_release:new(RelName, NewVsn),
276    ExtendRelease = rlx_state:get_configured_release(State0, RelName2, NewVsn2),
277    Applications1 = rlx_release:goals(ExtendRelease),
278    Release1 = rlx_release:parsed_goals(Release0, merge_application_goals(Applications, Applications1)),
279    Release2 = rlx_release:config(Release1, Config),
280    {ok, rlx_state:add_configured_release(State0, Release2)}.
281
282list_of_overlay_vars_files(undefined) ->
283    [];
284list_of_overlay_vars_files([]) ->
285    [];
286list_of_overlay_vars_files([H | _]=Vars) when erlang:is_list(H) ;
287                                              is_tuple(H) ->
288    Vars;
289list_of_overlay_vars_files(FileName) when is_list(FileName) ->
290    [FileName].
291
292merge_application_goals(Goals, BaseGoals) ->
293    lists:foldl(fun({Key, Goal}, Acc) ->
294                        lists:keystore(Key, 1, Acc, {Key, Goal})
295                end, BaseGoals, rlx_release:parse_goals(Goals)).
296