Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

455 lignes
18 KiB

il y a 5 ans
  1. -module(rebar_compiler_erl).
  2. -behaviour(rebar_compiler).
  3. -export([context/1,
  4. needed_files/4,
  5. dependencies/3, dependencies/4,
  6. compile/4, compile_and_track/4,
  7. clean/2,
  8. format_error/1]).
  9. -include("rebar.hrl").
  10. -include_lib("providers/include/providers.hrl").
  11. context(AppInfo) ->
  12. EbinDir = rebar_app_info:ebin_dir(AppInfo),
  13. Mappings = [{".beam", EbinDir}],
  14. OutDir = rebar_app_info:dir(AppInfo),
  15. SrcDirs = rebar_dir:src_dirs(rebar_app_info:opts(AppInfo), ["src"]),
  16. ExistingSrcDirs = lists:filter(fun(D) ->
  17. ec_file:is_dir(filename:join(OutDir, D))
  18. end, SrcDirs),
  19. RebarOpts = rebar_app_info:opts(AppInfo),
  20. ErlOpts = rebar_opts:erl_opts(RebarOpts),
  21. ErlOptIncludes = proplists:get_all_values(i, ErlOpts),
  22. InclDirs = lists:map(fun(Incl) -> filename:absname(Incl) end, ErlOptIncludes),
  23. AbsIncl = [filename:join([OutDir, "include"]) | InclDirs],
  24. PTrans = proplists:get_all_values(parse_transform, ErlOpts),
  25. Macros = [case Tup of
  26. {d,Name} -> Name;
  27. {d,Name,Val} -> {Name,Val}
  28. end || Tup <- ErlOpts,
  29. is_tuple(Tup) andalso element(1,Tup) == d],
  30. #{src_dirs => ExistingSrcDirs,
  31. include_dirs => AbsIncl,
  32. src_ext => ".erl",
  33. out_mappings => Mappings,
  34. dependencies_opts => [{includes, AbsIncl}, {macros, Macros},
  35. {parse_transforms, PTrans}]}.
  36. needed_files(Graph, FoundFiles, _, AppInfo) ->
  37. OutDir = rebar_app_info:out_dir(AppInfo),
  38. Dir = rebar_app_info:dir(AppInfo),
  39. EbinDir = rebar_app_info:ebin_dir(AppInfo),
  40. RebarOpts = rebar_app_info:opts(AppInfo),
  41. ErlOpts = rebar_opts:erl_opts(RebarOpts),
  42. ?DEBUG("compile options: {erl_opts, ~p}.", [ErlOpts]),
  43. ?DEBUG("files to analyze ~p", [FoundFiles]),
  44. %% Make sure that the ebin dir is on the path
  45. ok = rebar_file_utils:ensure_dir(EbinDir),
  46. true = code:add_patha(filename:absname(EbinDir)),
  47. {ParseTransforms, Rest} = split_source_files(FoundFiles, ErlOpts),
  48. NeededErlFiles = case needed_files(Graph, ErlOpts, RebarOpts, OutDir, EbinDir, ParseTransforms) of
  49. [] ->
  50. needed_files(Graph, ErlOpts, RebarOpts, OutDir, EbinDir, Rest);
  51. _ ->
  52. %% at least one parse transform in the opts needs updating, so recompile all
  53. FoundFiles
  54. end,
  55. {ErlFirstFiles, ErlOptsFirst} = erl_first_files(RebarOpts, ErlOpts, Dir, NeededErlFiles),
  56. SubGraph = digraph_utils:subgraph(Graph, NeededErlFiles),
  57. DepErlsOrdered = digraph_utils:topsort(SubGraph),
  58. %% Break out the files required by other modules from those
  59. %% that none other depend of; the former must be sequentially
  60. %% built, the rest is parallelizable.
  61. OtherErls = lists:partition(
  62. fun(Erl) -> lists:any(
  63. fun(Edge) ->
  64. {_E, _V1, _V2, Kind} = digraph:edge(Graph, Edge),
  65. Kind =/= artifact
  66. end, digraph:in_edges(Graph, Erl)) end,
  67. lists:reverse([Dep || Dep <- DepErlsOrdered,
  68. not lists:member(Dep, ErlFirstFiles)])
  69. ),
  70. PrivIncludes = [{i, filename:join(OutDir, Src)}
  71. || Src <- rebar_dir:all_src_dirs(RebarOpts, ["src"], [])],
  72. AdditionalOpts = PrivIncludes ++ [{i, filename:join(OutDir, "include")}, {i, OutDir}, return],
  73. true = digraph:delete(SubGraph),
  74. {{ErlFirstFiles, ErlOptsFirst ++ AdditionalOpts},
  75. {OtherErls, ErlOpts ++ AdditionalOpts}}.
  76. dependencies(Source, SourceDir, Dirs) ->
  77. case file:open(Source, [read]) of
  78. {ok, Fd} ->
  79. Incls = parse_attrs(Fd, [], SourceDir),
  80. AbsIncls = expand_file_names(Incls, Dirs),
  81. ok = file:close(Fd),
  82. AbsIncls;
  83. {error, Reason} ->
  84. throw(?PRV_ERROR({cannot_read_file, Source, file:format_error(Reason)}))
  85. end.
  86. dependencies(Source, _SourceDir, Dirs, DepOpts) ->
  87. rebar_compiler_epp:ensure_started(),
  88. OptPTrans = proplists:get_value(parse_transforms, DepOpts, []),
  89. try rebar_compiler_epp:deps(Source, DepOpts) of
  90. #{include := AbsIncls,
  91. missing_include_file := _MissIncl,
  92. missing_include_lib := _MissInclLib,
  93. parse_transform := PTrans,
  94. behaviour := Behaviours} ->
  95. %% TODO: check for core transforms?
  96. {_MissIncl, _MissInclLib} =/= {[],[]} andalso
  97. ?DEBUG("Missing: ~p", [{_MissIncl, _MissInclLib}]),
  98. lists:filtermap(
  99. fun (Mod) -> rebar_compiler_epp:resolve_source(Mod, Dirs) end,
  100. OptPTrans ++ PTrans ++ Behaviours) ++ AbsIncls
  101. catch
  102. error:{badmatch, {error, Reason}} ->
  103. case file:format_error(Reason) of
  104. "unknown POSIX error" ->
  105. throw(?PRV_ERROR({cannot_read_file, Source, Reason}));
  106. ReadableReason ->
  107. throw(?PRV_ERROR({cannot_read_file, Source, ReadableReason}))
  108. end;
  109. error:Reason ->
  110. throw(?PRV_ERROR({cannot_read_file, Source, Reason}))
  111. end.
  112. compile(Source, [{_, OutDir}], Config, ErlOpts) ->
  113. case compile:file(Source, [{outdir, OutDir} | ErlOpts]) of
  114. {ok, _Mod} ->
  115. ok;
  116. {ok, _Mod, []} ->
  117. ok;
  118. {ok, _Mod, Ws} ->
  119. FormattedWs = format_error_sources(Ws, Config),
  120. rebar_compiler:ok_tuple(Source, FormattedWs);
  121. {error, Es, Ws} ->
  122. error_tuple(Source, Es, Ws, Config, ErlOpts);
  123. error ->
  124. error
  125. end.
  126. compile_and_track(Source, [{Ext, OutDir}], Config, ErlOpts) ->
  127. rebar_compiler_epp:flush(),
  128. BuildOpts = [{outdir, OutDir} | ErlOpts],
  129. Target = target_base(OutDir, Source) ++ Ext,
  130. AllOpts = case erlang:function_exported(compile, env_compiler_options, 0) of
  131. true -> BuildOpts ++ compile:env_compiler_options();
  132. false -> BuildOpts
  133. end,
  134. case compile:file(Source, BuildOpts) of
  135. {ok, _Mod} ->
  136. {ok, [{Source, Target, AllOpts}]};
  137. {ok, _Mod, []} ->
  138. {ok, [{Source, Target, AllOpts}]};
  139. {ok, _Mod, Ws} ->
  140. FormattedWs = format_error_sources(Ws, Config),
  141. {ok, Warns} = rebar_compiler:ok_tuple(Source, FormattedWs),
  142. {ok, [{Source, Target, AllOpts}], Warns};
  143. {error, Es, Ws} ->
  144. error_tuple(Source, Es, Ws, Config, ErlOpts);
  145. error ->
  146. error
  147. end.
  148. clean(Files, AppInfo) ->
  149. EbinDir = rebar_app_info:ebin_dir(AppInfo),
  150. [begin
  151. Source = filename:basename(File, ".erl"),
  152. Target = target_base(EbinDir, Source) ++ ".beam",
  153. file:delete(Target)
  154. end || File <- Files].
  155. %%
  156. error_tuple(Module, Es, Ws, AllOpts, Opts) ->
  157. FormattedEs = format_error_sources(Es, AllOpts),
  158. FormattedWs = format_error_sources(Ws, AllOpts),
  159. rebar_compiler:error_tuple(Module, FormattedEs, FormattedWs, Opts).
  160. format_error_sources(Es, Opts) ->
  161. [{rebar_compiler:format_error_source(Src, Opts), Desc}
  162. || {Src, Desc} <- Es].
  163. %% Get files which need to be compiled first, i.e. those specified in erl_first_files
  164. %% and parse_transform options. Also produce specific erl_opts for these first
  165. %% files, so that yet to be compiled parse transformations are excluded from it.
  166. erl_first_files(Opts, ErlOpts, Dir, NeededErlFiles) ->
  167. ErlFirstFilesConf = rebar_opts:get(Opts, erl_first_files, []),
  168. valid_erl_first_conf(ErlFirstFilesConf),
  169. NeededSrcDirs = lists:usort(lists:map(fun filename:dirname/1, NeededErlFiles)),
  170. %% NOTE: order of files here is important!
  171. ErlFirstFiles =
  172. [filename:join(Dir, File) || File <- ErlFirstFilesConf,
  173. lists:member(filename:join(Dir, File), NeededErlFiles)],
  174. {ParseTransforms, ParseTransformsErls} =
  175. lists:unzip(lists:flatmap(
  176. fun(PT) ->
  177. PTerls = [filename:join(D, module_to_erl(PT)) || D <- NeededSrcDirs],
  178. [{PT, PTerl} || PTerl <- PTerls, lists:member(PTerl, NeededErlFiles)]
  179. end, proplists:get_all_values(parse_transform, ErlOpts))),
  180. ErlOptsFirst = lists:filter(fun({parse_transform, PT}) ->
  181. not lists:member(PT, ParseTransforms);
  182. (_) ->
  183. true
  184. end, ErlOpts),
  185. {ErlFirstFiles ++ ParseTransformsErls, ErlOptsFirst}.
  186. split_source_files(SourceFiles, ErlOpts) ->
  187. ParseTransforms = proplists:get_all_values(parse_transform, ErlOpts),
  188. lists:partition(fun(Source) ->
  189. lists:member(filename_to_atom(Source), ParseTransforms)
  190. end, SourceFiles).
  191. filename_to_atom(F) -> list_to_atom(filename:rootname(filename:basename(F))).
  192. %% Get subset of SourceFiles which need to be recompiled, respecting
  193. %% dependencies induced by given graph G.
  194. needed_files(Graph, ErlOpts, RebarOpts, Dir, OutDir, SourceFiles) ->
  195. PrivIncludes = [{i, filename:join(Dir, Src)}
  196. || Src <- rebar_dir:all_src_dirs(RebarOpts, ["src"], [])],
  197. SharedOpts = [{i, filename:join(Dir, "include")},
  198. {i, Dir}] ++ PrivIncludes ++ ErlOpts,
  199. CompilerOptsSet = erl_compiler_opts_set(),
  200. lists:filter(fun(Source) ->
  201. TargetBase = target_base(OutDir, Source),
  202. Target = TargetBase ++ ".beam",
  203. AllOpts = [{outdir, filename:dirname(Target)} | SharedOpts],
  204. digraph:vertex(Graph, Source) > {Source, filelib:last_modified(Target)}
  205. orelse opts_changed(Graph, AllOpts, Target, TargetBase)
  206. orelse CompilerOptsSet
  207. end, SourceFiles).
  208. target_base(OutDir, Source) ->
  209. filename:join(OutDir, filename:basename(Source, ".erl")).
  210. opts_changed(Graph, NewOpts, Target, TargetBase) ->
  211. TotalOpts = case erlang:function_exported(compile, env_compiler_options, 0) of
  212. true -> NewOpts ++ compile:env_compiler_options();
  213. false -> NewOpts
  214. end,
  215. TargetOpts = case digraph:vertex(Graph, Target) of
  216. {_Target, {artifact, Opts}} -> % tracked dep is found
  217. Opts;
  218. false -> % not found; might be a non-tracked DAG
  219. case compile_info(TargetBase) of
  220. {ok, Opts} -> Opts;
  221. _ -> []
  222. end
  223. end,
  224. lists:any(fun effects_code_generation/1,
  225. lists:usort(TotalOpts) -- lists:usort(TargetOpts)).
  226. effects_code_generation(Option) ->
  227. case Option of
  228. beam -> false;
  229. report_warnings -> false;
  230. report_errors -> false;
  231. return_errors-> false;
  232. return_warnings-> false;
  233. report -> false;
  234. warnings_as_errors -> false;
  235. binary -> false;
  236. verbose -> false;
  237. {cwd,_} -> false;
  238. {outdir, _} -> false;
  239. _ -> true
  240. end.
  241. compile_info(Target) ->
  242. case beam_lib:chunks(Target, [compile_info]) of
  243. {ok, {_mod, Chunks}} ->
  244. CompileInfo = proplists:get_value(compile_info, Chunks, []),
  245. {ok, proplists:get_value(options, CompileInfo, [])};
  246. {error, beam_lib, Reason} ->
  247. ?WARN("Couldn't read debug info from ~p for reason: ~p", [Target, Reason]),
  248. {error, Reason}
  249. end.
  250. erl_compiler_opts_set() ->
  251. EnvSet = case os:getenv("ERL_COMPILER_OPTIONS") of
  252. false -> false;
  253. _ -> true
  254. end,
  255. %% return false if changed env opts would have been caught in opts_changed/2
  256. EnvSet andalso not erlang:function_exported(compile, env_compiler_options, 0).
  257. valid_erl_first_conf(FileList) ->
  258. Strs = filter_file_list(FileList),
  259. case rebar_utils:is_list_of_strings(Strs) of
  260. true -> true;
  261. false -> ?ABORT("An invalid file list (~p) was provided as part of your erl_first_files directive",
  262. [FileList])
  263. end.
  264. filter_file_list(FileList) ->
  265. Atoms = lists:filter( fun(X) -> is_atom(X) end, FileList),
  266. case Atoms of
  267. [] ->
  268. FileList;
  269. _ ->
  270. atoms_in_erl_first_files_warning(Atoms),
  271. lists:filter( fun(X) -> not(is_atom(X)) end, FileList)
  272. end.
  273. atoms_in_erl_first_files_warning(Atoms) ->
  274. W = "You have provided atoms as file entries in erl_first_files; "
  275. "erl_first_files only expects lists of filenames as strings. "
  276. "The following modules (~p) may not work as expected and it is advised "
  277. "that you change these entires to string format "
  278. "(e.g., \"src/module.erl\") ",
  279. ?WARN(W, [Atoms]).
  280. module_to_erl(Mod) ->
  281. atom_to_list(Mod) ++ ".erl".
  282. parse_attrs(Fd, Includes, Dir) ->
  283. case io:parse_erl_form(Fd, "") of
  284. {ok, Form, _Line} ->
  285. case erl_syntax:type(Form) of
  286. attribute ->
  287. NewIncludes = process_attr(Form, Includes, Dir),
  288. parse_attrs(Fd, NewIncludes, Dir);
  289. _ ->
  290. parse_attrs(Fd, Includes, Dir)
  291. end;
  292. {eof, _} ->
  293. lists:usort(Includes);
  294. _Err ->
  295. parse_attrs(Fd, Includes, Dir)
  296. end.
  297. process_attr(Form, Includes, Dir) ->
  298. AttrName = erl_syntax:atom_value(erl_syntax:attribute_name(Form)),
  299. process_attr(AttrName, Form, Includes, Dir).
  300. process_attr(import, Form, Includes, _Dir) ->
  301. case erl_syntax_lib:analyze_import_attribute(Form) of
  302. {Mod, _Funs} ->
  303. [module_to_erl(Mod)|Includes];
  304. Mod ->
  305. [module_to_erl(Mod)|Includes]
  306. end;
  307. process_attr(file, Form, Includes, _Dir) ->
  308. {File, _} = erl_syntax_lib:analyze_file_attribute(Form),
  309. [File|Includes];
  310. process_attr(include, Form, Includes, _Dir) ->
  311. [FileNode] = erl_syntax:attribute_arguments(Form),
  312. File = erl_syntax:string_value(FileNode),
  313. [File|Includes];
  314. process_attr(include_lib, Form, Includes, Dir) ->
  315. [FileNode] = erl_syntax:attribute_arguments(Form),
  316. RawFile = erl_syntax:string_value(FileNode),
  317. maybe_expand_include_lib_path(RawFile, Dir) ++ Includes;
  318. process_attr(behavior, Form, Includes, _Dir) ->
  319. process_attr(behaviour, Form, Includes, _Dir);
  320. process_attr(behaviour, Form, Includes, _Dir) ->
  321. [FileNode] = erl_syntax:attribute_arguments(Form),
  322. File = module_to_erl(erl_syntax:atom_value(FileNode)),
  323. [File|Includes];
  324. process_attr(compile, Form, Includes, _Dir) ->
  325. [Arg] = erl_syntax:attribute_arguments(Form),
  326. case erl_syntax:concrete(Arg) of
  327. {parse_transform, Mod} ->
  328. [module_to_erl(Mod)|Includes];
  329. {core_transform, Mod} ->
  330. [module_to_erl(Mod)|Includes];
  331. L when is_list(L) ->
  332. lists:foldl(
  333. fun({parse_transform, Mod}, Acc) ->
  334. [module_to_erl(Mod)|Acc];
  335. ({core_transform, Mod}, Acc) ->
  336. [module_to_erl(Mod)|Acc];
  337. (_, Acc) ->
  338. Acc
  339. end, Includes, L);
  340. _ ->
  341. Includes
  342. end;
  343. process_attr(_, _Form, Includes, _Dir) ->
  344. Includes.
  345. %% NOTE: If, for example, one of the entries in Files, refers to
  346. %% gen_server.erl, that entry will be dropped. It is dropped because
  347. %% such an entry usually refers to the beam file, and we don't pass a
  348. %% list of OTP src dirs for finding gen_server.erl's full path. Also,
  349. %% if gen_server.erl was modified, it's not rebar's task to compile a
  350. %% new version of the beam file. Therefore, it's reasonable to drop
  351. %% such entries. Also see process_attr(behaviour, Form, Includes).
  352. -spec expand_file_names([file:filename()],
  353. [file:filename()]) -> [file:filename()].
  354. expand_file_names(Files, Dirs) ->
  355. %% We check if Files exist by itself or within the directories
  356. %% listed in Dirs.
  357. %% Return the list of files matched.
  358. lists:flatmap(
  359. fun(Incl) ->
  360. case filelib:is_regular(Incl) of
  361. true ->
  362. [Incl];
  363. false ->
  364. Res = rebar_utils:find_files_in_dirs(Dirs, [$^, Incl, $$], true),
  365. case Res of
  366. [] ->
  367. ?DEBUG("FILE ~p NOT FOUND", [Incl]),
  368. [];
  369. _ ->
  370. Res
  371. end
  372. end
  373. end, Files).
  374. %% Given a path like "stdlib/include/erl_compile.hrl", return
  375. %% "OTP_INSTALL_DIR/lib/erlang/lib/stdlib-x.y.z/include/erl_compile.hrl".
  376. %% Usually a simple [Lib, SubDir, File1] = filename:split(File) should
  377. %% work, but to not crash when an unusual include_lib path is used,
  378. %% utilize more elaborate logic.
  379. maybe_expand_include_lib_path(File, Dir) ->
  380. File1 = filename:basename(File),
  381. case filename:split(filename:dirname(File)) of
  382. [_] ->
  383. warn_and_find_path(File, Dir);
  384. [Lib | SubDir] ->
  385. case code:lib_dir(list_to_atom(Lib), list_to_atom(filename:join(SubDir))) of
  386. {error, bad_name} ->
  387. warn_and_find_path(File, Dir);
  388. AppDir ->
  389. [filename:join(AppDir, File1)]
  390. end
  391. end.
  392. %% The use of -include_lib was probably incorrect by the user but lets try to make it work.
  393. %% We search in the outdir and outdir/../include to see if the header exists.
  394. warn_and_find_path(File, Dir) ->
  395. SrcHeader = filename:join(Dir, File),
  396. case filelib:is_regular(SrcHeader) of
  397. true ->
  398. [SrcHeader];
  399. false ->
  400. IncludeDir = filename:join(rebar_utils:droplast(filename:split(Dir))++["include"]),
  401. IncludeHeader = filename:join(IncludeDir, File),
  402. case filelib:is_regular(IncludeHeader) of
  403. true ->
  404. [filename:join(IncludeDir, File)];
  405. false ->
  406. []
  407. end
  408. end.
  409. format_error({cannot_read_file, Source, Reason}) ->
  410. lists:flatten(io_lib:format("Cannot read file '~s': ~s", [Source, Reason]));
  411. format_error(Other) ->
  412. io_lib:format("~p", [Other]).