summaryrefslogtreecommitdiff
path: root/src/rebar_paths.erl
blob: 160f9fa2afd44c44ab8caa5b3deaf9cf72781f1c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
-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;
                 %% invalidate the cache in case we're proceeding during
                 %% compilation steps by setting the app details to `[]', which
                 %% is its empty value; the details will then be reloaded
                 %% from disk when found
                 case rebar_app_info:app_details(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).