選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

242 行
9.7 KiB

  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, plugins and runtime deps.
  48. TmpList = lists:foldl(
  49. fun(deps, [deps | _] = Acc) -> Acc;
  50. (plugins, [plugins | _] = Acc) -> Acc;
  51. (runtime, [runtime | _] = Acc) -> Acc;
  52. (deps, Acc) -> [deps | Acc -- [deps]];
  53. (plugins, Acc) -> [plugins | Acc -- [plugins]];
  54. (runtime, Acc) -> [runtime | Acc -- [runtime]];
  55. (_, Acc) -> Acc
  56. end,
  57. [],
  58. List
  59. ),
  60. lists:reverse(TmpList).
  61. purge_and_load([], _) ->
  62. ok;
  63. purge_and_load([{_Group, Apps}|Rest], Seen) ->
  64. %% We have: a list of all applications in the current priority group,
  65. %% a list of all loaded modules with their active path, and a list of
  66. %% seen applications.
  67. %%
  68. %% We do the following:
  69. %% 1. identify the apps that have not been solved yet
  70. %% 2. find the paths for all apps in the current group
  71. %% 3. unload and reload apps that may have changed paths in order
  72. %% to get updated module lists and specs
  73. %% (we ignore started apps and apps that have not run for this)
  74. %% This part turns out to be the bottleneck of this module, so
  75. %% to speed it up, using clash detection proves useful:
  76. %% only reload apps that clashed since others are unlikely to
  77. %% conflict in significant ways
  78. %% 4. create a list of modules to check from that app list—only loaded
  79. %% modules make sense to check.
  80. %% 5. check the modules to match their currently loaded paths with
  81. %% the path set from the apps in the current group; modules
  82. %% that differ must be purged; others can stay
  83. %% 1)
  84. AppNames = [AppName || App <- Apps,
  85. AppName <- [rebar_app_info:name(App)],
  86. not sets:is_element(AppName, Seen)],
  87. GoodApps = [App || AppName <- AppNames,
  88. App <- Apps,
  89. rebar_app_info:name(App) =:= AppName],
  90. %% 2)
  91. %% (no need for extra_src_dirs since those get put into ebin;
  92. %% also no need for OTP libs; we want to allow overtaking them)
  93. GoodAppPaths = [rebar_app_info:ebin_dir(App) || App <- GoodApps],
  94. %% 3)
  95. [begin
  96. AtomApp = binary_to_atom(AppName, utf8),
  97. %% blind load/unload won't interrupt an already-running app,
  98. %% preventing odd errors, maybe!
  99. case application:unload(AtomApp) of
  100. ok -> application:load(AtomApp);
  101. _ -> ok
  102. end
  103. end || AppName <- AppNames,
  104. %% Shouldn't unload ourselves; rebar runs without ever
  105. %% being started and unloading breaks logging!
  106. AppName =/= <<"rebar">>],
  107. %% 4)
  108. CandidateMods = lists:append(
  109. %% Start by asking the currently loaded app (if loaded)
  110. %% since it would be the primary source of conflicting modules
  111. [case application:get_key(AppName, modules) of
  112. {ok, Mods} ->
  113. Mods;
  114. undefined ->
  115. %% if not found, parse the app file on disk, in case
  116. %% the app's modules are used without it being loaded;
  117. %% invalidate the cache in case we're proceeding during
  118. %% compilation steps by setting the app details to `[]', which
  119. %% is its empty value; the details will then be reloaded
  120. %% from disk when found
  121. case rebar_app_info:app_details(rebar_app_info:app_details(App, [])) of
  122. [] -> [];
  123. Details -> proplists:get_value(modules, Details, [])
  124. end
  125. end || App <- GoodApps,
  126. AppName <- [binary_to_atom(rebar_app_info:name(App), utf8)]]
  127. ),
  128. ModPaths = [{Mod,Path} || Mod <- CandidateMods,
  129. erlang:function_exported(Mod, module_info, 0),
  130. {file, Path} <- [code:is_loaded(Mod)]],
  131. %% 5)
  132. Mods = misloaded_modules(GoodAppPaths, ModPaths),
  133. [purge_mod(Mod) || Mod <- Mods],
  134. purge_and_load(Rest, sets:union(Seen, sets:from_list(AppNames))).
  135. purge(Paths, ModPaths) ->
  136. SortedPaths = lists:sort(Paths),
  137. lists:map(fun purge_mod/1,
  138. [Mod || {Mod, Path} <- ModPaths,
  139. is_list(Path), % not 'preloaded' or mocked
  140. any_prefix(Path, SortedPaths)]
  141. ).
  142. misloaded_modules(GoodAppPaths, ModPaths) ->
  143. %% Identify paths that are invalid; i.e. app paths that cover an
  144. %% app in the desired group, but are not in the desired group.
  145. lists:usort(
  146. [Mod || {Mod, Path} <- ModPaths,
  147. is_list(Path), % not 'preloaded' or mocked
  148. not any_prefix(Path, GoodAppPaths)]
  149. ).
  150. any_prefix(Path, Paths) ->
  151. lists:any(fun(P) -> lists:prefix(P, Path) end, Paths).
  152. %% assume paths currently set are good; only unload a module so next call
  153. %% uses the correctly set paths
  154. purge_mod(Mod) ->
  155. code:soft_purge(Mod) andalso code:delete(Mod).
  156. %% This is a tricky O(n²) check since we want to
  157. %% know whether an app clashes with any of the top priority groups.
  158. %%
  159. %% For example, let's say we have `[deps, plugins]', then we want
  160. %% to find the plugins that clash with deps:
  161. %%
  162. %% `[{deps, [ClashingPlugins]}, {plugins, []}]'
  163. %%
  164. %% In case we'd ever have alternative or additional types, we can
  165. %% find all clashes from other 'groups'.
  166. clashing_app_names(_, [], Acc) ->
  167. lists:reverse(Acc);
  168. clashing_app_names(PrevNames, [{G,AppNames} | Rest], Acc) ->
  169. CurrentNames = sets:subtract(AppNames, PrevNames),
  170. NextNames = sets:subtract(sets:union([A || {_, A} <- Rest]), PrevNames),
  171. Clashes = sets:intersection(CurrentNames, NextNames),
  172. NewAcc = [{G, sets:to_list(Clashes)} | Acc],
  173. clashing_app_names(sets:union(PrevNames, CurrentNames), Rest, NewAcc).
  174. path_groups(Targets, State) ->
  175. [{Target, get_paths(Target, State)} || Target <- Targets].
  176. app_groups(Targets, State) ->
  177. [{Target, get_apps(Target, State)} || Target <- Targets].
  178. get_paths(deps, State) ->
  179. rebar_state:code_paths(State, all_deps);
  180. get_paths(plugins, State) ->
  181. rebar_state:code_paths(State, all_plugin_deps);
  182. get_paths(runtime, State) ->
  183. RuntimeApps = get_apps(runtime, State),
  184. [rebar_app_info:ebin_dir(App) || App <- RuntimeApps].
  185. get_apps(deps, State) ->
  186. %% The code paths for deps also include the top level apps
  187. %% and the extras, which we don't have here; we have to
  188. %% add the apps by hand
  189. case rebar_state:project_apps(State) of
  190. undefined -> [];
  191. List -> List
  192. end ++
  193. rebar_state:all_deps(State);
  194. get_apps(plugins, State) ->
  195. rebar_state:all_plugin_deps(State);
  196. get_apps(runtime, State) ->
  197. %% We get all project apps and for each of them we find
  198. %% their runtime deps (i.e., `applications' and `included_applications').
  199. ProjectApps = rebar_state:project_apps(State),
  200. AppsList = rebar_state:project_apps(State) ++ rebar_state:all_deps(State),
  201. get_runtime_apps(ProjectApps, sets:new(), AppsList).
  202. get_runtime_apps([], RuntimeApps, _AppsList) -> sets:to_list(RuntimeApps);
  203. %% We skip those apps that are not AppInfos.
  204. get_runtime_apps([App|Rest], AppsAcc0, AppsList) when is_atom(App) orelse is_binary(App) ->
  205. get_runtime_apps(Rest, AppsAcc0, AppsList);
  206. get_runtime_apps([App|Rest0], AppsAcc0, AppsList) ->
  207. Apps = rebar_app_info:applications(App),
  208. IncludedApps = rebar_app_info:included_applications(App),
  209. TotalApps0 = [atom_to_binary(A, utf8) || A <- (Apps ++ IncludedApps)],
  210. TotalApps = TotalApps0 -- [rebar_app_info:name(A) || A <- sets:to_list(AppsAcc0)],
  211. {Rest1, AppsAcc1} =
  212. lists:foldl(
  213. fun(AppName, {Rest, Acc}) ->
  214. %% We only care about those apps we ccould find in the state.
  215. case rebar_app_utils:find(AppName, AppsList) of
  216. {ok, AppInfo} -> {[AppInfo|Rest], sets:add_element(AppInfo, Acc)};
  217. error -> {Rest, Acc}
  218. end
  219. end, {Rest0, sets:add_element(App, AppsAcc0)}, TotalApps),
  220. get_runtime_apps(Rest1 ++ TotalApps, AppsAcc1, AppsList).