您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

555 行
21 KiB

14 年前
  1. %% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
  2. %% ex: ts=4 sw=4 et
  3. %% -------------------------------------------------------------------
  4. %%
  5. %% rebar: Erlang Build Tools
  6. %%
  7. %% Copyright (c) 2009 Dave Smith (dizzyd@dizzyd.com)
  8. %%
  9. %% Permission is hereby granted, free of charge, to any person obtaining a copy
  10. %% of this software and associated documentation files (the "Software"), to deal
  11. %% in the Software without restriction, including without limitation the rights
  12. %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. %% copies of the Software, and to permit persons to whom the Software is
  14. %% furnished to do so, subject to the following conditions:
  15. %%
  16. %% The above copyright notice and this permission notice shall be included in
  17. %% all copies or substantial portions of the Software.
  18. %%
  19. %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. %% THE SOFTWARE.
  26. %% -------------------------------------------------------------------
  27. -module(rebar_core).
  28. -export([process_commands/2]).
  29. -include("rebar.hrl").
  30. %% ===================================================================
  31. %% Internal functions
  32. %% ===================================================================
  33. process_commands([], ParentConfig) ->
  34. AbortTrapped = rebar_config:get_global(abort_trapped, false),
  35. case {get_operations(ParentConfig), AbortTrapped} of
  36. {0, _} ->
  37. %% None of the commands had any effect
  38. ?ABORT;
  39. {_, true} ->
  40. %% An abort was previously trapped
  41. ?ABORT;
  42. _ ->
  43. ok
  44. end;
  45. process_commands([Command | Rest], ParentConfig) ->
  46. %% Reset skip dirs
  47. ParentConfig1 = rebar_config:reset_skip_dirs(ParentConfig),
  48. Operations = rebar_config:get_xconf(ParentConfig1, operations),
  49. ParentConfig4 =
  50. try
  51. %% Convert the code path so that all the entries are absolute paths.
  52. %% If not, code:set_path() may choke on invalid relative paths when trying
  53. %% to restore the code path from inside a subdirectory.
  54. true = rebar_utils:expand_code_path(),
  55. {ParentConfig2, _DirSet} = process_dir(rebar_utils:get_cwd(),
  56. ParentConfig1, Command,
  57. sets:new()),
  58. case get_operations(ParentConfig2) of
  59. Operations ->
  60. %% This command didn't do anything
  61. ?CONSOLE("Command '~p' not understood or not applicable~n",
  62. [Command]);
  63. _ ->
  64. ok
  65. end,
  66. ParentConfig3 = rebar_config:clean_config(ParentConfig1, ParentConfig2),
  67. %% Wipe out vsn cache to avoid invalid hits when
  68. %% dependencies are updated
  69. rebar_config:set_xconf(ParentConfig3, vsn_cache, dict:new())
  70. catch
  71. throw:rebar_abort ->
  72. case rebar_config:get_global(keep_going, false) of
  73. false ->
  74. ?ABORT;
  75. true ->
  76. ?WARN("Continuing on after abort: ~p\n", [Rest]),
  77. rebar_config:set_global(abort_trapped, true),
  78. ParentConfig1
  79. end
  80. end,
  81. process_commands(Rest, ParentConfig4).
  82. process_dir(Dir, ParentConfig, Command, DirSet) ->
  83. case filelib:is_dir(Dir) of
  84. false ->
  85. ?WARN("Skipping non-existent sub-dir: ~p\n", [Dir]),
  86. {ParentConfig, DirSet};
  87. true ->
  88. ok = file:set_cwd(Dir),
  89. Config = maybe_load_local_config(Dir, ParentConfig),
  90. %% Save the current code path and then update it with
  91. %% lib_dirs. Children inherit parents code path, but we
  92. %% also want to ensure that we restore everything to pristine
  93. %% condition after processing this child
  94. CurrentCodePath = update_code_path(Config),
  95. %% Get the list of processing modules and check each one against
  96. %% CWD to see if it's a fit -- if it is, use that set of modules
  97. %% to process this dir.
  98. {ok, AvailModuleSets} = application:get_env(rebar, modules),
  99. ModuleSet = choose_module_set(AvailModuleSets, Dir),
  100. maybe_process_dir(ModuleSet, Config, CurrentCodePath,
  101. Dir, Command, DirSet)
  102. end.
  103. maybe_process_dir({[], undefined}=ModuleSet, Config, CurrentCodePath,
  104. Dir, Command, DirSet) ->
  105. process_dir0(Dir, Command, DirSet, Config, CurrentCodePath, ModuleSet);
  106. maybe_process_dir({_, ModuleSetFile}=ModuleSet, Config, CurrentCodePath,
  107. Dir, Command, DirSet) ->
  108. case lists:suffix(".app.src", ModuleSetFile)
  109. orelse lists:suffix(".app", ModuleSetFile) of
  110. true ->
  111. %% .app or .app.src file, check if is_skipped_app
  112. maybe_process_dir0(ModuleSetFile, ModuleSet,
  113. Config, CurrentCodePath, Dir,
  114. Command, DirSet);
  115. false ->
  116. %% not an app dir, no need to consider apps=/skip_apps=
  117. process_dir0(Dir, Command, DirSet, Config,
  118. CurrentCodePath, ModuleSet)
  119. end.
  120. maybe_process_dir0(AppFile, ModuleSet, Config, CurrentCodePath,
  121. Dir, Command, DirSet) ->
  122. case rebar_app_utils:is_skipped_app(Config, AppFile) of
  123. {Config1, {true, SkippedApp}} ->
  124. ?DEBUG("Skipping app: ~p~n", [SkippedApp]),
  125. Config2 = increment_operations(Config1),
  126. {Config2, DirSet};
  127. {Config1, false} ->
  128. process_dir0(Dir, Command, DirSet, Config1,
  129. CurrentCodePath, ModuleSet)
  130. end.
  131. process_dir0(Dir, Command, DirSet, Config0, CurrentCodePath,
  132. {DirModules, ModuleSetFile}) ->
  133. %% Get the list of modules for "any dir". This is a catch-all list
  134. %% of modules that are processed in addition to modules associated
  135. %% with this directory type. These any_dir modules are processed
  136. %% FIRST.
  137. {ok, AnyDirModules} = application:get_env(rebar, any_dir_modules),
  138. Modules = AnyDirModules ++ DirModules,
  139. %% Invoke 'preprocess' on the modules -- this yields a list of other
  140. %% directories that should be processed _before_ the current one.
  141. {Config1, Predirs} = acc_modules(Modules, preprocess, Config0,
  142. ModuleSetFile),
  143. SubdirAssoc = remember_cwd_subdir(Dir, Predirs),
  144. %% Get the list of plug-in modules from rebar.config. These
  145. %% modules may participate in preprocess and postprocess.
  146. {ok, PluginModules} = plugin_modules(Config1, SubdirAssoc),
  147. {Config2, PluginPredirs} = acc_modules(PluginModules, preprocess,
  148. Config1, ModuleSetFile),
  149. AllPredirs = Predirs ++ PluginPredirs,
  150. ?DEBUG("Predirs: ~p\n", [AllPredirs]),
  151. {Config3, DirSet2} = process_each(AllPredirs, Command, Config2,
  152. ModuleSetFile, DirSet),
  153. %% Make sure the CWD is reset properly; processing the dirs may have
  154. %% caused it to change
  155. ok = file:set_cwd(Dir),
  156. %% Check that this directory is not on the skip list
  157. Config7 = case rebar_config:is_skip_dir(Config3, Dir) of
  158. true ->
  159. %% Do not execute the command on the directory, as some
  160. %% module has requested a skip on it.
  161. ?INFO("Skipping ~s in ~s\n", [Command, Dir]),
  162. Config3;
  163. false ->
  164. %% Check for and get command specific environments
  165. {Config4, Env} = setup_envs(Config3, Modules),
  166. %% Execute any before_command plugins on this directory
  167. Config5 = execute_pre(Command, PluginModules,
  168. Config4, ModuleSetFile, Env),
  169. %% Execute the current command on this directory
  170. Config6 = execute(Command, Modules ++ PluginModules,
  171. Config5, ModuleSetFile, Env),
  172. %% Execute any after_command plugins on this directory
  173. execute_post(Command, PluginModules,
  174. Config6, ModuleSetFile, Env)
  175. end,
  176. %% Mark the current directory as processed
  177. DirSet3 = sets:add_element(Dir, DirSet2),
  178. %% Invoke 'postprocess' on the modules. This yields a list of other
  179. %% directories that should be processed _after_ the current one.
  180. {Config8, Postdirs} = acc_modules(Modules ++ PluginModules, postprocess,
  181. Config7, ModuleSetFile),
  182. ?DEBUG("Postdirs: ~p\n", [Postdirs]),
  183. Res = process_each(Postdirs, Command, Config8,
  184. ModuleSetFile, DirSet3),
  185. %% Make sure the CWD is reset properly; processing the dirs may have
  186. %% caused it to change
  187. ok = file:set_cwd(Dir),
  188. %% Once we're all done processing, reset the code path to whatever
  189. %% the parent initialized it to
  190. restore_code_path(CurrentCodePath),
  191. %% Return the updated {config, dirset} as result
  192. Res.
  193. remember_cwd_subdir(Cwd, Subdirs) ->
  194. Store = fun(Dir, Dict) ->
  195. case dict:find(Dir, Dict) of
  196. error ->
  197. ?DEBUG("Associate sub_dir ~s with ~s~n",
  198. [Dir, Cwd]),
  199. dict:store(Dir, Cwd, Dict);
  200. {ok, Existing} ->
  201. ?ABORT("Internal consistency assertion failed.~n"
  202. "sub_dir ~s already associated with ~s.~n"
  203. "Duplicate sub_dirs or deps entries?",
  204. [Dir, Existing]),
  205. Dict
  206. end
  207. end,
  208. lists:foldl(Store, dict:new(), Subdirs).
  209. maybe_load_local_config(Dir, ParentConfig) ->
  210. %% We need to ensure we don't overwrite custom
  211. %% config when we are dealing with base_dir.
  212. case processing_base_dir(Dir) of
  213. true ->
  214. ParentConfig;
  215. false ->
  216. rebar_config:new(ParentConfig)
  217. end.
  218. processing_base_dir(Dir) ->
  219. Dir == rebar_config:get_global(base_dir, undefined).
  220. %%
  221. %% Given a list of directories and a set of previously processed directories,
  222. %% process each one we haven't seen yet
  223. %%
  224. process_each([], _Command, Config, _ModuleSetFile, DirSet) ->
  225. %% reset cached setup_env
  226. Config1 = rebar_config:reset_env(Config),
  227. {Config1, DirSet};
  228. process_each([Dir | Rest], Command, Config, ModuleSetFile, DirSet) ->
  229. case sets:is_element(Dir, DirSet) of
  230. true ->
  231. ?DEBUG("Skipping ~s; already processed!\n", [Dir]),
  232. process_each(Rest, Command, Config, ModuleSetFile, DirSet);
  233. false ->
  234. {Config1, DirSet2} = process_dir(Dir, Config, Command, DirSet),
  235. Config2 = rebar_config:clean_config(Config, Config1),
  236. %% reset cached setup_env
  237. Config3 = rebar_config:reset_env(Config2),
  238. process_each(Rest, Command, Config3, ModuleSetFile, DirSet2)
  239. end.
  240. %%
  241. %% Given a list of module sets from rebar.app and a directory, find
  242. %% the appropriate subset of modules for this directory
  243. %%
  244. choose_module_set([], _Dir) ->
  245. {[], undefined};
  246. choose_module_set([{Type, Modules} | Rest], Dir) ->
  247. case is_dir_type(Type, Dir) of
  248. {true, File} ->
  249. {Modules, File};
  250. false ->
  251. choose_module_set(Rest, Dir)
  252. end.
  253. is_dir_type(app_dir, Dir) ->
  254. rebar_app_utils:is_app_dir(Dir);
  255. is_dir_type(rel_dir, Dir) ->
  256. rebar_rel_utils:is_rel_dir(Dir);
  257. is_dir_type(_, _) ->
  258. false.
  259. execute_pre(Command, Modules, Config, ModuleFile, Env) ->
  260. execute_plugin_hook("pre_", Command, Modules,
  261. Config, ModuleFile, Env).
  262. execute_post(Command, Modules, Config, ModuleFile, Env) ->
  263. execute_plugin_hook("post_", Command, Modules,
  264. Config, ModuleFile, Env).
  265. execute_plugin_hook(Hook, Command, Modules, Config, ModuleFile, Env) ->
  266. HookFunction = list_to_atom(Hook ++ atom_to_list(Command)),
  267. execute(HookFunction, Modules, Config, ModuleFile, Env).
  268. %%
  269. %% Execute a command across all applicable modules
  270. %%
  271. execute(Command, Modules, Config, ModuleFile, Env) ->
  272. case select_modules(Modules, Command, []) of
  273. [] ->
  274. Cmd = atom_to_list(Command),
  275. case lists:prefix("pre_", Cmd)
  276. orelse lists:prefix("post_", Cmd) of
  277. true ->
  278. ok;
  279. false ->
  280. ?WARN("'~p' command does not apply to directory ~s\n",
  281. [Command, rebar_utils:get_cwd()])
  282. end,
  283. Config;
  284. TargetModules ->
  285. %% Provide some info on where we are
  286. Dir = rebar_utils:get_cwd(),
  287. ?CONSOLE("==> ~s (~s)\n", [filename:basename(Dir), Command]),
  288. Config1 = increment_operations(Config),
  289. %% Run the available modules
  290. apply_hooks(pre_hooks, Config1, Command, Env),
  291. case catch(run_modules(TargetModules, Command,
  292. Config1, ModuleFile)) of
  293. {ok, NewConfig} ->
  294. apply_hooks(post_hooks, NewConfig, Command, Env),
  295. NewConfig;
  296. {error, failed} ->
  297. ?ABORT;
  298. {Module, {error, _} = Other} ->
  299. ?ABORT("~p failed while processing ~s in module ~s: ~s\n",
  300. [Command, Dir, Module,
  301. io_lib:print(Other, 1, 80, -1)]);
  302. Other ->
  303. ?ABORT("~p failed while processing ~s: ~s\n",
  304. [Command, Dir, io_lib:print(Other, 1, 80, -1)])
  305. end
  306. end.
  307. %% Increment the count of operations, since some module
  308. %% responds to this command
  309. increment_operations(Config) ->
  310. Operations = get_operations(Config),
  311. rebar_config:set_xconf(Config, operations, Operations + 1).
  312. get_operations(Config) ->
  313. {ok, Operations} = rebar_config:get_xconf(Config, operations),
  314. Operations.
  315. update_code_path(Config) ->
  316. case rebar_config:get_local(Config, lib_dirs, []) of
  317. [] ->
  318. no_change;
  319. Paths ->
  320. OldPath = code:get_path(),
  321. LibPaths = expand_lib_dirs(Paths, rebar_utils:get_cwd(), []),
  322. ok = code:add_pathsa(LibPaths),
  323. {old, OldPath}
  324. end.
  325. restore_code_path(no_change) ->
  326. ok;
  327. restore_code_path({old, Path}) ->
  328. %% Verify that all of the paths still exist -- some dynamically
  329. %% added paths can get blown away during clean.
  330. true = code:set_path([F || F <- Path, filelib:is_file(F)]),
  331. ok.
  332. expand_lib_dirs([], _Root, Acc) ->
  333. Acc;
  334. expand_lib_dirs([Dir | Rest], Root, Acc) ->
  335. Apps = filelib:wildcard(filename:join([Dir, "*", "ebin"])),
  336. FqApps = [filename:join([Root, A]) || A <- Apps],
  337. expand_lib_dirs(Rest, Root, Acc ++ FqApps).
  338. select_modules([], _Command, Acc) ->
  339. lists:reverse(Acc);
  340. select_modules([Module | Rest], Command, Acc) ->
  341. {module, Module} = code:ensure_loaded(Module),
  342. case erlang:function_exported(Module, Command, 2) of
  343. true ->
  344. select_modules(Rest, Command, [Module | Acc]);
  345. false ->
  346. select_modules(Rest, Command, Acc)
  347. end.
  348. run_modules([], _Command, Config, _File) ->
  349. {ok, Config};
  350. run_modules([Module | Rest], Command, Config, File) ->
  351. case Module:Command(Config, File) of
  352. ok ->
  353. run_modules(Rest, Command, Config, File);
  354. {ok, NewConfig} ->
  355. run_modules(Rest, Command, NewConfig, File);
  356. {error, _} = Error ->
  357. {Module, Error}
  358. end.
  359. apply_hooks(Mode, Config, Command, Env) ->
  360. Hooks = rebar_config:get_local(Config, Mode, []),
  361. lists:foreach(fun apply_hook/1,
  362. [{Env, Hook} || Hook <- Hooks,
  363. element(1, Hook) =:= Command orelse
  364. element(2, Hook) =:= Command]).
  365. apply_hook({Env, {Arch, Command, Hook}}) ->
  366. case rebar_utils:is_arch(Arch) of
  367. true ->
  368. apply_hook({Env, {Command, Hook}});
  369. false ->
  370. ok
  371. end;
  372. apply_hook({Env, {Command, Hook}}) ->
  373. Msg = lists:flatten(io_lib:format("Command [~p] failed!~n", [Command])),
  374. rebar_utils:sh(Hook, [{env, Env}, {abort_on_error, Msg}]).
  375. setup_envs(Config, Modules) ->
  376. lists:foldl(fun(M, {C,E}=T) ->
  377. case erlang:function_exported(M, setup_env, 1) of
  378. true ->
  379. Env = M:setup_env(C),
  380. C1 = rebar_config:set_env(C, M, Env),
  381. {C1, E++Env};
  382. false ->
  383. T
  384. end
  385. end, {Config, []}, Modules).
  386. acc_modules(Modules, Command, Config, File) ->
  387. acc_modules(select_modules(Modules, Command, []),
  388. Command, Config, File, []).
  389. acc_modules([], _Command, Config, _File, Acc) ->
  390. {Config, Acc};
  391. acc_modules([Module | Rest], Command, Config, File, Acc) ->
  392. {Config1, Dirs1} = case Module:Command(Config, File) of
  393. {ok, Dirs} ->
  394. {Config, Dirs};
  395. {ok, NewConfig, Dirs} ->
  396. {NewConfig, Dirs}
  397. end,
  398. acc_modules(Rest, Command, Config1, File, Acc ++ Dirs1).
  399. %%
  400. %% Return a flat list of rebar plugin modules.
  401. %%
  402. plugin_modules(Config, SubdirAssoc) ->
  403. Modules = lists:flatten(rebar_config:get_all(Config, plugins)),
  404. plugin_modules(Config, SubdirAssoc, ulist(Modules)).
  405. ulist(L) ->
  406. ulist(L, []).
  407. ulist([], Acc) ->
  408. lists:reverse(Acc);
  409. ulist([H | T], Acc) ->
  410. case lists:member(H, Acc) of
  411. true ->
  412. ulist(T, Acc);
  413. false ->
  414. ulist(T, [H | Acc])
  415. end.
  416. plugin_modules(_Config, _SubdirAssoc, []) ->
  417. {ok, []};
  418. plugin_modules(Config, SubdirAssoc, Modules) ->
  419. FoundModules = [M || M <- Modules, code:which(M) =/= non_existing],
  420. plugin_modules(Config, SubdirAssoc, FoundModules, Modules -- FoundModules).
  421. plugin_modules(_Config, _SubdirAssoc, FoundModules, []) ->
  422. {ok, FoundModules};
  423. plugin_modules(Config, SubdirAssoc, FoundModules, MissingModules) ->
  424. {Loaded, NotLoaded} = load_plugin_modules(Config, SubdirAssoc,
  425. MissingModules),
  426. AllViablePlugins = FoundModules ++ Loaded,
  427. case NotLoaded =/= [] of
  428. true ->
  429. %% NB: we continue to ignore this situation, as did the
  430. %% original code
  431. ?WARN("Missing plugins: ~p\n", [NotLoaded]);
  432. false ->
  433. ?DEBUG("Loaded plugins: ~p~n", [AllViablePlugins]),
  434. ok
  435. end,
  436. {ok, AllViablePlugins}.
  437. load_plugin_modules(Config, SubdirAssoc, Modules) ->
  438. Cwd = rebar_utils:get_cwd(),
  439. PluginDir = case rebar_config:get_local(Config, plugin_dir, undefined) of
  440. undefined ->
  441. filename:join(Cwd, "plugins");
  442. Dir ->
  443. Dir
  444. end,
  445. %% Find relevant sources in base_dir and plugin_dir
  446. Erls = string:join([atom_to_list(M)++"\\.erl" || M <- Modules], "|"),
  447. RE = "^" ++ Erls ++ "\$",
  448. BaseDir = get_plugin_base_dir(Cwd, SubdirAssoc),
  449. %% If a plugin is found both in base_dir and plugin_dir, the clash
  450. %% will provoke an error and we'll abort.
  451. Sources = rebar_utils:find_files(PluginDir, RE, false)
  452. ++ rebar_utils:find_files(BaseDir, RE, false),
  453. %% Compile and load plugins
  454. Loaded = [load_plugin(Src) || Src <- Sources],
  455. FilterMissing = is_missing_plugin(Loaded),
  456. NotLoaded = [V || V <- Modules, FilterMissing(V)],
  457. {Loaded, NotLoaded}.
  458. get_plugin_base_dir(Cwd, SubdirAssoc) ->
  459. case dict:find(Cwd, SubdirAssoc) of
  460. {ok, BaseDir} ->
  461. BaseDir;
  462. error ->
  463. Cwd
  464. end.
  465. is_missing_plugin(Loaded) ->
  466. fun(Mod) -> not lists:member(Mod, Loaded) end.
  467. load_plugin(Src) ->
  468. case compile:file(Src, [binary, return_errors]) of
  469. {ok, Mod, Bin} ->
  470. load_plugin_module(Mod, Bin, Src);
  471. {error, Errors, _Warnings} ->
  472. ?ABORT("Plugin ~s contains compilation errors: ~p~n",
  473. [Src, Errors])
  474. end.
  475. load_plugin_module(Mod, Bin, Src) ->
  476. case code:is_loaded(Mod) of
  477. {file, Loaded} ->
  478. ?ABORT("Plugin ~p clashes with previously loaded module ~p~n",
  479. [Mod, Loaded]);
  480. false ->
  481. ?INFO("Loading plugin ~p from ~s~n", [Mod, Src]),
  482. {module, Mod} = code:load_binary(Mod, Src, Bin),
  483. Mod
  484. end.