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.

462 lignes
18 KiB

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