summaryrefslogtreecommitdiff
path: root/src/rebar_paths.erl
diff options
context:
space:
mode:
authorsimonxu72 <simon.xu72@gmail.com>2018-10-16 18:37:28 +0800
committersimonxu72 <simon.xu72@gmail.com>2018-10-16 18:37:28 +0800
commit41d133856bf199034b0eeb0903bedc2071fba7e1 (patch)
tree15135eaf1501e016ec1b91b275356a0cfd92d867 /src/rebar_paths.erl
parentb81871c61809a9e5c09f54d6c8298908d18a760c (diff)
parent7bfc8110d1736d2cbf61e19d2fc16dc8e854b460 (diff)
Merge remote-tracking branch 'upstream/master'
Diffstat (limited to 'src/rebar_paths.erl')
-rw-r--r--src/rebar_paths.erl208
1 files changed, 208 insertions, 0 deletions
diff --git a/src/rebar_paths.erl b/src/rebar_paths.erl
new file mode 100644
index 0000000..82c0218
--- /dev/null
+++ b/src/rebar_paths.erl
@@ -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).