Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

435 рядки
17 KiB

10 роки тому
10 роки тому
10 роки тому
14 роки тому
10 роки тому
10 роки тому
13 роки тому
14 роки тому
14 роки тому
14 роки тому
10 роки тому
10 роки тому
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. {string:strip(Name, both, $\n), string:strip(Email, both, $\n)};
  110. {error, _} ->
  111. %% Use neither if one doesn't exist
  112. {"Anonymous", "anonymous@example.org"}
  113. end;
  114. {error, _} ->
  115. %% Ok, try mecurial
  116. case rebar_utils:sh("hg showconfig ui.username", [return_on_error]) of
  117. {ok, NameEmail} ->
  118. case re:run(NameEmail, "^(.*) <(.*)>$", [{capture, [1,2], list}]) of
  119. {match, [Name, Email]} ->
  120. {Name, Email};
  121. _ ->
  122. {"Anonymous", "anonymous@example.org"}
  123. end;
  124. {error, _} ->
  125. {"Anonymous", "anonymous@example.org"}
  126. end
  127. end.
  128. %% Load variable definitions from the 'Globals' file in the home template
  129. %% directory
  130. global_variables(State) ->
  131. GlobalFile = rebar_dir:template_globals(State),
  132. case file:consult(GlobalFile) of
  133. {error, enoent} -> [];
  134. {ok, Data} -> proplists:get_value(variables, Data, [])
  135. end.
  136. %% drop the documentation for variables when present
  137. drop_var_docs([]) -> [];
  138. drop_var_docs([{K,V,_}|Rest]) -> [{K,V} | drop_var_docs(Rest)];
  139. drop_var_docs([{K,V}|Rest]) -> [{K,V} | drop_var_docs(Rest)].
  140. %% Load the template index, resolve all variables, and then execute
  141. %% the template.
  142. create({Template, Type, File}, Files, UserVars, Force, State) ->
  143. TemplateTerms = consult(load_file(Files, Type, File)),
  144. Vars = drop_var_docs(override_vars(UserVars, get_template_vars(TemplateTerms, State))),
  145. maybe_warn_about_name(Vars),
  146. TemplateCwd = filename:dirname(File),
  147. execute_template(TemplateTerms, Files, {Template, Type, TemplateCwd}, Vars, Force).
  148. maybe_warn_about_name(Vars) ->
  149. Name = proplists:get_value(name, Vars, "valid"),
  150. case validate_atom(Name) of
  151. invalid ->
  152. ?WARN("The 'name' variable is often associated with Erlang "
  153. "module names and/or file names. The value submitted "
  154. "(~s) isn't an unquoted Erlang atom. Templates "
  155. "generated may contain errors.",
  156. [Name]);
  157. valid ->
  158. ok
  159. end.
  160. validate_atom(Str) ->
  161. case io_lib:fread("~a", unicode:characters_to_list(Str)) of
  162. {ok, [Atom], ""} ->
  163. case io_lib:write_atom(Atom) of
  164. "'" ++ _ -> invalid; % quoted
  165. _ -> valid % unquoted
  166. end;
  167. _ ->
  168. invalid
  169. end.
  170. %% Run template instructions one at a time.
  171. execute_template([], _, {Template,_,_}, _, _) ->
  172. ?DEBUG("Template ~s applied", [Template]),
  173. ok;
  174. %% We can't execute the description
  175. execute_template([{description, _} | Terms], Files, Template, Vars, Force) ->
  176. execute_template(Terms, Files, Template, Vars, Force);
  177. %% We can't execute variables
  178. execute_template([{variables, _} | Terms], Files, Template, Vars, Force) ->
  179. execute_template(Terms, Files, Template, Vars, Force);
  180. %% Create a directory
  181. execute_template([{dir, Path} | Terms], Files, Template, Vars, Force) ->
  182. ?DEBUG("Creating directory ~p", [Path]),
  183. case ec_file:mkdir_p(expand_path(Path, Vars)) of
  184. ok ->
  185. ok;
  186. {error, Reason} ->
  187. ?ABORT("Failed while processing template instruction "
  188. "{dir, ~p}: ~p", [Path, Reason])
  189. end,
  190. execute_template(Terms, Files, Template, Vars, Force);
  191. %% Change permissions on a file
  192. execute_template([{chmod, File, Perm} | Terms], Files, Template, Vars, Force) ->
  193. Path = expand_path(File, Vars),
  194. case file:change_mode(Path, Perm) of
  195. ok ->
  196. execute_template(Terms, Files, Template, Vars, Force);
  197. {error, Reason} ->
  198. ?ABORT("Failed while processing template instruction "
  199. "{chmod, ~.8#, ~p}: ~p", [Perm, File, Reason])
  200. end;
  201. %% Create a raw untemplated file
  202. execute_template([{file, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) ->
  203. ?DEBUG("Creating file ~p", [To]),
  204. Data = load_file(Files, Type, filename:join(Cwd, From)),
  205. Out = expand_path(To,Vars),
  206. case write_file(Out, Data, Force) of
  207. ok -> ok;
  208. {error, exists} -> ?INFO("File ~p already exists.", [Out])
  209. end,
  210. execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force);
  211. %% Operate on a django template
  212. execute_template([{template, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) ->
  213. ?DEBUG("Executing template file ~p", [From]),
  214. Out = expand_path(To, Vars),
  215. Tpl = load_file(Files, Type, filename:join(Cwd, From)),
  216. case write_file(Out, render(Tpl, Vars), Force) of
  217. ok ->
  218. ok;
  219. {error, exists} ->
  220. ?INFO("File ~p already exists", [Out])
  221. end,
  222. execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force);
  223. %% Unknown
  224. execute_template([Instruction|Terms], Files, Tpl={Template,_,_}, Vars, Force) ->
  225. ?WARN("Unknown template instruction ~p in template ~s",
  226. [Instruction, Template]),
  227. execute_template(Terms, Files, Tpl, Vars, Force).
  228. %% Workaround to allow variable substitution in path names without going
  229. %% through the ErlyDTL compilation step. Parse the string and replace
  230. %% as we go.
  231. expand_path([], _) -> [];
  232. expand_path("{{"++Rest, Vars) -> replace_var(Rest, [], Vars);
  233. expand_path([H|T], Vars) -> [H | expand_path(T, Vars)].
  234. %% Actual variable replacement.
  235. replace_var("}}"++Rest, Acc, Vars) ->
  236. Var = lists:reverse(Acc),
  237. Val = proplists:get_value(list_to_atom(Var), Vars, ""),
  238. Val ++ expand_path(Rest, Vars);
  239. replace_var([H|T], Acc, Vars) ->
  240. replace_var(T, [H|Acc], Vars).
  241. %% Load a list of all the files in the escript and on disk
  242. find_templates(State) ->
  243. DiskTemplates = find_disk_templates(State),
  244. PluginTemplates = find_plugin_templates(State),
  245. {MainTemplates, Files} =
  246. case rebar_state:escript_path(State) of
  247. undefined ->
  248. {find_priv_templates(State), []};
  249. _ ->
  250. %% Cache the files since we'll potentially need to walk it several times
  251. %% over the course of a run.
  252. F = cache_escript_files(State),
  253. {find_escript_templates(F), F}
  254. end,
  255. AvailTemplates = find_available_templates([MainTemplates,
  256. PluginTemplates,
  257. DiskTemplates]),
  258. ?DEBUG("Available templates: ~p\n", [AvailTemplates]),
  259. {AvailTemplates, Files}.
  260. find_available_templates(TemplateListList) ->
  261. AvailTemplates = prioritize_templates(TemplateListList),
  262. ?DEBUG("Available templates: ~p\n", [AvailTemplates]),
  263. AvailTemplates.
  264. prioritize_templates([TemplateList]) ->
  265. tag_names(TemplateList);
  266. prioritize_templates([TemplateList | TemplateListList]) ->
  267. prioritize_templates(tag_names(TemplateList),
  268. prioritize_templates(TemplateListList)).
  269. %% Scan the current escript for available files
  270. cache_escript_files(State) ->
  271. {ok, Files} = rebar_utils:escript_foldl(
  272. fun(Name, _, GetBin, Acc) ->
  273. [{Name, GetBin()} | Acc]
  274. end,
  275. [], rebar_state:escript_path(State)),
  276. Files.
  277. %% Find all the template indexes hiding in the rebar3 escript.
  278. find_escript_templates(Files) ->
  279. [{escript, Name}
  280. || {Name, _Bin} <- Files,
  281. re:run(Name, ?TEMPLATE_RE, [{capture, none}]) == match].
  282. find_priv_templates(State) ->
  283. OtherTemplates = rebar_utils:find_files(code:priv_dir(rebar), ?TEMPLATE_RE),
  284. HomeFiles = rebar_utils:find_files(rebar_dir:template_dir(State),
  285. ?TEMPLATE_RE, true), % recursive
  286. [{file, F} || F <- OtherTemplates ++ HomeFiles].
  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. %% Take an existing list of templates and tag them by name the way
  309. %% the user would enter it from the CLI
  310. tag_names(List) ->
  311. [{filename:basename(File, ".template"), Type, File}
  312. || {Type, File} <- List].
  313. %% If multiple templates share the same name, those in the escript (built-in)
  314. %% take precedence. Otherwise, the on-disk order is the one to win.
  315. prioritize_templates([], Acc) -> Acc;
  316. prioritize_templates([{Name, Type, File} | Rest], Valid) ->
  317. case lists:keyfind(Name, 1, Valid) of
  318. false ->
  319. prioritize_templates(Rest, [{Name, Type, File} | Valid]);
  320. {_, escript, _} ->
  321. ?DEBUG("Skipping template ~p, due to presence of a built-in "
  322. "template with the same name", [Name]),
  323. prioritize_templates(Rest, Valid);
  324. {_, plugin, _} ->
  325. ?DEBUG("Skipping template ~p, due to presence of a plugin "
  326. "template with the same name", [Name]),
  327. prioritize_templates(Rest, Valid);
  328. {_, file, _} ->
  329. ?DEBUG("Skipping template ~p, due to presence of a custom "
  330. "template at ~s", [Name, File]),
  331. prioritize_templates(Rest, Valid)
  332. end.
  333. %% Read the contents of a file from the appropriate source
  334. load_file(Files, escript, Name) ->
  335. {Name, Bin} = lists:keyfind(Name, 1, Files),
  336. Bin;
  337. load_file(_Files, plugin, Name) ->
  338. {ok, Bin} = file:read_file(Name),
  339. Bin;
  340. load_file(_Files, file, Name) ->
  341. {ok, Bin} = file:read_file(Name),
  342. Bin.
  343. %% Given a string or binary, parse it into a list of terms, ala file:consult/1
  344. consult(Str) when is_list(Str) ->
  345. consult([], Str, []);
  346. consult(Bin) when is_binary(Bin)->
  347. consult([], binary_to_list(Bin), []).
  348. consult(Cont, Str, Acc) ->
  349. case erl_scan:tokens(Cont, Str, 0) of
  350. {done, Result, Remaining} ->
  351. case Result of
  352. {ok, Tokens, _} ->
  353. case erl_parse:parse_term(Tokens) of
  354. {ok, Term} -> consult([], Remaining, [Term | Acc]);
  355. {error, Reason} -> {error, Reason}
  356. end;
  357. {eof, _Other} ->
  358. lists:reverse(Acc);
  359. {error, Info, _} ->
  360. {error, Info}
  361. end;
  362. {more, Cont1} ->
  363. consult(Cont1, eof, Acc)
  364. end.
  365. write_file(Output, Data, Force) ->
  366. %% determine if the target file already exists
  367. FileExists = filelib:is_regular(Output),
  368. %% perform the function if we're allowed,
  369. %% otherwise just process the next template
  370. case Force orelse FileExists =:= false of
  371. true ->
  372. ok = filelib:ensure_dir(Output),
  373. case {Force, FileExists} of
  374. {true, true} ->
  375. ?INFO("Writing ~s (forcibly overwriting)",
  376. [Output]);
  377. _ ->
  378. ?INFO("Writing ~s", [Output])
  379. end,
  380. case file:write_file(Output, Data) of
  381. ok ->
  382. ok;
  383. {error, Reason} ->
  384. ?ABORT("Failed to write output file ~p: ~p\n",
  385. [Output, Reason])
  386. end;
  387. false ->
  388. {error, exists}
  389. end.
  390. %%
  391. %% Render a binary to a string, using mustache and the specified context
  392. %%
  393. render(Bin, Context) ->
  394. bbmustache:render(ec_cnv:to_binary(Bin), Context, [{key_type, atom}]).