25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

467 lines
19 KiB

14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
14 년 전
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_templater).
  28. -export([new/3,
  29. list_templates/1,
  30. create/1]).
  31. %% API for other utilities that need templating functionality
  32. -export([resolve_variables/2,
  33. render/2]).
  34. -include("rebar.hrl").
  35. -define(TEMPLATE_RE, "^[^._].*\\.template\$").
  36. -define(ERLYDTL_COMPILE_OPTS, [report_warnings, return_errors, {auto_escape, false}, {out_dir, false}]).
  37. %% ===================================================================
  38. %% Public API
  39. %% ===================================================================
  40. new(app, DirName, State) ->
  41. create1(State, DirName, "otp_app");
  42. new(lib, DirName, State) ->
  43. create1(State, DirName, "otp_lib");
  44. new(plugin, DirName, State) ->
  45. create1(State, DirName, "plugin");
  46. new(rel, DirName, State) ->
  47. create1(State, DirName, "otp_rel").
  48. list_templates(State) ->
  49. {AvailTemplates, Files} = find_templates(State),
  50. ?DEBUG("Available templates: ~p\n", [AvailTemplates]),
  51. lists:foreach(
  52. fun({Type, F}) ->
  53. BaseName = filename:basename(F, ".template"),
  54. TemplateTerms = consult(load_file(Files, Type, F)),
  55. {_, VarList} = lists:keyfind(variables, 1, TemplateTerms),
  56. Vars = lists:foldl(fun({V,_}, Acc) ->
  57. [atom_to_list(V) | Acc]
  58. end, [], VarList),
  59. ?INFO(" * ~s: ~s (~p) (variables: ~p)\n",
  60. [BaseName, F, Type, string:join(Vars, ", ")])
  61. end, AvailTemplates),
  62. ok.
  63. create(State) ->
  64. TemplateId = template_id(State),
  65. create1(State, "", TemplateId).
  66. %%
  67. %% Given a list of key value pairs, for each string value attempt to
  68. %% render it using Dict as the context. Storing the result in Dict as Key.
  69. %%
  70. resolve_variables([], Dict) ->
  71. Dict;
  72. resolve_variables([{Key, Value0} | Rest], Dict) when is_list(Value0) ->
  73. Value = render(list_to_binary(Value0), Dict),
  74. resolve_variables(Rest, dict:store(Key, Value, Dict));
  75. resolve_variables([{Key, {list, Dicts}} | Rest], Dict) when is_list(Dicts) ->
  76. %% just un-tag it so erlydtl can use it
  77. resolve_variables(Rest, dict:store(Key, Dicts, Dict));
  78. resolve_variables([_Pair | Rest], Dict) ->
  79. resolve_variables(Rest, Dict).
  80. %%
  81. %% Render a binary to a string, using erlydtl and the specified context
  82. %%
  83. render(Template, Context) when is_atom(Template) ->
  84. Template:render(Context);
  85. render(Template, Context) ->
  86. Module = list_to_atom(Template++"_dtl"),
  87. Module:render(Context).
  88. %% ===================================================================
  89. %% Internal functions
  90. %% ===================================================================
  91. create1(State, AppDir, TemplateId) ->
  92. ec_file:mkdir_p(AppDir),
  93. file:set_cwd(AppDir),
  94. {AvailTemplates, Files} = find_templates(State),
  95. ?DEBUG("Available templates: ~p\n", [AvailTemplates]),
  96. %% Using the specified template id, find the matching template file/type.
  97. %% Note that if you define the same template in both ~/.rebar/templates
  98. %% that is also present in the escript, the one on the file system will
  99. %% be preferred.
  100. {Type, Template} = select_template(AvailTemplates, TemplateId),
  101. %% Load the template definition as is and get the list of variables the
  102. %% template requires.
  103. Context0 = dict:from_list([{appid, AppDir}]),
  104. TemplateTerms = consult(load_file(Files, Type, Template)),
  105. case lists:keyfind(variables, 1, TemplateTerms) of
  106. {variables, Vars} ->
  107. case parse_vars(Vars, Context0) of
  108. {error, Entry} ->
  109. Context1 = undefined,
  110. ?ABORT("Failed while processing variables from template ~p."
  111. "Variable definitions must follow form of "
  112. "[{atom(), term()}]. Failed at: ~p\n",
  113. [TemplateId, Entry]);
  114. Context1 ->
  115. ok
  116. end;
  117. false ->
  118. ?WARN("No variables section found in template ~p; "
  119. "using empty context.\n", [TemplateId]),
  120. Context1 = Context0
  121. end,
  122. %% Load variables from disk file, if provided
  123. Context2 = case rebar_state:get(State, template_vars, undefined) of
  124. undefined ->
  125. Context1;
  126. File ->
  127. case consult(load_file([], file, File)) of
  128. {error, Reason} ->
  129. ?ABORT("Unable to load template_vars from ~s: ~p\n",
  130. [File, Reason]);
  131. Terms ->
  132. %% TODO: Cleanup/merge with similar code in rebar_reltool
  133. M = fun(_Key, _Base, Override) -> Override end,
  134. dict:merge(M, Context1, dict:from_list(Terms))
  135. end
  136. end,
  137. %% For each variable, see if it's defined in global vars -- if it is,
  138. %% prefer that value over the defaults
  139. Context3 = update_vars(State, dict:fetch_keys(Context2), Context1),
  140. ?DEBUG("Template ~p context: ~p\n", [TemplateId, dict:to_list(Context2)]),
  141. %% Handle variables that possibly include other variables in their
  142. %% definition
  143. %Context = resolve_variables(dict:to_list(Context3), Context3),
  144. %?DEBUG("Resolved Template ~p context: ~p\n",
  145. %[TemplateId, dict:to_list(Context)]),
  146. %% Now, use our context to process the template definition -- this
  147. %% permits us to use variables within the definition for filenames.
  148. %FinalTemplate = consult(render(load_file(Files, Type, Template), Context)),
  149. %?DEBUG("Final template def ~p: ~p\n", [TemplateId, FinalTemplate]),
  150. %% Execute the instructions in the finalized template
  151. Force = rebar_state:get(State, force, "0"),
  152. execute_template([], TemplateTerms, Type, TemplateId, Context3, Force, []).
  153. find_templates(State) ->
  154. %% Load a list of all the files in the escript -- cache them since
  155. %% we'll potentially need to walk it several times over the course of
  156. %% a run.
  157. Files = cache_escript_files(State),
  158. %% Build a list of available templates
  159. AvailTemplates = find_disk_templates(State)
  160. ++ find_escript_templates(Files),
  161. {AvailTemplates, Files}.
  162. %%
  163. %% Scan the current escript for available files
  164. %%
  165. cache_escript_files(State) ->
  166. {ok, Files} = rebar_utils:escript_foldl(
  167. fun(Name, _, GetBin, Acc) ->
  168. [{Name, GetBin()} | Acc]
  169. end,
  170. [], rebar_state:get(State, escript)),
  171. Files.
  172. template_id(State) ->
  173. case rebar_state:get(State, template, undefined) of
  174. undefined ->
  175. ?ABORT("No template specified.\n", []);
  176. TemplateId ->
  177. TemplateId
  178. end.
  179. find_escript_templates(Files) ->
  180. [{escript, Name}
  181. || {Name, _Bin} <- Files,
  182. re:run(Name, ?TEMPLATE_RE, [{capture, none}]) == match].
  183. find_disk_templates(State) ->
  184. OtherTemplates = find_other_templates(State),
  185. HomeFiles = rebar_utils:find_files(filename:join([os:getenv("HOME"),
  186. ".rebar", "templates"]),
  187. ?TEMPLATE_RE),
  188. LocalFiles = rebar_utils:find_files(".", ?TEMPLATE_RE, true),
  189. [{file, F} || F <- OtherTemplates ++ HomeFiles ++ LocalFiles].
  190. find_other_templates(State) ->
  191. case rebar_state:get(State, template_dir, undefined) of
  192. undefined ->
  193. [];
  194. TemplateDir ->
  195. rebar_utils:find_files(TemplateDir, ?TEMPLATE_RE)
  196. end.
  197. select_template([], Template) ->
  198. ?ABORT("Template ~s not found.\n", [Template]);
  199. select_template([{Type, Avail} | Rest], Template) ->
  200. case filename:basename(Avail, ".template") == Template of
  201. true ->
  202. {Type, Avail};
  203. false ->
  204. select_template(Rest, Template)
  205. end.
  206. %%
  207. %% Read the contents of a file from the appropriate source
  208. %%
  209. load_file(Files, escript, Name) ->
  210. {Name, Bin} = lists:keyfind(Name, 1, Files),
  211. Bin;
  212. load_file(_Files, file, Name) ->
  213. {ok, Bin} = file:read_file(Name),
  214. Bin.
  215. %%
  216. %% Parse/validate variables out from the template definition
  217. %%
  218. parse_vars([], Dict) ->
  219. Dict;
  220. parse_vars([{Key, Value} | Rest], Dict) when is_atom(Key) ->
  221. parse_vars(Rest, dict:store(Key, Value, Dict));
  222. parse_vars([Other | _Rest], _Dict) ->
  223. {error, Other};
  224. parse_vars(Other, _Dict) ->
  225. {error, Other}.
  226. %%
  227. %% Given a list of keys in Dict, see if there is a corresponding value defined
  228. %% in the global config; if there is, update the key in Dict with it
  229. %%
  230. update_vars(_State, [], Dict) ->
  231. Dict;
  232. update_vars(State, [Key | Rest], Dict) ->
  233. Value = rebar_state:get(State, Key, dict:fetch(Key, Dict)),
  234. update_vars(State, Rest, dict:store(Key, Value, Dict)).
  235. %%
  236. %% Given a string or binary, parse it into a list of terms, ala file:consult/1
  237. %%
  238. consult(Str) when is_list(Str) ->
  239. consult([], Str, []);
  240. consult(Bin) when is_binary(Bin)->
  241. consult([], binary_to_list(Bin), []).
  242. consult(Cont, Str, Acc) ->
  243. case erl_scan:tokens(Cont, Str, 0) of
  244. {done, Result, Remaining} ->
  245. case Result of
  246. {ok, Tokens, _} ->
  247. {ok, Term} = erl_parse:parse_term(Tokens),
  248. consult([], Remaining, [maybe_dict(Term) | Acc]);
  249. {eof, _Other} ->
  250. lists:reverse(Acc);
  251. {error, Info, _} ->
  252. {error, Info}
  253. end;
  254. {more, Cont1} ->
  255. consult(Cont1, eof, Acc)
  256. end.
  257. maybe_dict({Key, {list, Dicts}}) ->
  258. %% this is a 'list' element; a list of lists representing dicts
  259. {Key, {list, [dict:from_list(D) || D <- Dicts]}};
  260. maybe_dict(Term) ->
  261. Term.
  262. write_file(Output, Data, Force) ->
  263. %% determine if the target file already exists
  264. FileExists = filelib:is_regular(Output),
  265. %% perform the function if we're allowed,
  266. %% otherwise just process the next template
  267. case Force =:= "1" orelse FileExists =:= false of
  268. true ->
  269. ok = filelib:ensure_dir(Output),
  270. case {Force, FileExists} of
  271. {"1", true} ->
  272. ?INFO("Writing ~s (forcibly overwriting)~n",
  273. [Output]);
  274. _ ->
  275. ?INFO("Writing ~s~n", [Output])
  276. end,
  277. case file:write_file(Output, Data) of
  278. ok ->
  279. ok;
  280. {error, Reason} ->
  281. ?ABORT("Failed to write output file ~p: ~p\n",
  282. [Output, Reason])
  283. end;
  284. false ->
  285. {error, exists}
  286. end.
  287. prepend_instructions(Instructions, Rest) when is_list(Instructions) ->
  288. Instructions ++ Rest;
  289. prepend_instructions(Instruction, Rest) ->
  290. [Instruction|Rest].
  291. %%
  292. %% Execute each instruction in a template definition file.
  293. %%
  294. execute_template(_Files, [], _TemplateType, _TemplateName,
  295. _Context, _Force, ExistingFiles) ->
  296. case ExistingFiles of
  297. [] ->
  298. ok;
  299. _ ->
  300. Msg = lists:flatten([io_lib:format("\t* ~p~n", [F]) ||
  301. F <- lists:reverse(ExistingFiles)]),
  302. Help = "To force overwriting, specify -f/--force/force=1"
  303. " on the command line.\n",
  304. ?ERROR("One or more files already exist on disk and "
  305. "were not generated:~n~s~s", [Msg , Help])
  306. end;
  307. execute_template(Files, [{'if', Cond, True} | Rest], TemplateType,
  308. TemplateName, Context, Force, ExistingFiles) ->
  309. execute_template(Files, [{'if', Cond, True, []}|Rest], TemplateType,
  310. TemplateName, Context, Force, ExistingFiles);
  311. execute_template(Files, [{'if', Cond, True, False} | Rest], TemplateType,
  312. TemplateName, Context, Force, ExistingFiles) ->
  313. Instructions = case dict:find(Cond, Context) of
  314. {ok, true} ->
  315. True;
  316. {ok, "true"} ->
  317. True;
  318. _ ->
  319. False
  320. end,
  321. execute_template(Files, prepend_instructions(Instructions, Rest),
  322. TemplateType, TemplateName, Context, Force,
  323. ExistingFiles);
  324. execute_template(Files, [{'case', Variable, Values, Instructions} | Rest], TemplateType,
  325. TemplateName, Context, Force, ExistingFiles) ->
  326. {ok, Value} = dict:find(Variable, Context),
  327. Instructions2 = case lists:member(Value, Values) of
  328. true ->
  329. Instructions;
  330. _ ->
  331. []
  332. end,
  333. execute_template(Files, prepend_instructions(Instructions2, Rest),
  334. TemplateType, TemplateName, Context, Force,
  335. ExistingFiles);
  336. execute_template(Files, [{template, Input, Output} | Rest], TemplateType,
  337. TemplateName, Context, Force, ExistingFiles) ->
  338. _InputName = filename:join(filename:dirname(TemplateName), Input),
  339. %File = load_file(Files, TemplateType, InputName),
  340. OutputTemplateName = make_template_name("rebar_output_template", Output),
  341. {ok, OutputTemplateName1} = erlydtl:compile_template(Output, OutputTemplateName, ?ERLYDTL_COMPILE_OPTS),
  342. {ok, OutputRendered} = OutputTemplateName1:render(dict:to_list(Context)),
  343. {ok, Rendered} = render(Input, dict:to_list(Context)),
  344. case write_file(lists:flatten(io_lib:format("~s", [OutputRendered])), Rendered, Force) of
  345. ok ->
  346. execute_template(Files, Rest, TemplateType, TemplateName,
  347. Context, Force, ExistingFiles);
  348. {error, exists} ->
  349. execute_template(Files, Rest, TemplateType, TemplateName,
  350. Context, Force, [Output|ExistingFiles])
  351. end;
  352. execute_template(Files, [{file, Input, Output} | Rest], TemplateType,
  353. TemplateName, Context, Force, ExistingFiles) ->
  354. InputName = filename:join(filename:dirname(TemplateName), Input),
  355. File = load_file(Files, TemplateType, InputName),
  356. case write_file(Output, File, Force) of
  357. ok ->
  358. execute_template(Files, Rest, TemplateType, TemplateName,
  359. Context, Force, ExistingFiles);
  360. {error, exists} ->
  361. execute_template(Files, Rest, TemplateType, TemplateName,
  362. Context, Force, [Output|ExistingFiles])
  363. end;
  364. execute_template(Files, [{dir, Name} | Rest], TemplateType,
  365. TemplateName, Context, Force, ExistingFiles) ->
  366. case filelib:ensure_dir(filename:join(Name, "dummy")) of
  367. ok ->
  368. execute_template(Files, Rest, TemplateType, TemplateName,
  369. Context, Force, ExistingFiles);
  370. {error, Reason} ->
  371. ?ABORT("Failed while processing template instruction "
  372. "{dir, ~s}: ~p\n", [Name, Reason])
  373. end;
  374. execute_template(Files, [{copy, Input, Output} | Rest], TemplateType,
  375. TemplateName, Context, Force, ExistingFiles) ->
  376. InputName = filename:join(filename:dirname(TemplateName), Input),
  377. try rebar_file_utils:cp_r([InputName ++ "/*"], Output) of
  378. ok ->
  379. execute_template(Files, Rest, TemplateType, TemplateName,
  380. Context, Force, ExistingFiles)
  381. catch _:_ ->
  382. ?ABORT("Failed while processing template instruction "
  383. "{copy, ~s, ~s}~n", [Input, Output])
  384. end;
  385. execute_template(Files, [{chmod, Mod, File} | Rest], TemplateType,
  386. TemplateName, Context, Force, ExistingFiles)
  387. when is_integer(Mod) ->
  388. case file:change_mode(File, Mod) of
  389. ok ->
  390. execute_template(Files, Rest, TemplateType, TemplateName,
  391. Context, Force, ExistingFiles);
  392. {error, Reason} ->
  393. ?ABORT("Failed while processing template instruction "
  394. "{chmod, ~b, ~s}: ~p~n", [Mod, File, Reason])
  395. end;
  396. execute_template(Files, [{symlink, Existing, New} | Rest], TemplateType,
  397. TemplateName, Context, Force, ExistingFiles) ->
  398. case file:make_symlink(Existing, New) of
  399. ok ->
  400. execute_template(Files, Rest, TemplateType, TemplateName,
  401. Context, Force, ExistingFiles);
  402. {error, Reason} ->
  403. ?ABORT("Failed while processing template instruction "
  404. "{symlink, ~s, ~s}: ~p~n", [Existing, New, Reason])
  405. end;
  406. execute_template(Files, [{variables, _} | Rest], TemplateType,
  407. TemplateName, Context, Force, ExistingFiles) ->
  408. execute_template(Files, Rest, TemplateType, TemplateName,
  409. Context, Force, ExistingFiles);
  410. execute_template(Files, [Other | Rest], TemplateType, TemplateName,
  411. Context, Force, ExistingFiles) ->
  412. ?WARN("Skipping unknown template instruction: ~p\n", [Other]),
  413. execute_template(Files, Rest, TemplateType, TemplateName, Context,
  414. Force, ExistingFiles).
  415. -spec make_template_name(string(), term()) -> module().
  416. make_template_name(Base, Value) ->
  417. %% Seed so we get different values each time
  418. random:seed(erlang:now()),
  419. Hash = erlang:phash2(Value),
  420. Ran = random:uniform(10000000),
  421. erlang:list_to_atom(Base ++ "_" ++
  422. erlang:integer_to_list(Hash) ++
  423. "_" ++ erlang:integer_to_list(Ran)).