%% BEFORE THIS FIX: rebar3 ct 266.78s user 144.06s system 144% cpu 4:33.70 total %% CURRENT TIME: rebar3 ct 419.30s user 301.00s system 152% cpu 7:51.98 total -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).