You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

211 lines
8.1 KiB

пре 6 година
  1. -module(rebar_paths).
  2. -include("rebar.hrl").
  3. -type target() :: deps | plugins.
  4. -type targets() :: [target(), ...].
  5. -export_type([target/0, targets/0]).
  6. -export([set_paths/2, unset_paths/2]).
  7. -export([clashing_apps/2]).
  8. -ifdef(TEST).
  9. -export([misloaded_modules/2]).
  10. -endif.
  11. -spec set_paths(targets(), rebar_state:t()) -> ok.
  12. set_paths(UserTargets, State) ->
  13. Targets = normalize_targets(UserTargets),
  14. GroupPaths = path_groups(Targets, State),
  15. Paths = lists:append(lists:reverse([P || {_, P} <- GroupPaths])),
  16. code:add_pathsa(Paths),
  17. AppGroups = app_groups(Targets, State),
  18. purge_and_load(AppGroups, sets:new()),
  19. ok.
  20. -spec unset_paths(targets(), rebar_state:t()) -> ok.
  21. unset_paths(UserTargets, State) ->
  22. Targets = normalize_targets(UserTargets),
  23. GroupPaths = path_groups(Targets, State),
  24. Paths = lists:append([P || {_, P} <- GroupPaths]),
  25. [code:del_path(P) || P <- Paths],
  26. purge(Paths, code:all_loaded()),
  27. ok.
  28. -spec clashing_apps(targets(), rebar_state:t()) -> [{target(), [binary()]}].
  29. clashing_apps(Targets, State) ->
  30. AppGroups = app_groups(Targets, State),
  31. AppNames = [{G, sets:from_list(
  32. [rebar_app_info:name(App) || App <- Apps]
  33. )} || {G, Apps} <- AppGroups],
  34. clashing_app_names(sets:new(), AppNames, []).
  35. %%%%%%%%%%%%%%%
  36. %%% PRIVATE %%%
  37. %%%%%%%%%%%%%%%
  38. %% The paths are to be set in the reverse order; i.e. the default
  39. %% path is always last when possible (minimize cases where a build
  40. %% tool version clashes with an app's), and put the highest priorities
  41. %% first.
  42. -spec normalize_targets(targets()) -> targets().
  43. normalize_targets(List) ->
  44. %% Plan for the eventuality of getting values piped in
  45. %% from future versions of rebar3, possibly from plugins and so on,
  46. %% which means we'd risk failing kind of violently. We only support
  47. %% deps and plugins
  48. TmpList = lists:foldl(
  49. fun(deps, [deps | _] = Acc) -> Acc;
  50. (plugins, [plugins | _] = Acc) -> Acc;
  51. (deps, Acc) -> [deps | Acc -- [deps]];
  52. (plugins, Acc) -> [plugins | Acc -- [plugins]];
  53. (_, Acc) -> Acc
  54. end,
  55. [],
  56. List
  57. ),
  58. lists:reverse(TmpList).
  59. purge_and_load([], _) ->
  60. ok;
  61. purge_and_load([{_Group, Apps}|Rest], Seen) ->
  62. %% We have: a list of all applications in the current priority group,
  63. %% a list of all loaded modules with their active path, and a list of
  64. %% seen applications.
  65. %%
  66. %% We do the following:
  67. %% 1. identify the apps that have not been solved yet
  68. %% 2. find the paths for all apps in the current group
  69. %% 3. unload and reload apps that may have changed paths in order
  70. %% to get updated module lists and specs
  71. %% (we ignore started apps and apps that have not run for this)
  72. %% This part turns out to be the bottleneck of this module, so
  73. %% to speed it up, using clash detection proves useful:
  74. %% only reload apps that clashed since others are unlikely to
  75. %% conflict in significant ways
  76. %% 4. create a list of modules to check from that app list—only loaded
  77. %% modules make sense to check.
  78. %% 5. check the modules to match their currently loaded paths with
  79. %% the path set from the apps in the current group; modules
  80. %% that differ must be purged; others can stay
  81. %% 1)
  82. AppNames = [AppName || App <- Apps,
  83. AppName <- [rebar_app_info:name(App)],
  84. not sets:is_element(AppName, Seen)],
  85. GoodApps = [App || AppName <- AppNames,
  86. App <- Apps,
  87. rebar_app_info:name(App) =:= AppName],
  88. %% 2)
  89. %% (no need for extra_src_dirs since those get put into ebin;
  90. %% also no need for OTP libs; we want to allow overtaking them)
  91. GoodAppPaths = [rebar_app_info:ebin_dir(App) || App <- GoodApps],
  92. %% 3)
  93. [begin
  94. AtomApp = binary_to_atom(AppName, utf8),
  95. %% blind load/unload won't interrupt an already-running app,
  96. %% preventing odd errors, maybe!
  97. case application:unload(AtomApp) of
  98. ok -> application:load(AtomApp);
  99. _ -> ok
  100. end
  101. end || AppName <- AppNames,
  102. %% Shouldn't unload ourselves; rebar runs without ever
  103. %% being started and unloading breaks logging!
  104. AppName =/= <<"rebar">>],
  105. %% 4)
  106. CandidateMods = lists:append(
  107. %% Start by asking the currently loaded app (if loaded)
  108. %% since it would be the primary source of conflicting modules
  109. [case application:get_key(AppName, modules) of
  110. {ok, Mods} ->
  111. Mods;
  112. undefined ->
  113. %% if not found, parse the app file on disk, in case
  114. %% the app's modules are used without it being loaded;
  115. %% invalidate the cache in case we're proceeding during
  116. %% compilation steps by setting the app details to `[]', which
  117. %% is its empty value; the details will then be reloaded
  118. %% from disk when found
  119. case rebar_app_info:app_details(rebar_app_info:app_details(App, [])) of
  120. [] -> [];
  121. Details -> proplists:get_value(modules, Details, [])
  122. end
  123. end || App <- GoodApps,
  124. AppName <- [binary_to_atom(rebar_app_info:name(App), utf8)]]
  125. ),
  126. ModPaths = [{Mod,Path} || Mod <- CandidateMods,
  127. erlang:function_exported(Mod, module_info, 0),
  128. {file, Path} <- [code:is_loaded(Mod)]],
  129. %% 5)
  130. Mods = misloaded_modules(GoodAppPaths, ModPaths),
  131. [purge_mod(Mod) || Mod <- Mods],
  132. purge_and_load(Rest, sets:union(Seen, sets:from_list(AppNames))).
  133. purge(Paths, ModPaths) ->
  134. SortedPaths = lists:sort(Paths),
  135. lists:map(fun purge_mod/1,
  136. [Mod || {Mod, Path} <- ModPaths,
  137. is_list(Path), % not 'preloaded' or mocked
  138. any_prefix(Path, SortedPaths)]
  139. ).
  140. misloaded_modules(GoodAppPaths, ModPaths) ->
  141. %% Identify paths that are invalid; i.e. app paths that cover an
  142. %% app in the desired group, but are not in the desired group.
  143. lists:usort(
  144. [Mod || {Mod, Path} <- ModPaths,
  145. is_list(Path), % not 'preloaded' or mocked
  146. not any_prefix(Path, GoodAppPaths)]
  147. ).
  148. any_prefix(Path, Paths) ->
  149. lists:any(fun(P) -> lists:prefix(P, Path) end, Paths).
  150. %% assume paths currently set are good; only unload a module so next call
  151. %% uses the correctly set paths
  152. purge_mod(Mod) ->
  153. code:soft_purge(Mod) andalso code:delete(Mod).
  154. %% This is a tricky O(n²) check since we want to
  155. %% know whether an app clashes with any of the top priority groups.
  156. %%
  157. %% For example, let's say we have `[deps, plugins]', then we want
  158. %% to find the plugins that clash with deps:
  159. %%
  160. %% `[{deps, [ClashingPlugins]}, {plugins, []}]'
  161. %%
  162. %% In case we'd ever have alternative or additional types, we can
  163. %% find all clashes from other 'groups'.
  164. clashing_app_names(_, [], Acc) ->
  165. lists:reverse(Acc);
  166. clashing_app_names(PrevNames, [{G,AppNames} | Rest], Acc) ->
  167. CurrentNames = sets:subtract(AppNames, PrevNames),
  168. NextNames = sets:subtract(sets:union([A || {_, A} <- Rest]), PrevNames),
  169. Clashes = sets:intersection(CurrentNames, NextNames),
  170. NewAcc = [{G, sets:to_list(Clashes)} | Acc],
  171. clashing_app_names(sets:union(PrevNames, CurrentNames), Rest, NewAcc).
  172. path_groups(Targets, State) ->
  173. [{Target, get_paths(Target, State)} || Target <- Targets].
  174. app_groups(Targets, State) ->
  175. [{Target, get_apps(Target, State)} || Target <- Targets].
  176. get_paths(deps, State) ->
  177. rebar_state:code_paths(State, all_deps);
  178. get_paths(plugins, State) ->
  179. rebar_state:code_paths(State, all_plugin_deps).
  180. get_apps(deps, State) ->
  181. %% The code paths for deps also include the top level apps
  182. %% and the extras, which we don't have here; we have to
  183. %% add the apps by hand
  184. case rebar_state:project_apps(State) of
  185. undefined -> [];
  186. List -> List
  187. end ++
  188. rebar_state:all_deps(State);
  189. get_apps(plugins, State) ->
  190. rebar_state:all_plugin_deps(State).