-
Notifications
You must be signed in to change notification settings - Fork 521
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1907 from ferd/refactor-env-paths
Refactor env path handling and fix some bugs related to it
- Loading branch information
Showing
16 changed files
with
523 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
-module(rebar_paths). | ||
-include("rebar.hrl"). | ||
|
||
-type target() :: deps | plugins. | ||
-type targets() :: [target(), ...]. | ||
-export_type([target/0, targets/0]). | ||
-export([set_paths/2, unset_paths/2]). | ||
-export([clashing_apps/2]). | ||
|
||
-ifdef(TEST). | ||
-export([misloaded_modules/2]). | ||
-endif. | ||
|
||
-spec set_paths(targets(), rebar_state:t()) -> ok. | ||
set_paths(UserTargets, State) -> | ||
Targets = normalize_targets(UserTargets), | ||
GroupPaths = path_groups(Targets, State), | ||
Paths = lists:append(lists:reverse([P || {_, P} <- GroupPaths])), | ||
code:add_pathsa(Paths), | ||
AppGroups = app_groups(Targets, State), | ||
purge_and_load(AppGroups, sets:new()), | ||
ok. | ||
|
||
-spec unset_paths(targets(), rebar_state:t()) -> ok. | ||
unset_paths(UserTargets, State) -> | ||
Targets = normalize_targets(UserTargets), | ||
GroupPaths = path_groups(Targets, State), | ||
Paths = lists:append([P || {_, P} <- GroupPaths]), | ||
[code:del_path(P) || P <- Paths], | ||
purge(Paths, code:all_loaded()), | ||
ok. | ||
|
||
-spec clashing_apps(targets(), rebar_state:t()) -> [{target(), [binary()]}]. | ||
clashing_apps(Targets, State) -> | ||
AppGroups = app_groups(Targets, State), | ||
AppNames = [{G, sets:from_list( | ||
[rebar_app_info:name(App) || App <- Apps] | ||
)} || {G, Apps} <- AppGroups], | ||
clashing_app_names(sets:new(), AppNames, []). | ||
|
||
%%%%%%%%%%%%%%% | ||
%%% PRIVATE %%% | ||
%%%%%%%%%%%%%%% | ||
|
||
%% The paths are to be set in the reverse order; i.e. the default | ||
%% path is always last when possible (minimize cases where a build | ||
%% tool version clashes with an app's), and put the highest priorities | ||
%% first. | ||
-spec normalize_targets(targets()) -> targets(). | ||
normalize_targets(List) -> | ||
%% Plan for the eventuality of getting values piped in | ||
%% from future versions of rebar3, possibly from plugins and so on, | ||
%% which means we'd risk failing kind of violently. We only support | ||
%% deps and plugins | ||
TmpList = lists:foldl( | ||
fun(deps, [deps | _] = Acc) -> Acc; | ||
(plugins, [plugins | _] = Acc) -> Acc; | ||
(deps, Acc) -> [deps | Acc -- [deps]]; | ||
(plugins, Acc) -> [plugins | Acc -- [plugins]]; | ||
(_, Acc) -> Acc | ||
end, | ||
[], | ||
List | ||
), | ||
lists:reverse(TmpList). | ||
|
||
purge_and_load([], _) -> | ||
ok; | ||
purge_and_load([{_Group, Apps}|Rest], Seen) -> | ||
%% We have: a list of all applications in the current priority group, | ||
%% a list of all loaded modules with their active path, and a list of | ||
%% seen applications. | ||
%% | ||
%% We do the following: | ||
%% 1. identify the apps that have not been solved yet | ||
%% 2. find the paths for all apps in the current group | ||
%% 3. unload and reload apps that may have changed paths in order | ||
%% to get updated module lists and specs | ||
%% (we ignore started apps and apps that have not run for this) | ||
%% This part turns out to be the bottleneck of this module, so | ||
%% to speed it up, using clash detection proves useful: | ||
%% only reload apps that clashed since others are unlikely to | ||
%% conflict in significant ways | ||
%% 4. create a list of modules to check from that app list—only loaded | ||
%% modules make sense to check. | ||
%% 5. check the modules to match their currently loaded paths with | ||
%% the path set from the apps in the current group; modules | ||
%% that differ must be purged; others can stay | ||
|
||
%% 1) | ||
AppNames = [AppName || App <- Apps, | ||
AppName <- [rebar_app_info:name(App)], | ||
not sets:is_element(AppName, Seen)], | ||
GoodApps = [App || AppName <- AppNames, | ||
App <- Apps, | ||
rebar_app_info:name(App) =:= AppName], | ||
%% 2) | ||
%% (no need for extra_src_dirs since those get put into ebin; | ||
%% also no need for OTP libs; we want to allow overtaking them) | ||
GoodAppPaths = [rebar_app_info:ebin_dir(App) || App <- GoodApps], | ||
%% 3) | ||
[begin | ||
AtomApp = binary_to_atom(AppName, utf8), | ||
%% blind load/unload won't interrupt an already-running app, | ||
%% preventing odd errors, maybe! | ||
case application:unload(AtomApp) of | ||
ok -> application:load(AtomApp); | ||
_ -> ok | ||
end | ||
end || AppName <- AppNames, | ||
%% Shouldn't unload ourselves; rebar runs without ever | ||
%% being started and unloading breaks logging! | ||
AppName =/= <<"rebar">>], | ||
|
||
%% 4) | ||
CandidateMods = lists:append( | ||
%% Start by asking the currently loaded app (if loaded) | ||
%% since it would be the primary source of conflicting modules | ||
[case application:get_key(AppName, modules) of | ||
{ok, Mods} -> | ||
Mods; | ||
undefined -> | ||
%% if not found, parse the app file on disk, in case | ||
%% the app's modules are used without it being loaded | ||
case rebar_app_info:app_details(App) of | ||
[] -> []; | ||
Details -> proplists:get_value(modules, Details, []) | ||
end | ||
end || App <- GoodApps, | ||
AppName <- [binary_to_atom(rebar_app_info:name(App), utf8)]] | ||
), | ||
ModPaths = [{Mod,Path} || Mod <- CandidateMods, | ||
erlang:function_exported(Mod, module_info, 0), | ||
{file, Path} <- [code:is_loaded(Mod)]], | ||
|
||
%% 5) | ||
Mods = misloaded_modules(GoodAppPaths, ModPaths), | ||
[purge_mod(Mod) || Mod <- Mods], | ||
|
||
purge_and_load(Rest, sets:union(Seen, sets:from_list(AppNames))). | ||
|
||
purge(Paths, ModPaths) -> | ||
SortedPaths = lists:sort(Paths), | ||
lists:map(fun purge_mod/1, | ||
[Mod || {Mod, Path} <- ModPaths, | ||
is_list(Path), % not 'preloaded' or mocked | ||
any_prefix(Path, SortedPaths)] | ||
). | ||
|
||
misloaded_modules(GoodAppPaths, ModPaths) -> | ||
%% Identify paths that are invalid; i.e. app paths that cover an | ||
%% app in the desired group, but are not in the desired group. | ||
lists:usort( | ||
[Mod || {Mod, Path} <- ModPaths, | ||
is_list(Path), % not 'preloaded' or mocked | ||
not any_prefix(Path, GoodAppPaths)] | ||
). | ||
|
||
any_prefix(Path, Paths) -> | ||
lists:any(fun(P) -> lists:prefix(P, Path) end, Paths). | ||
|
||
%% assume paths currently set are good; only unload a module so next call | ||
%% uses the correctly set paths | ||
purge_mod(Mod) -> | ||
code:soft_purge(Mod) andalso code:delete(Mod). | ||
|
||
|
||
%% This is a tricky O(n²) check since we want to | ||
%% know whether an app clashes with any of the top priority groups. | ||
%% | ||
%% For example, let's say we have `[deps, plugins]', then we want | ||
%% to find the plugins that clash with deps: | ||
%% | ||
%% `[{deps, [ClashingPlugins]}, {plugins, []}]' | ||
%% | ||
%% In case we'd ever have alternative or additional types, we can | ||
%% find all clashes from other 'groups'. | ||
clashing_app_names(_, [], Acc) -> | ||
lists:reverse(Acc); | ||
clashing_app_names(PrevNames, [{G,AppNames} | Rest], Acc) -> | ||
CurrentNames = sets:subtract(AppNames, PrevNames), | ||
NextNames = sets:subtract(sets:union([A || {_, A} <- Rest]), PrevNames), | ||
Clashes = sets:intersection(CurrentNames, NextNames), | ||
NewAcc = [{G, sets:to_list(Clashes)} | Acc], | ||
clashing_app_names(sets:union(PrevNames, CurrentNames), Rest, NewAcc). | ||
|
||
path_groups(Targets, State) -> | ||
[{Target, get_paths(Target, State)} || Target <- Targets]. | ||
|
||
app_groups(Targets, State) -> | ||
[{Target, get_apps(Target, State)} || Target <- Targets]. | ||
|
||
get_paths(deps, State) -> | ||
rebar_state:code_paths(State, all_deps); | ||
get_paths(plugins, State) -> | ||
rebar_state:code_paths(State, all_plugin_deps). | ||
|
||
get_apps(deps, State) -> | ||
%% The code paths for deps also include the top level apps | ||
%% and the extras, which we don't have here; we have to | ||
%% add the apps by hand | ||
case rebar_state:project_apps(State) of | ||
undefined -> []; | ||
List -> List | ||
end ++ | ||
rebar_state:all_deps(State); | ||
get_apps(plugins, State) -> | ||
rebar_state:all_plugin_deps(State). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.