236 lines
10 KiB
Erlang
236 lines
10 KiB
Erlang
-module(rlx_resolve).
|
|
|
|
-export([solve_release/2,
|
|
format_error/1]).
|
|
|
|
-include("relx.hrl").
|
|
-include("rlx_log.hrl").
|
|
|
|
-spec format_error(ErrorDetail::term()) -> iolist().
|
|
format_error({no_goals_specified, {RelName, RelVsn}}) ->
|
|
io_lib:format("No applications configured to be included in release ~s-~s", [RelName, RelVsn]);
|
|
format_error({release_erts_error, Dir}) ->
|
|
io_lib:format("Unable to find erts in ~s", [Dir]);
|
|
format_error({app_not_found, AppName, undefined}) ->
|
|
io_lib:format("Application needed for release not found: ~p", [AppName]);
|
|
format_error({app_not_found, AppName, AppVsn}) ->
|
|
io_lib:format("Application needed for release not found: ~p-~s", [AppName, AppVsn]).
|
|
|
|
solve_release(Release, State0) ->
|
|
RelName = rlx_release:name(Release),
|
|
RelVsn = rlx_release:vsn(Release),
|
|
?log_debug("Solving Release ~p-~s", [RelName, RelVsn]),
|
|
AllApps = rlx_state:available_apps(State0),
|
|
|
|
%% get per release config values and override the State with them
|
|
Config = rlx_release:config(Release),
|
|
{ok, State1} = lists:foldl(fun rlx_config:load/2, {ok, State0}, Config),
|
|
case rlx_release:goals(Release) of
|
|
[] ->
|
|
erlang:error(?RLX_ERROR({no_goals_specified, {RelName, RelVsn}}));
|
|
Goals ->
|
|
LibDirs = rlx_state:lib_dirs(State1),
|
|
|
|
|
|
{CheckCodeLibDirs, LibDirs1} =
|
|
case rlx_state:system_libs(State0) of
|
|
B when is_boolean(B) ->
|
|
%% even if we don't include system libs (`system_libs' being false)
|
|
%% here we still want to check for the system apps in the code path.
|
|
%% so return `true' for `CheckCodeLibDirs'
|
|
{true, LibDirs};
|
|
SystemLibs ->
|
|
?log_debug("System libs dir to search for apps ~ts", [SystemLibs]),
|
|
{false, [SystemLibs | LibDirs]}
|
|
end,
|
|
|
|
ExcludeApps = rlx_state:exclude_apps(State1),
|
|
Pkgs = subset(Goals, AllApps, LibDirs1, CheckCodeLibDirs, ExcludeApps),
|
|
set_resolved(Release, Pkgs, State1)
|
|
end.
|
|
|
|
%% find the app_info records for each application and its deps needed for the release
|
|
subset(Goals, World, LibDirs, CheckCodeLibDirs, ExcludeApps) ->
|
|
{Apps, _} = fold_apps(Goals, World, sets:new(), LibDirs, CheckCodeLibDirs, [], ExcludeApps),
|
|
Apps.
|
|
|
|
subset(Goal, World, Seen, LibDirs, CheckCodeLibDirs, OptionalApplications, ExcludeApps) ->
|
|
{Name, Vsn} = name_version(Goal),
|
|
case sets:is_element(Name, Seen) of
|
|
true ->
|
|
{[], Seen};
|
|
_ ->
|
|
case find_app(Name, Vsn, World, LibDirs, CheckCodeLibDirs) of
|
|
not_found ->
|
|
case lists:member(Name, OptionalApplications) of
|
|
true ->
|
|
%% don't add to Seen since optional applications are only
|
|
%% per-application and not global, so another app could
|
|
%% depend on this dependency
|
|
{[], Seen};
|
|
false ->
|
|
erlang:error(?RLX_ERROR({app_not_found, Name, Vsn}))
|
|
end;
|
|
AppInfo=#{applications := Applications,
|
|
included_applications := IncludedApplications,
|
|
optional_applications := OptionalApplications0} ->
|
|
{Apps, Seen2} = fold_apps(Applications ++ IncludedApplications ++ OptionalApplications0,
|
|
World,
|
|
sets:add_element(Name, Seen),
|
|
LibDirs,
|
|
CheckCodeLibDirs,
|
|
OptionalApplications0,
|
|
ExcludeApps),
|
|
|
|
%% don't add excluded apps
|
|
%% TODO: should an excluded app's deps also be excluded?
|
|
case lists:member(Name, ExcludeApps) of
|
|
true ->
|
|
{Apps, Seen2};
|
|
false ->
|
|
%% place the deps of the App before it
|
|
{Apps ++ [AppInfo], Seen2}
|
|
end
|
|
end
|
|
end.
|
|
|
|
fold_apps(Apps, World, Seen, LibDirs, CheckCodeLibDirs, OptionalApplications, ExcludeApps) ->
|
|
lists:foldl(fun(App, {AppAcc, SeenAcc}) ->
|
|
{NewApps, SeenAcc1} = subset(App, World, SeenAcc, LibDirs,
|
|
CheckCodeLibDirs, OptionalApplications, ExcludeApps),
|
|
%% put new apps after the existing list to keep the user defined order
|
|
{AppAcc ++ NewApps, SeenAcc1}
|
|
end, {[], Seen}, Apps).
|
|
|
|
set_resolved(Release0, Pkgs, State) ->
|
|
case rlx_release:realize(Release0, Pkgs) of
|
|
{ok, Release1} ->
|
|
?log_debug("Resolved ~p-~s", [rlx_release:name(Release1), rlx_release:vsn(Release1)]),
|
|
?log_debug("~s", [rlx_release:format(Release1)]),
|
|
case rlx_state:include_erts(State) of
|
|
IncludeErts when is_atom(IncludeErts) ->
|
|
{ok, Release1, rlx_state:add_realized_release(State, Release1)};
|
|
ErtsDir ->
|
|
try
|
|
%% figure out erts version from the path given
|
|
[Erts | _] = filelib:wildcard(filename:join(ErtsDir, "erts-*")),
|
|
[_, ErtsVsn] = rlx_string:lexemes(filename:basename(Erts), "-"),
|
|
Release2 = rlx_release:erts(Release1, ErtsVsn),
|
|
{ok, Release2, rlx_state:add_realized_release(State, Release2)}
|
|
catch
|
|
_:_ ->
|
|
erlang:error(?RLX_ERROR({release_erts_error, ErtsDir}))
|
|
end
|
|
end
|
|
end.
|
|
|
|
name_version(Name) when is_atom(Name) ->
|
|
{Name, undefined};
|
|
name_version({Name, #{vsn := Vsn}}) ->
|
|
{Name, Vsn}.
|
|
|
|
%% Applications are first searched for in the `Apps' variable which is a map
|
|
%% of application name to `rlx_app_info' map. This variable is passed to `relx'
|
|
%% function calls to build the release or tarball
|
|
%%
|
|
%% If the application isn't found in the `Apps' map then the `lib_dirs' list of
|
|
%% directories is searched for the application under `<dir>/*/ebin/Name.app'.
|
|
%% Lastly, if the application still isn't found then the code path is checked
|
|
%% using `code:lib_dir'.
|
|
find_app(Name, Vsn, Apps, LibDirs, CheckCodeLibDirs) ->
|
|
case maps:find(Name, Apps) of
|
|
{ok, AppInfo} ->
|
|
%% verify the app is the version we want and if not try
|
|
%% finding the app-vsn needed in the configured paths
|
|
case check_app(Name, Vsn, AppInfo) of
|
|
true ->
|
|
AppInfo;
|
|
false ->
|
|
search_for_app(Name, Vsn, LibDirs, CheckCodeLibDirs)
|
|
end;
|
|
error ->
|
|
search_for_app(Name, Vsn, LibDirs, CheckCodeLibDirs)
|
|
end.
|
|
|
|
search_for_app(Name, Vsn, LibDirs, CheckCodeLibDirs) ->
|
|
case find_app_in_dir(Name, Vsn, LibDirs) of
|
|
not_found when CheckCodeLibDirs =:= true ->
|
|
find_app_in_code_path(Name, Vsn);
|
|
not_found when CheckCodeLibDirs =:= false ->
|
|
%% app not found in any lib dir we are configured to search
|
|
%% and user set a custom `system_libs' directory so we do
|
|
%% not look in `code:lib_dir'
|
|
not_found;
|
|
AppInfo ->
|
|
case check_app(Name, Vsn, AppInfo) of
|
|
true ->
|
|
AppInfo;
|
|
false when CheckCodeLibDirs =:= true ->
|
|
find_app_in_code_path(Name, Vsn)
|
|
end
|
|
end.
|
|
|
|
find_app_in_dir(_Name, _Vsn, []) ->
|
|
not_found;
|
|
find_app_in_dir(Name, Vsn, [Dir | Rest]) ->
|
|
AppFile = filename:join([Dir, "*", "ebin", [Name, ".app"]]),
|
|
case filelib:wildcard(AppFile) of
|
|
[] ->
|
|
find_app_in_dir(Name, Vsn, Rest);
|
|
Matches ->
|
|
case rlx_util:list_search(fun(AppFilePath) ->
|
|
to_app(Name, Vsn, AppFilePath)
|
|
end, Matches) of
|
|
{value, App} ->
|
|
App;
|
|
false ->
|
|
find_app_in_dir(Name, Vsn, Rest)
|
|
end
|
|
end.
|
|
|
|
find_app_in_code_path(Name, Vsn) ->
|
|
case code:lib_dir(Name) of
|
|
{error, bad_name} ->
|
|
not_found;
|
|
Dir ->
|
|
case to_app(Name, Vsn, filename:join([Dir, "ebin", [Name, ".app"]])) of
|
|
{true, AppInfo} ->
|
|
AppInfo;
|
|
false ->
|
|
not_found
|
|
end
|
|
end.
|
|
|
|
%% returns true if the AppInfo is the same name and version as the
|
|
%% arguments where `undefined' means any version.
|
|
check_app(Name, undefined, App) ->
|
|
rlx_app_info:name(App) =:= Name;
|
|
check_app(Name, Vsn, App) ->
|
|
rlx_app_info:name(App) =:= Name
|
|
andalso rlx_app_info:vsn(App) =:= Vsn.
|
|
|
|
to_app(Name, Vsn, AppFilePath) ->
|
|
AppData = case file:consult(AppFilePath) of
|
|
{ok, [{application, _Name, Data}]} -> Data;
|
|
Other -> erlang:error(?RLX_ERROR({bad_app_file, AppFilePath, Other}))
|
|
end,
|
|
Applications = proplists:get_value(applications, AppData, []),
|
|
IncludedApplications = proplists:get_value(included_applications, AppData, []),
|
|
OptionalApplications = proplists:get_value(optional_applications, AppData, []),
|
|
|
|
case lists:keyfind(vsn, 1, AppData) of
|
|
{_, Vsn1} when Vsn =:= undefined ;
|
|
Vsn =:= Vsn1 ->
|
|
{true, #{name => Name,
|
|
vsn => Vsn1,
|
|
|
|
applications => Applications,
|
|
included_applications => IncludedApplications,
|
|
optional_applications => OptionalApplications,
|
|
|
|
dir => filename:dirname(filename:dirname(AppFilePath)),
|
|
link => false}};
|
|
_ ->
|
|
false
|
|
end.
|