You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

453 lines
18 KiB

пре 15 година
пре 15 година
пре 15 година
пре 15 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 14 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 13 година
пре 14 година
пре 14 година
пре 14 година
пре 14 година
пре 10 година
  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/4,
  29. list_templates/1,
  30. render/2]).
  31. -include("rebar.hrl").
  32. -define(TEMPLATE_RE, "^(?!\\._).*\\.template\$").
  33. %% ===================================================================
  34. %% Public API
  35. %% ===================================================================
  36. %% Apply a template
  37. new(Template, Vars, Force, State) ->
  38. {AvailTemplates, Files} = find_templates(State),
  39. ?DEBUG("Looking for ~p", [Template]),
  40. case lists:keyfind(Template, 1, AvailTemplates) of
  41. false -> {not_found, Template};
  42. TemplateTup -> create(TemplateTup, Files, Vars, Force, State)
  43. end.
  44. %% Give a list of templates with their expanded content
  45. list_templates(State) ->
  46. {AvailTemplates, Files} = find_templates(State),
  47. [list_template(Files, Template, State) || Template <- AvailTemplates].
  48. %% ===================================================================
  49. %% Internal Functions
  50. %% ===================================================================
  51. %% Expand a single template's value
  52. list_template(Files, {Name, Type, File}, State) ->
  53. case consult(load_file(Files, Type, File)) of
  54. {error, Reason} ->
  55. {error, {consult, File, Reason}};
  56. TemplateTerms ->
  57. {Name, Type, File,
  58. get_template_description(TemplateTerms),
  59. get_template_vars(TemplateTerms, State)}
  60. end.
  61. %% Load up the template description out from a list of attributes read in
  62. %% a .template file.
  63. get_template_description(TemplateTerms) ->
  64. case lists:keyfind(description, 1, TemplateTerms) of
  65. {_, Desc} -> Desc;
  66. false -> undefined
  67. end.
  68. %% Load up the variables out from a list of attributes read in a .template file
  69. %% and return them merged with the globally-defined and default variables.
  70. get_template_vars(TemplateTerms, State) ->
  71. Vars = case lists:keyfind(variables, 1, TemplateTerms) of
  72. {_, Value} -> Value;
  73. false -> []
  74. end,
  75. override_vars(Vars, override_vars(global_variables(State), default_variables())).
  76. %% Provide a way to merge a set of variables with another one. The left-hand
  77. %% set of variables takes precedence over the right-hand set.
  78. %% In the case where left-hand variable description contains overriden defaults, but
  79. %% the right-hand one contains additional data such as documentation, the resulting
  80. %% variable description will contain the widest set of information possible.
  81. override_vars([], General) -> General;
  82. override_vars([{Var, Default} | Rest], General) ->
  83. case lists:keytake(Var, 1, General) of
  84. {value, {Var, _Default, Doc}, NewGeneral} ->
  85. [{Var, Default, Doc} | override_vars(Rest, NewGeneral)];
  86. {value, {Var, _Default}, NewGeneral} ->
  87. [{Var, Default} | override_vars(Rest, NewGeneral)];
  88. false ->
  89. [{Var, Default} | override_vars(Rest, General)]
  90. end;
  91. override_vars([{Var, Default, Doc} | Rest], General) ->
  92. [{Var, Default, Doc} | override_vars(Rest, lists:keydelete(Var, 1, General))].
  93. %% Default variables, generated dynamically.
  94. default_variables() ->
  95. {DefaultAuthor, DefaultEmail} = default_author_and_email(),
  96. {{Y,M,D},{H,Min,S}} = calendar:universal_time(),
  97. [{date, lists:flatten(io_lib:format("~4..0w-~2..0w-~2..0w",[Y,M,D]))},
  98. {datetime, lists:flatten(io_lib:format("~4..0w-~2..0w-~2..0wT~2..0w:~2..0w:~2..0w+00:00",[Y,M,D,H,Min,S]))},
  99. {author_name, DefaultAuthor},
  100. {author_email, DefaultEmail},
  101. {copyright_year, integer_to_list(Y)},
  102. {apps_dir, "apps", "Directory where applications will be created if needed"}].
  103. default_author_and_email() ->
  104. %% See if we can get a git user and email to use as defaults
  105. case rebar_utils:sh("git config --global user.name", [return_on_error]) of
  106. {ok, Name} ->
  107. case rebar_utils:sh("git config --global user.email", [return_on_error]) of
  108. {ok, Email} ->
  109. {rebar_string:trim(Name, both, "\n"),
  110. rebar_string:trim(Email, both, "\n")};
  111. {error, _} ->
  112. %% Use neither if one doesn't exist
  113. {"Anonymous", "anonymous@example.org"}
  114. end;
  115. {error, _} ->
  116. %% Ok, try mecurial
  117. case rebar_utils:sh("hg showconfig ui.username", [return_on_error]) of
  118. {ok, NameEmail} ->
  119. case re:run(NameEmail, "^(.*) <(.*)>$", [{capture, [1,2], list}, unicode]) of
  120. {match, [Name, Email]} ->
  121. {Name, Email};
  122. _ ->
  123. {"Anonymous", "anonymous@example.org"}
  124. end;
  125. {error, _} ->
  126. {"Anonymous", "anonymous@example.org"}
  127. end
  128. end.
  129. %% Load variable definitions from the 'Globals' file in the home template
  130. %% directory
  131. global_variables(State) ->
  132. GlobalFile = rebar_dir:template_globals(State),
  133. case file:consult(GlobalFile) of
  134. {error, enoent} -> [];
  135. {ok, Data} -> proplists:get_value(variables, Data, [])
  136. end.
  137. %% drop the documentation for variables when present
  138. drop_var_docs([]) -> [];
  139. drop_var_docs([{K,V,_}|Rest]) -> [{K,V} | drop_var_docs(Rest)];
  140. drop_var_docs([{K,V}|Rest]) -> [{K,V} | drop_var_docs(Rest)].
  141. %% Load the template index, resolve all variables, and then execute
  142. %% the template.
  143. create({Template, Type, File}, Files, UserVars, Force, State) ->
  144. TemplateTerms = consult(load_file(Files, Type, File)),
  145. Vars = drop_var_docs(override_vars(UserVars, get_template_vars(TemplateTerms, State))),
  146. maybe_warn_about_name(Vars),
  147. TemplateCwd = filename:dirname(File),
  148. execute_template(TemplateTerms, Files, {Template, Type, TemplateCwd}, Vars, Force).
  149. maybe_warn_about_name(Vars) ->
  150. Name = proplists:get_value(name, Vars, "valid"),
  151. case validate_atom(Name) of
  152. invalid ->
  153. ?WARN("The 'name' variable is often associated with Erlang "
  154. "module names and/or file names. The value submitted "
  155. "(~ts) isn't an unquoted Erlang atom. Templates "
  156. "generated may contain errors.",
  157. [Name]);
  158. valid ->
  159. ok
  160. end.
  161. validate_atom(Str) ->
  162. case io_lib:fread("~a", unicode:characters_to_list(Str)) of
  163. {ok, [Atom], ""} ->
  164. case io_lib:write_atom(Atom) of
  165. "'" ++ _ -> invalid; % quoted
  166. _ -> valid % unquoted
  167. end;
  168. _ ->
  169. invalid
  170. end.
  171. %% Run template instructions one at a time.
  172. execute_template([], _, {Template,_,_}, _, _) ->
  173. ?DEBUG("Template ~ts applied", [Template]),
  174. ok;
  175. %% We can't execute the description
  176. execute_template([{description, _} | Terms], Files, Template, Vars, Force) ->
  177. execute_template(Terms, Files, Template, Vars, Force);
  178. %% We can't execute variables
  179. execute_template([{variables, _} | Terms], Files, Template, Vars, Force) ->
  180. execute_template(Terms, Files, Template, Vars, Force);
  181. %% Create a directory
  182. execute_template([{dir, Path} | Terms], Files, Template, Vars, Force) ->
  183. ?DEBUG("Creating directory ~p", [Path]),
  184. case ec_file:mkdir_p(expand_path(Path, Vars)) of
  185. ok ->
  186. ok;
  187. {error, Reason} ->
  188. ?ABORT("Failed while processing template instruction "
  189. "{dir, ~p}: ~p", [Path, Reason])
  190. end,
  191. execute_template(Terms, Files, Template, Vars, Force);
  192. %% Change permissions on a file
  193. execute_template([{chmod, File, Perm} | Terms], Files, Template, Vars, Force) ->
  194. Path = expand_path(File, Vars),
  195. case file:change_mode(Path, Perm) of
  196. ok ->
  197. execute_template(Terms, Files, Template, Vars, Force);
  198. {error, Reason} ->
  199. ?ABORT("Failed while processing template instruction "
  200. "{chmod, ~.8#, ~p}: ~p", [Perm, File, Reason])
  201. end;
  202. %% Create a raw untemplated file
  203. execute_template([{file, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) ->
  204. ?DEBUG("Creating file ~p", [To]),
  205. Data = load_file(Files, Type, filename:join(Cwd, From)),
  206. Out = expand_path(To,Vars),
  207. case write_file(Out, Data, Force) of
  208. ok -> ok;
  209. {error, exists} -> ?INFO("File ~p already exists.", [Out])
  210. end,
  211. execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force);
  212. %% Operate on a django template
  213. execute_template([{template, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) ->
  214. ?DEBUG("Executing template file ~p", [From]),
  215. Out = expand_path(To, Vars),
  216. Tpl = load_file(Files, Type, filename:join(Cwd, From)),
  217. case write_file(Out, render(Tpl, Vars), Force) of
  218. ok ->
  219. ok;
  220. {error, exists} ->
  221. ?INFO("File ~p already exists", [Out])
  222. end,
  223. execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force);
  224. %% Unknown
  225. execute_template([Instruction|Terms], Files, Tpl={Template,_,_}, Vars, Force) ->
  226. ?WARN("Unknown template instruction ~p in template ~ts",
  227. [Instruction, Template]),
  228. execute_template(Terms, Files, Tpl, Vars, Force).
  229. %% Workaround to allow variable substitution in path names without going
  230. %% through the ErlyDTL compilation step. Parse the string and replace
  231. %% as we go.
  232. expand_path([], _) -> [];
  233. expand_path("{{"++Rest, Vars) -> replace_var(Rest, [], Vars);
  234. expand_path([H|T], Vars) -> [H | expand_path(T, Vars)].
  235. %% Actual variable replacement.
  236. replace_var("}}"++Rest, Acc, Vars) ->
  237. Var = lists:reverse(Acc),
  238. Val = proplists:get_value(list_to_atom(Var), Vars, ""),
  239. Val ++ expand_path(Rest, Vars);
  240. replace_var([H|T], Acc, Vars) ->
  241. replace_var(T, [H|Acc], Vars).
  242. %% Load a list of all the files in the escript and on disk
  243. find_templates(State) ->
  244. DiskTemplates = find_disk_templates(State),
  245. PluginTemplates = find_plugin_templates(State),
  246. {MainTemplates, Files} =
  247. case rebar_state:escript_path(State) of
  248. undefined -> % running in local install
  249. {find_localinstall_templates(State), []};
  250. _ ->
  251. %% Cache the files since we'll potentially need to walk it several times
  252. %% over the course of a run.
  253. F = cache_escript_files(State),
  254. {find_escript_templates(F), F}
  255. end,
  256. AvailTemplates = find_available_templates([MainTemplates,
  257. PluginTemplates,
  258. DiskTemplates]),
  259. ?DEBUG("Available templates: ~p\n", [AvailTemplates]),
  260. {AvailTemplates, Files}.
  261. find_available_templates(TemplateListList) ->
  262. AvailTemplates = prioritize_templates(TemplateListList),
  263. ?DEBUG("Available templates: ~p\n", [AvailTemplates]),
  264. AvailTemplates.
  265. prioritize_templates([TemplateList]) ->
  266. tag_names(TemplateList);
  267. prioritize_templates([TemplateList | TemplateListList]) ->
  268. prioritize_templates(tag_names(TemplateList),
  269. prioritize_templates(TemplateListList)).
  270. %% Scan the current escript for available files
  271. cache_escript_files(State) ->
  272. {ok, Files} = rebar_utils:escript_foldl(
  273. fun(Name, _, GetBin, Acc) ->
  274. [{Name, GetBin()} | Acc]
  275. end,
  276. [], rebar_state:escript_path(State)),
  277. Files.
  278. %% Find all the template indexes hiding in the rebar3 escript.
  279. find_escript_templates(Files) ->
  280. [{escript, Name}
  281. || {Name, _Bin} <- Files,
  282. re:run(Name, ?TEMPLATE_RE, [{capture, none}, unicode]) == match].
  283. find_localinstall_templates(_State) ->
  284. Templates = rebar_utils:find_files(code:priv_dir(rebar), ?TEMPLATE_RE),
  285. %% Pretend we're still running escripts; should work transparently.
  286. [{builtin, F} || F <- Templates].
  287. %% Fetch template indexes that sit on disk in the user's HOME
  288. find_disk_templates(State) ->
  289. OtherTemplates = find_other_templates(State),
  290. HomeFiles = rebar_utils:find_files(rebar_dir:template_dir(State),
  291. ?TEMPLATE_RE, true), % recursive
  292. [{file, F} || F <- OtherTemplates ++ HomeFiles].
  293. %% Fetch template indexes that sit on disk in custom areas
  294. find_other_templates(State) ->
  295. case rebar_state:get(State, template_dir, undefined) of
  296. undefined ->
  297. [];
  298. TemplateDir ->
  299. rebar_utils:find_files(TemplateDir, ?TEMPLATE_RE, true) % recursive
  300. end.
  301. %% Fetch template indexes that sit on disk in plugins
  302. find_plugin_templates(State) ->
  303. [{plugin, File}
  304. || App <- rebar_state:all_plugin_deps(State),
  305. Priv <- [rebar_app_info:priv_dir(App)],
  306. Priv =/= undefined,
  307. File <- rebar_utils:find_files(Priv, ?TEMPLATE_RE)]
  308. ++ %% and add global plugins too
  309. [{plugin, File}
  310. || PSource <- rebar_state:get(State, {plugins, global}, []),
  311. Plugin <- [plugin_provider(PSource)],
  312. is_atom(Plugin),
  313. Priv <- [code:priv_dir(Plugin)],
  314. Priv =/= undefined,
  315. File <- rebar_utils:find_files(Priv, ?TEMPLATE_RE)].
  316. plugin_provider(P) when is_atom(P) -> P;
  317. plugin_provider(T) when is_tuple(T) -> element(1, T).
  318. %% Take an existing list of templates and tag them by name the way
  319. %% the user would enter it from the CLI
  320. tag_names(List) ->
  321. [{filename:basename(File, ".template"), Type, File}
  322. || {Type, File} <- List].
  323. %% If multiple templates share the same name, those in the escript (built-in)
  324. %% take precedence. Otherwise, the on-disk order is the one to win.
  325. prioritize_templates([], Acc) -> Acc;
  326. prioritize_templates([{Name, Type, File} | Rest], Valid) ->
  327. case lists:keyfind(Name, 1, Valid) of
  328. false ->
  329. prioritize_templates(Rest, [{Name, Type, File} | Valid]);
  330. {_, escript, _} ->
  331. ?DEBUG("Skipping template ~p, due to presence of a built-in "
  332. "template with the same name", [Name]),
  333. prioritize_templates(Rest, Valid);
  334. {_, builtin, _} ->
  335. ?DEBUG("Skipping template ~p, due to presence of a built-in "
  336. "template with the same name", [Name]),
  337. prioritize_templates(Rest, Valid);
  338. {_, plugin, _} ->
  339. ?DEBUG("Skipping template ~p, due to presence of a plugin "
  340. "template with the same name", [Name]),
  341. prioritize_templates(Rest, Valid);
  342. {_, file, _} ->
  343. ?DEBUG("Skipping template ~p, due to presence of a custom "
  344. "template at ~ts", [Name, File]),
  345. prioritize_templates(Rest, Valid)
  346. end.
  347. %% Read the contents of a file from the appropriate source
  348. load_file(Files, escript, Name) ->
  349. {Name, Bin} = lists:keyfind(Name, 1, Files),
  350. Bin;
  351. load_file(_Files, builtin, Name) ->
  352. {ok, Bin} = file:read_file(Name),
  353. Bin;
  354. load_file(_Files, plugin, Name) ->
  355. {ok, Bin} = file:read_file(Name),
  356. Bin;
  357. load_file(_Files, file, Name) ->
  358. {ok, Bin} = file:read_file(Name),
  359. Bin.
  360. %% Given a string or binary, parse it into a list of terms, ala file:consult/1
  361. consult(Str) when is_list(Str) ->
  362. consult([], Str, []);
  363. consult(Bin) when is_binary(Bin)->
  364. consult([], binary_to_list(Bin), []).
  365. consult(Cont, Str, Acc) ->
  366. case erl_scan:tokens(Cont, Str, 0) of
  367. {done, Result, Remaining} ->
  368. case Result of
  369. {ok, Tokens, _} ->
  370. case erl_parse:parse_term(Tokens) of
  371. {ok, Term} -> consult([], Remaining, [Term | Acc]);
  372. {error, Reason} -> {error, Reason}
  373. end;
  374. {eof, _Other} ->
  375. lists:reverse(Acc);
  376. {error, Info, _} ->
  377. {error, Info}
  378. end;
  379. {more, Cont1} ->
  380. consult(Cont1, eof, Acc)
  381. end.
  382. write_file(Output, Data, Force) ->
  383. %% determine if the target file already exists
  384. FileExists = filelib:is_regular(Output),
  385. %% perform the function if we're allowed,
  386. %% otherwise just process the next template
  387. case Force orelse FileExists =:= false of
  388. true ->
  389. ok = filelib:ensure_dir(Output),
  390. case {Force, FileExists} of
  391. {true, true} ->
  392. ?INFO("Writing ~ts (forcibly overwriting)",
  393. [Output]);
  394. _ ->
  395. ?INFO("Writing ~ts", [Output])
  396. end,
  397. case file:write_file(Output, Data) of
  398. ok ->
  399. ok;
  400. {error, Reason} ->
  401. ?ABORT("Failed to write output file ~p: ~p\n",
  402. [Output, Reason])
  403. end;
  404. false ->
  405. {error, exists}
  406. end.
  407. %%
  408. %% Render a binary to a string, using mustache and the specified context
  409. %%
  410. render(Bin, Context) ->
  411. bbmustache:render(rebar_utils:to_binary(Bin), Context, [{key_type, atom}]).