-module(rlx_config). -export([to_state/1, load/2, format_error/1]). -include("relx.hrl"). -include("rlx_log.hrl"). %% TODO: list out each supported config -type t() :: [{atom(), term()}]. -export_type([t/0]). to_state(Config) -> to_state(Config, rlx_state:new()). to_state(Config, State) -> %% setup warnings_as_errors before loading the rest so we can error on %% any warning during the load State1 = rlx_state:warnings_as_errors(State, proplists:get_bool(warnings_as_errors, Config)), lists:foldl(fun load/2, {ok, State1}, Config). -spec load(term(), {ok, rlx_state:t()} | relx:error()) -> {ok, rlx_state:t()} | relx:error(). load({mode, Mode}, {ok, State}) -> State1 = rlx_state:mode(State, Mode), Expanded = expand_mode(Mode), {ok, State2} = lists:foldl(fun load/2, {ok, State1}, Expanded), {ok, State2}; load({lib_dirs, Dirs}, {ok, State}) -> LibDirs = rlx_file_utils:wildcard_paths(Dirs), State1 = rlx_state:add_lib_dirs(State, LibDirs), {ok, State1}; load({exclude_apps, ExcludeApps0}, {ok, State0}) -> {ok, rlx_state:exclude_apps(State0, ExcludeApps0)}; load({exclude_modules, ExcludeModules0}, {ok, State0}) -> {ok, rlx_state:exclude_modules(State0, ExcludeModules0)}; load({debug_info, DebugInfo}, {ok, State0}) -> {ok, rlx_state:debug_info(State0, DebugInfo)}; load({overrides, Overrides0}, {ok, State0}) -> {ok, rlx_state:overrides(State0, Overrides0)}; load({dev_mode, false}, {ok, State0}) -> {ok, rlx_state:dev_mode(State0, false)}; load({dev_mode, true}, {ok, State0}) -> {ok, State1} = load({mode, dev}, {ok, State0}), {ok, rlx_state:dev_mode(State1, true)}; load({upfrom, UpFrom}, {ok, State0}) -> {ok, rlx_state:upfrom(State0, UpFrom)}; load({include_src, IncludeSrc}, {ok, State0}) -> {ok, rlx_state:include_src(State0, IncludeSrc)}; load({release, {RelName, Vsn, {extend, {RelName2, Vsn2}}}, Applications}, {ok, State0}) -> NewVsn = parse_vsn(Vsn), NewVsn2 = parse_vsn(Vsn2), add_extended_release(RelName, NewVsn, RelName2, NewVsn2, Applications, State0); load({release, {RelName, Vsn, {extend, RelName2}}, Applications}, {ok, State0}) -> NewVsn = parse_vsn(Vsn), add_extended_release(RelName, NewVsn, RelName2, NewVsn, Applications, State0); load({release, {RelName, Vsn, {extend, {RelName2, Vsn2}}}, Applications, Config}, {ok, State0}) -> NewVsn = parse_vsn(Vsn), NewVsn2 = parse_vsn(Vsn2), add_extended_release(RelName, NewVsn, RelName2, NewVsn2, Applications, Config, State0); load({release, {RelName, Vsn, {extend, RelName2}}, Applications, Config}, {ok, State0}) -> NewVsn = parse_vsn(Vsn), add_extended_release(RelName, NewVsn, RelName2, NewVsn, Applications, Config, State0); load({release, {RelName, Vsn}, {erts, ErtsVsn}, Applications}, {ok, State0}) -> NewVsn = parse_vsn(Vsn), Release0 = rlx_release:erts(rlx_release:new(RelName, NewVsn), ErtsVsn), Release1 = rlx_release:goals(Release0, Applications), {ok, rlx_state:add_configured_release(State0, Release1)}; load({release, {RelName, Vsn}, {erts, ErtsVsn}, Applications, Config}, {ok, State0}) -> NewVsn = parse_vsn(Vsn), Release0 = rlx_release:erts(rlx_release:new(RelName, NewVsn), ErtsVsn), Release1 = rlx_release:goals(Release0, Applications), Release2 = rlx_release:config(Release1, Config), {ok, rlx_state:add_configured_release(State0, Release2)}; load({release, {RelName, Vsn}, Applications}, {ok, State0}) -> NewVsn = parse_vsn(Vsn), Release0 = rlx_release:new(RelName, NewVsn), Release1 = rlx_release:goals(Release0, Applications), {ok, rlx_state:add_configured_release(State0, Release1)}; load({release, {RelName, Vsn}, Applications, Config}, {ok, State0}) -> NewVsn = parse_vsn(Vsn), Release0 = rlx_release:new(RelName, NewVsn), Release1 = rlx_release:goals(Release0, Applications), Release2 = rlx_release:config(Release1, Config), {ok, rlx_state:add_configured_release(State0, Release2)}; load({vm_args, false}, {ok, State}) -> {ok, rlx_state:vm_args(State, false)}; load({vm_args, undefined}, {ok, State}) -> {ok, rlx_state:vm_args(State, undefined)}; load({vm_args, VmArgs}, {ok, State}) -> {ok, rlx_state:vm_args(State, filename:absname(VmArgs))}; load({vm_args_src, false}, {ok, State}) -> {ok, rlx_state:vm_args_src(State, false)}; load({vm_args_src, undefined}, {ok, State}) -> {ok, rlx_state:vm_args_src(State, undefined)}; load({vm_args_src, VmArgs}, {ok, State}) -> {ok, rlx_state:vm_args_src(State, filename:absname(VmArgs))}; load({sys_config, false}, {ok, State}) -> {ok, rlx_state:sys_config(State, false)}; load({sys_config, undefined}, {ok, State}) -> {ok, rlx_state:sys_config(State, undefined)}; load({sys_config, SysConfig}, {ok, State}) -> {ok, rlx_state:sys_config(State, filename:absname(SysConfig))}; load({sys_config_src, false}, {ok, State}) -> {ok, rlx_state:sys_config_src(State, false)}; load({sys_config_src, undefined}, {ok, State}) -> {ok, rlx_state:sys_config_src(State, undefined)}; load({sys_config_src, SysConfigSrc}, {ok, State}) -> {ok, rlx_state:sys_config_src(State, filename:absname(SysConfigSrc))}; load({root_dir, Root}, {ok, State}) -> {ok, rlx_state:root_dir(State, filename:absname(Root))}; load({output_dir, OutputDir}, {ok, State}) -> {ok, rlx_state:base_output_dir(State, filename:absname(OutputDir))}; load({overlay_vars_values, OverlayVarsValues}, {ok, State}) -> CurrentOverlayVarsValues = rlx_state:overlay_vars_values(State), NewOverlayVarsValues = CurrentOverlayVarsValues ++ OverlayVarsValues, {ok, rlx_state:overlay_vars_values(State, NewOverlayVarsValues)}; load({overlay_vars, OverlayVars}, {ok, State}) -> CurrentOverlayVars = rlx_state:overlay_vars(State), NewOverlayVars0 = list_of_overlay_vars_files(OverlayVars), NewOverlayVars1 = CurrentOverlayVars ++ NewOverlayVars0, {ok, rlx_state:overlay_vars(State, NewOverlayVars1)}; load({warnings_as_errors, WarningsAsErrors}, {ok, State}) -> {ok, rlx_state:warnings_as_errors(State, WarningsAsErrors)}; load({src_tests, SrcTests}, {ok, State}) -> {ok, rlx_state:src_tests(State, SrcTests)}; load({exref, ExRef}, {ok, State}) -> {ok, rlx_state:exref(State, ExRef)}; load({check_for_undefined_functions, CheckForUndefinedFunctions}, {ok, State}) -> {ok, rlx_state:check_for_undefined_functions(State, CheckForUndefinedFunctions)}; load({include_erts, IncludeErts}, {ok, State}) -> {ok, rlx_state:include_erts(State, IncludeErts)}; load({system_libs, SystemLibs}, {ok, State}) when is_boolean(SystemLibs) -> {ok, rlx_state:system_libs(State, SystemLibs)}; load({system_libs, SystemLibs}, {ok, State}) when is_list(SystemLibs) -> case filelib:is_dir(SystemLibs) of true -> {ok, rlx_state:system_libs(State, SystemLibs)}; false -> erlang:error(?RLX_ERROR({bad_system_libs, SystemLibs})) end; load({overlay, Overlay}, {ok, State}) -> {ok, rlx_state:overlay(State, Overlay)}; load({extended_start_script_hooks, ExtendedStartScriptHooks}, {ok, State}) -> {ok, rlx_state:extended_start_script_hooks(State, ExtendedStartScriptHooks)}; load({extended_start_script, ExtendedStartScript}, {ok, State}) -> {ok, rlx_state:extended_start_script(State, ExtendedStartScript)}; load({extended_start_script_extensions, ExtendedStartScriptExtensions}, {ok, State}) -> {ok, rlx_state:extended_start_script_extensions(State, ExtendedStartScriptExtensions)}; load({include_start_scripts_for, IncludeStartScriptsFor}, {ok, State}) when is_list(IncludeStartScriptsFor) -> {ok, rlx_state:include_start_scripts_for(State, IncludeStartScriptsFor)}; load({generate_start_script, GenerateStartScript}, {ok, State}) -> {ok, rlx_state:generate_start_script(State, GenerateStartScript)}; load({include_nodetool, IncludeNodetool}, {ok, State}) -> {ok, rlx_state:include_nodetool(State, IncludeNodetool)}; load(_, Error={error, _}) -> erlang:error(?RLX_ERROR(Error)); load(InvalidTerm, {ok, State}) -> Warning = {invalid_term, InvalidTerm}, case rlx_state:warnings_as_errors(State) of true -> erlang:error(?RLX_ERROR(Warning)); false -> ?log_warn(format_error(Warning)), {ok, State} end. format_error({bad_system_libs, SetSystemLibs}) -> io_lib:format("Config value for system_libs must be a boolean or directory but found: ~p", [SetSystemLibs]); format_error({invalid_term, InvalidTerm}) -> io_lib:format("Unknown term found in relx configuration: ~p", [InvalidTerm]). %% -spec expand_mode(rlx_state:mode()) -> [term()]. expand_mode(dev) -> [{include_src, true}, {debug_info, keep}, {include_erts, false}]; expand_mode(prod) -> [{include_src, false}, {debug_info, strip}, {include_erts, true}, {dev_mode, false}]; expand_mode(minimal) -> %% minimal is prod without the erts [{include_src, false}, {debug_info, strip}, {include_erts, false}, {dev_mode, false}]. parse_vsn(Vsn) when Vsn =:= git ; Vsn =:= "git" -> try_vsn(fun git_tag_vsn/0); parse_vsn({git, short}) -> try_vsn(fun() -> git_ref("--short") end); parse_vsn({git, long}) -> try_vsn(fun() -> git_ref("") end); parse_vsn({file, File}) -> try_vsn(fun() -> {ok, Vsn} = file:read_file(File), rlx_util:to_string(rlx_string:trim(rlx_util:to_string(Vsn), both, "\n")) end); parse_vsn(Vsn) when Vsn =:= semver ; Vsn =:= "semver" -> try_vsn(fun git_tag_vsn/0); parse_vsn({semver, _}) -> try_vsn(fun git_tag_vsn/0); parse_vsn({cmd, Command}) -> try_vsn(fun() -> rlx_util:sh(Command) end); parse_vsn(Vsn) -> Vsn. try_vsn(Fun) -> try Fun() catch error:_ -> "0.0.0" end. git_ref(Arg) -> String = rlx_util:sh("git rev-parse " ++ Arg ++ " HEAD"), Vsn = rlx_string:trim(String, both, "\n"), case length(Vsn) =:= 40 orelse length(Vsn) =:= 7 of true -> Vsn; false -> %% if the result isn't exactly either 40 or 7 characters then %% it must have failed {ok, Dir} = file:get_cwd(), ?log_warn("Getting ref of git repo failed in directory ~ts. Falling back to version 0", [Dir]), "0.0.0" end. git_tag_vsn() -> {Vsn, RawRef, RawCount} = collect_default_refcount(), build_vsn_string(Vsn, RawRef, RawCount). collect_default_refcount() -> %% Get the tag timestamp and minimal ref from the system. The %% timestamp is really important from an ordering perspective. RawRef = rlx_util:sh("git log -n 1 --pretty=format:'%h\n' "), {Tag, Vsn} = parse_tags(), RawCount = get_patch_count(Tag), {Vsn, RawRef, RawCount}. get_patch_count(RawRef) -> Ref = re:replace(RawRef, "\\s", "", [global]), Cmd = io_lib:format("git rev-list --count ~s..HEAD", [Ref]), rlx_util:sh(Cmd). build_vsn_string(Vsn, RawRef, RawCount) -> %% Cleanup the tag and the Ref information. Basically leading 'v's and %% whitespace needs to go away. RefTag = [".ref", re:replace(RawRef, "\\s", "", [global])], Count = re:replace(RawCount, "\\D", "", [global]), %% Create the valid [semver](http://semver.org) version from the tag case iolist_to_binary(Count) of <<"0">> -> lists:flatten(Vsn); CountBin -> binary_to_list(iolist_to_binary([Vsn, "+build.", CountBin, RefTag])) end. parse_tags() -> Tag = rlx_util:sh("git describe --abbrev=0 --tags"), Vsn = rlx_string:trim(rlx_string:trim(Tag, leading, "v"), trailing, "\n"), {Tag, Vsn}. add_extended_release(RelName, NewVsn, RelName2, NewVsn2, Applications, State0) -> Release0 = rlx_release:new(RelName, NewVsn), ExtendRelease = rlx_state:get_configured_release(State0, RelName2, NewVsn2), Applications1 = rlx_release:goals(ExtendRelease), Release1 = rlx_release:parsed_goals(Release0, merge_application_goals(Applications, Applications1)), {ok, rlx_state:add_configured_release(State0, Release1)}. add_extended_release(RelName, NewVsn, RelName2, NewVsn2, Applications, Config, State0) -> Release0 = rlx_release:new(RelName, NewVsn), ExtendRelease = rlx_state:get_configured_release(State0, RelName2, NewVsn2), Applications1 = rlx_release:goals(ExtendRelease), Release1 = rlx_release:parsed_goals(Release0, merge_application_goals(Applications, Applications1)), Release2 = rlx_release:config(Release1, Config), {ok, rlx_state:add_configured_release(State0, Release2)}. list_of_overlay_vars_files(undefined) -> []; list_of_overlay_vars_files([]) -> []; list_of_overlay_vars_files([H | _]=Vars) when erlang:is_list(H) ; is_tuple(H) -> Vars; list_of_overlay_vars_files(FileName) when is_list(FileName) -> [FileName]. merge_application_goals(Goals, BaseGoals) -> lists:foldl(fun({Key, Goal}, Acc) -> lists:keystore(Key, 1, Acc, {Key, Goal}) end, BaseGoals, rlx_release:parse_goals(Goals)).