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

14 years ago
14 years ago
14 years ago
  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}]).