您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

377 行
15 KiB

  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. %% API for other utilities that need templating functionality
  31. -export([resolve_variables/2,
  32. render/2]).
  33. -include("rebar.hrl").
  34. -define(TEMPLATE_RE, "^[^._].*\\.template\$").
  35. -define(ERLYDTL_COMPILE_OPTS, [report_warnings, return_errors, {auto_escape, false}, {out_dir, false}]).
  36. %% ===================================================================
  37. %% Public API
  38. %% ===================================================================
  39. %% Apply a template
  40. new(Template, Vars, Force, State) ->
  41. {AvailTemplates, Files} = find_templates(State),
  42. ?DEBUG("Looking for ~p~n", [Template]),
  43. case lists:keyfind(Template, 1, AvailTemplates) of
  44. false -> {not_found, Template};
  45. TemplateTup -> create(TemplateTup, Files, Vars, Force)
  46. end.
  47. %% Give a list of templates with their expanded content
  48. list_templates(State) ->
  49. {AvailTemplates, Files} = find_templates(State),
  50. [list_template(Files, Template) || Template <- AvailTemplates].
  51. %% ===================================================================
  52. %% Rendering API / legacy?
  53. %% ===================================================================
  54. %% Given a list of key value pairs, for each string value attempt to
  55. %% render it using Dict as the context. Storing the result in Dict as Key.
  56. %%
  57. resolve_variables([], Dict) ->
  58. Dict;
  59. resolve_variables([{Key, Value0} | Rest], Dict) when is_list(Value0) ->
  60. Value = render(Value0, Dict),
  61. resolve_variables(Rest, dict:store(Key, Value, Dict));
  62. resolve_variables([{Key, {list, Dicts}} | Rest], Dict) when is_list(Dicts) ->
  63. %% just un-tag it so erlydtl can use it
  64. resolve_variables(Rest, dict:store(Key, Dicts, Dict));
  65. resolve_variables([_Pair | Rest], Dict) ->
  66. resolve_variables(Rest, Dict).
  67. %%
  68. %% Render a binary to a string, using erlydtl and the specified context
  69. %%
  70. render(Template, Context) when is_atom(Template) ->
  71. Template:render(Context);
  72. render(Template, Context) ->
  73. Module = list_to_atom(Template++"_dtl"),
  74. Module:render(Context).
  75. %% ===================================================================
  76. %% Internal Functions
  77. %% ===================================================================
  78. %% Expand a single template's value
  79. list_template(Files, {Name, Type, File}) ->
  80. TemplateTerms = consult(load_file(Files, Type, File)),
  81. {Name, Type, File,
  82. get_template_description(TemplateTerms),
  83. get_template_vars(TemplateTerms)}.
  84. %% Load up the template description out from a list of attributes read in
  85. %% a .template file.
  86. get_template_description(TemplateTerms) ->
  87. case lists:keyfind(description, 1, TemplateTerms) of
  88. {_, Desc} -> Desc;
  89. false -> undefined
  90. end.
  91. %% Load up the variables out from a list of attributes read in a .template file
  92. %% and return them merged with the globally-defined and default variables.
  93. get_template_vars(TemplateTerms) ->
  94. Vars = case lists:keyfind(variables, 1, TemplateTerms) of
  95. {_, Value} -> Value;
  96. false -> []
  97. end,
  98. override_vars(Vars, override_vars(global_variables(), default_variables())).
  99. %% Provide a way to merge a set of variables with another one. The left-hand
  100. %% set of variables takes precedence over the right-hand set.
  101. %% In the case where left-hand variable description contains overriden defaults, but
  102. %% the right-hand one contains additional data such as documentation, the resulting
  103. %% variable description will contain the widest set of information possible.
  104. override_vars([], General) -> General;
  105. override_vars([{Var, Default} | Rest], General) ->
  106. case lists:keytake(Var, 1, General) of
  107. {value, {Var, _Default, Doc}, NewGeneral} ->
  108. [{Var, Default, Doc} | override_vars(Rest, NewGeneral)];
  109. {value, {Var, _Default}, NewGeneral} ->
  110. [{Var, Default} | override_vars(Rest, NewGeneral)];
  111. false ->
  112. [{Var, Default} | override_vars(Rest, General)]
  113. end;
  114. override_vars([{Var, Default, Doc} | Rest], General) ->
  115. [{Var, Default, Doc} | override_vars(Rest, lists:keydelete(Var, 1, General))].
  116. %% Default variables, generated dynamically.
  117. default_variables() ->
  118. {{Y,M,D},{H,Min,S}} = calendar:universal_time(),
  119. [{date, lists:flatten(io_lib:format("~4..0w-~2..0w-~2..0w",[Y,M,D]))},
  120. {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]))},
  121. {author_name, "Anonymous"},
  122. {author_email, "anonymous@example.org"},
  123. {copyright_year, integer_to_list(Y)},
  124. {apps_dir, "apps", "Directory where applications will be created if needed"}].
  125. %% Load variable definitions from the 'Globals' file in the home template
  126. %% directory
  127. global_variables() ->
  128. Home = rebar_dir:home_dir(),
  129. GlobalFile = filename:join([Home, ?CONFIG_DIR, "templates", "globals"]),
  130. case file:consult(GlobalFile) of
  131. {error, enoent} -> [];
  132. {ok, Data} -> proplists:get_value(variables, Data, [])
  133. end.
  134. %% drop the documentation for variables when present
  135. drop_var_docs([]) -> [];
  136. drop_var_docs([{K,V,_}|Rest]) -> [{K,V} | drop_var_docs(Rest)];
  137. drop_var_docs([{K,V}|Rest]) -> [{K,V} | drop_var_docs(Rest)].
  138. %% Load the template index, resolve all variables, and then execute
  139. %% the template.
  140. create({Template, Type, File}, Files, UserVars, Force) ->
  141. TemplateTerms = consult(load_file(Files, Type, File)),
  142. Vars = drop_var_docs(override_vars(UserVars, get_template_vars(TemplateTerms))),
  143. TemplateCwd = filename:dirname(File),
  144. execute_template(TemplateTerms, Files, {Template, Type, TemplateCwd}, Vars, Force).
  145. %% Run template instructions one at a time.
  146. execute_template([], _, {Template,_,_}, _, _) ->
  147. ?DEBUG("Template ~s applied~n", [Template]),
  148. ok;
  149. %% We can't execute the description
  150. execute_template([{description, _} | Terms], Files, Template, Vars, Force) ->
  151. execute_template(Terms, Files, Template, Vars, Force);
  152. %% We can't execute variables
  153. execute_template([{variables, _} | Terms], Files, Template, Vars, Force) ->
  154. execute_template(Terms, Files, Template, Vars, Force);
  155. %% Create a directory
  156. execute_template([{dir, Path} | Terms], Files, Template, Vars, Force) ->
  157. ?DEBUG("Creating directory ~p~n", [Path]),
  158. case ec_file:mkdir_p(expand_path(Path, Vars)) of
  159. ok ->
  160. ok;
  161. {error, Reason} ->
  162. ?ABORT("Failed while processing template instruction "
  163. "{dir, ~p}: ~p~n", [Path, Reason])
  164. end,
  165. execute_template(Terms, Files, Template, Vars, Force);
  166. %% Change permissions on a file
  167. execute_template([{chmod, File, Perm} | Terms], Files, Template, Vars, Force) ->
  168. Path = expand_path(File, Vars),
  169. case file:change_mode(Path, Perm) of
  170. ok ->
  171. execute_template(Terms, Files, Template, Vars, Force);
  172. {error, Reason} ->
  173. ?ABORT("Failed while processing template instruction "
  174. "{chmod, ~.8#, ~p}: ~p~n", [Perm, File, Reason])
  175. end;
  176. %% Create a raw untemplated file
  177. execute_template([{file, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) ->
  178. ?DEBUG("Creating file ~p~n", [To]),
  179. Data = load_file(Files, Type, filename:join(Cwd, From)),
  180. Out = expand_path(To,Vars),
  181. case write_file(Out, Data, Force) of
  182. ok -> ok;
  183. {error, exists} -> ?INFO("File ~p already exists.~n", [Out])
  184. end,
  185. execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force);
  186. %% Operate on a django template
  187. execute_template([{template, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) ->
  188. ?DEBUG("Executing template file ~p~n", [From]),
  189. Out = expand_path(To, Vars),
  190. Tpl = load_file(Files, Type, filename:join(Cwd, From)),
  191. TplName = make_template_name("rebar_template", Out),
  192. {ok, Mod} = erlydtl:compile_template(Tpl, TplName, ?ERLYDTL_COMPILE_OPTS),
  193. {ok, Output} = Mod:render(Vars),
  194. case write_file(Out, Output, Force) of
  195. ok -> ok;
  196. {error, exists} -> ?INFO("File ~p already exists~n", [Out])
  197. end,
  198. execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force);
  199. %% Unknown
  200. execute_template([Instruction|Terms], Files, Tpl={Template,_,_}, Vars, Force) ->
  201. ?WARN("Unknown template instruction ~p in template ~s",
  202. [Instruction, Template]),
  203. execute_template(Terms, Files, Tpl, Vars, Force).
  204. %% Workaround to allow variable substitution in path names without going
  205. %% through the ErlyDTL compilation step. Parse the string and replace
  206. %% as we go.
  207. expand_path([], _) -> [];
  208. expand_path("{{"++Rest, Vars) -> replace_var(Rest, [], Vars);
  209. expand_path([H|T], Vars) -> [H | expand_path(T, Vars)].
  210. %% Actual variable replacement.
  211. replace_var("}}"++Rest, Acc, Vars) ->
  212. Var = lists:reverse(Acc),
  213. Val = proplists:get_value(list_to_atom(Var), Vars, ""),
  214. Val ++ expand_path(Rest, Vars);
  215. replace_var([H|T], Acc, Vars) ->
  216. replace_var(T, [H|Acc], Vars).
  217. %% Load a list of all the files in the escript and on disk
  218. find_templates(State) ->
  219. %% Cache the files since we'll potentially need to walk it several times
  220. %% over the course of a run.
  221. Files = cache_escript_files(State),
  222. %% Build a list of available templates
  223. AvailTemplates = prioritize_templates(
  224. tag_names(find_disk_templates(State)),
  225. tag_names(find_escript_templates(Files))),
  226. ?DEBUG("Available templates: ~p\n", [AvailTemplates]),
  227. {AvailTemplates, Files}.
  228. %% Scan the current escript for available files
  229. cache_escript_files(State) ->
  230. {ok, Files} = rebar_utils:escript_foldl(
  231. fun(Name, _, GetBin, Acc) ->
  232. [{Name, GetBin()} | Acc]
  233. end,
  234. [], rebar_state:get(State, escript)),
  235. Files.
  236. %% Find all the template indexes hiding in the rebar3 escript.
  237. find_escript_templates(Files) ->
  238. [{escript, Name}
  239. || {Name, _Bin} <- Files,
  240. re:run(Name, ?TEMPLATE_RE, [{capture, none}]) == match].
  241. %% Fetch template indexes that sit on disk in the user's HOME
  242. find_disk_templates(State) ->
  243. OtherTemplates = find_other_templates(State),
  244. Home = rebar_dir:home_dir(),
  245. HomeFiles = rebar_utils:find_files(filename:join([Home, ?CONFIG_DIR, "templates"]),
  246. ?TEMPLATE_RE),
  247. [{file, F} || F <- OtherTemplates ++ HomeFiles].
  248. %% Fetch template indexes that sit on disk in custom areas
  249. find_other_templates(State) ->
  250. case rebar_state:get(State, template_dir, undefined) of
  251. undefined ->
  252. [];
  253. TemplateDir ->
  254. rebar_utils:find_files(TemplateDir, ?TEMPLATE_RE)
  255. end.
  256. %% Take an existing list of templates and tag them by name the way
  257. %% the user would enter it from the CLI
  258. tag_names(List) ->
  259. [{filename:basename(File, ".template"), Type, File}
  260. || {Type, File} <- List].
  261. %% If multiple templates share the same name, those in the escript (built-in)
  262. %% take precedence. Otherwise, the on-disk order is the one to win.
  263. prioritize_templates([], Acc) -> Acc;
  264. prioritize_templates([{Name, Type, File} | Rest], Valid) ->
  265. case lists:keyfind(Name, 1, Valid) of
  266. false ->
  267. prioritize_templates(Rest, [{Name, Type, File} | Valid]);
  268. {_, escript, _} ->
  269. ?DEBUG("Skipping template ~p, due to presence of a built-in "
  270. "template with the same name~n", [Name]),
  271. prioritize_templates(Rest, Valid);
  272. {_, file, _} ->
  273. ?DEBUG("Skipping template ~p, due to presence of a custom "
  274. "template at ~s~n", [File]),
  275. prioritize_templates(Rest, Valid)
  276. end.
  277. %% Read the contents of a file from the appropriate source
  278. load_file(Files, escript, Name) ->
  279. {Name, Bin} = lists:keyfind(Name, 1, Files),
  280. Bin;
  281. load_file(_Files, file, Name) ->
  282. {ok, Bin} = file:read_file(Name),
  283. Bin.
  284. %% Given a string or binary, parse it into a list of terms, ala file:consult/1
  285. consult(Str) when is_list(Str) ->
  286. consult([], Str, []);
  287. consult(Bin) when is_binary(Bin)->
  288. consult([], binary_to_list(Bin), []).
  289. consult(Cont, Str, Acc) ->
  290. case erl_scan:tokens(Cont, Str, 0) of
  291. {done, Result, Remaining} ->
  292. case Result of
  293. {ok, Tokens, _} ->
  294. {ok, Term} = erl_parse:parse_term(Tokens),
  295. consult([], Remaining, [Term | Acc]);
  296. {eof, _Other} ->
  297. lists:reverse(Acc);
  298. {error, Info, _} ->
  299. {error, Info}
  300. end;
  301. {more, Cont1} ->
  302. consult(Cont1, eof, Acc)
  303. end.
  304. write_file(Output, Data, Force) ->
  305. %% determine if the target file already exists
  306. FileExists = filelib:is_regular(Output),
  307. %% perform the function if we're allowed,
  308. %% otherwise just process the next template
  309. case Force orelse FileExists =:= false of
  310. true ->
  311. ok = filelib:ensure_dir(Output),
  312. case {Force, FileExists} of
  313. {true, true} ->
  314. ?INFO("Writing ~s (forcibly overwriting)",
  315. [Output]);
  316. _ ->
  317. ?INFO("Writing ~s", [Output])
  318. end,
  319. case file:write_file(Output, Data) of
  320. ok ->
  321. ok;
  322. {error, Reason} ->
  323. ?ABORT("Failed to write output file ~p: ~p\n",
  324. [Output, Reason])
  325. end;
  326. false ->
  327. {error, exists}
  328. end.
  329. -spec make_template_name(string(), term()) -> module().
  330. make_template_name(Base, Value) ->
  331. %% Seed so we get different values each time
  332. random:seed(erlang:now()),
  333. Hash = erlang:phash2(Value),
  334. Ran = random:uniform(10000000),
  335. erlang:list_to_atom(Base ++ "_" ++
  336. erlang:integer_to_list(Hash) ++
  337. "_" ++ erlang:integer_to_list(Ran)).