Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

441 rinda
18 KiB

pirms 10 gadiem
pirms 10 gadiem
pirms 10 gadiem
pirms 14 gadiem
pirms 10 gadiem
pirms 10 gadiem
pirms 14 gadiem
pirms 14 gadiem
  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 rebar_string:consult(binary_to_list(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 = rebar_string:consult(binary_to_list(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. Result = execute_template(TemplateTerms, Files, {Template, Type, TemplateCwd}, Vars, Force),
  149. maybe_print_final_message(proplists:get_value(message, TemplateTerms, undefined), Vars),
  150. Result.
  151. maybe_print_final_message(undefined, _) ->
  152. ok;
  153. maybe_print_final_message(Message, Values) ->
  154. io:format("~s~n", [render(Message, Values)]).
  155. maybe_warn_about_name(Vars) ->
  156. Name = proplists:get_value(name, Vars, "valid"),
  157. case validate_atom(Name) of
  158. invalid ->
  159. ?WARN("The 'name' variable is often associated with Erlang "
  160. "module names and/or file names. The value submitted "
  161. "(~ts) isn't an unquoted Erlang atom. Templates "
  162. "generated may contain errors.",
  163. [Name]);
  164. valid ->
  165. ok
  166. end.
  167. validate_atom(Str) ->
  168. case io_lib:fread("~a", unicode:characters_to_list(Str)) of
  169. {ok, [Atom], ""} ->
  170. case io_lib:write_atom(Atom) of
  171. "'" ++ _ -> invalid; % quoted
  172. _ -> valid % unquoted
  173. end;
  174. _ ->
  175. invalid
  176. end.
  177. %% Run template instructions one at a time.
  178. execute_template([], _, {Template,_,_}, _, _) ->
  179. ?DEBUG("Template ~ts applied", [Template]),
  180. ok;
  181. %% We can't execute the description
  182. execute_template([{description, _} | Terms], Files, Template, Vars, Force) ->
  183. execute_template(Terms, Files, Template, Vars, Force);
  184. %% We can't execute variables
  185. execute_template([{variables, _} | Terms], Files, Template, Vars, Force) ->
  186. execute_template(Terms, Files, Template, Vars, Force);
  187. %% We can't execute message
  188. execute_template([{message, _} | Terms], Files, Template, Vars, Force) ->
  189. execute_template(Terms, Files, Template, Vars, Force);
  190. %% Create a directory
  191. execute_template([{dir, Path} | Terms], Files, Template, Vars, Force) ->
  192. ?DEBUG("Creating directory ~p", [Path]),
  193. case ec_file:mkdir_p(expand_path(Path, Vars)) of
  194. ok ->
  195. ok;
  196. {error, Reason} ->
  197. ?ABORT("Failed while processing template instruction "
  198. "{dir, ~p}: ~p", [Path, Reason])
  199. end,
  200. execute_template(Terms, Files, Template, Vars, Force);
  201. %% Change permissions on a file
  202. execute_template([{chmod, File, Perm} | Terms], Files, Template, Vars, Force) ->
  203. Path = expand_path(File, Vars),
  204. case file:change_mode(Path, Perm) of
  205. ok ->
  206. execute_template(Terms, Files, Template, Vars, Force);
  207. {error, Reason} ->
  208. ?ABORT("Failed while processing template instruction "
  209. "{chmod, ~.8#, ~p}: ~p", [Perm, File, Reason])
  210. end;
  211. %% Create a raw untemplated file
  212. execute_template([{file, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) ->
  213. ?DEBUG("Creating file ~p", [To]),
  214. Data = load_file(Files, Type, filename:join(Cwd, From)),
  215. Out = expand_path(To,Vars),
  216. case write_file(Out, Data, Force) of
  217. ok -> ok;
  218. {error, exists} -> ?INFO("File ~p already exists.", [Out])
  219. end,
  220. execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force);
  221. %% Operate on a django template
  222. execute_template([{template, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) ->
  223. ?DEBUG("Executing template file ~p", [From]),
  224. Out = expand_path(To, Vars),
  225. Tpl = load_file(Files, Type, filename:join(Cwd, From)),
  226. case write_file(Out, render(Tpl, Vars), Force) of
  227. ok ->
  228. ok;
  229. {error, exists} ->
  230. ?INFO("File ~p already exists", [Out])
  231. end,
  232. execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force);
  233. %% Unknown
  234. execute_template([Instruction|Terms], Files, Tpl={Template,_,_}, Vars, Force) ->
  235. ?WARN("Unknown template instruction ~p in template ~ts",
  236. [Instruction, Template]),
  237. execute_template(Terms, Files, Tpl, Vars, Force).
  238. %% Workaround to allow variable substitution in path names without going
  239. %% through the ErlyDTL compilation step. Parse the string and replace
  240. %% as we go.
  241. expand_path([], _) -> [];
  242. expand_path("{{"++Rest, Vars) -> replace_var(Rest, [], Vars);
  243. expand_path([H|T], Vars) -> [H | expand_path(T, Vars)].
  244. %% Actual variable replacement.
  245. replace_var("}}"++Rest, Acc, Vars) ->
  246. Var = lists:reverse(Acc),
  247. Val = proplists:get_value(list_to_atom(Var), Vars, ""),
  248. Val ++ expand_path(Rest, Vars);
  249. replace_var([H|T], Acc, Vars) ->
  250. replace_var(T, [H|Acc], Vars).
  251. %% Load a list of all the files in the escript and on disk
  252. find_templates(State) ->
  253. DiskTemplates = find_disk_templates(State),
  254. PluginTemplates = find_plugin_templates(State),
  255. {MainTemplates, Files} =
  256. case rebar_state:escript_path(State) of
  257. undefined -> % running in local install
  258. {find_localinstall_templates(State), []};
  259. _ ->
  260. %% Cache the files since we'll potentially need to walk it several times
  261. %% over the course of a run.
  262. F = cache_escript_files(State),
  263. {find_escript_templates(F), F}
  264. end,
  265. AvailTemplates = find_available_templates([MainTemplates,
  266. PluginTemplates,
  267. DiskTemplates]),
  268. ?DEBUG("Available templates: ~p\n", [AvailTemplates]),
  269. {AvailTemplates, Files}.
  270. find_available_templates(TemplateListList) ->
  271. AvailTemplates = prioritize_templates(TemplateListList),
  272. ?DEBUG("Available templates: ~p\n", [AvailTemplates]),
  273. AvailTemplates.
  274. prioritize_templates([TemplateList]) ->
  275. tag_names(TemplateList);
  276. prioritize_templates([TemplateList | TemplateListList]) ->
  277. prioritize_templates(tag_names(TemplateList),
  278. prioritize_templates(TemplateListList)).
  279. %% Scan the current escript for available files
  280. cache_escript_files(State) ->
  281. {ok, Files} = rebar_utils:escript_foldl(
  282. fun(Name, _, GetBin, Acc) ->
  283. [{Name, GetBin()} | Acc]
  284. end,
  285. [], rebar_state:escript_path(State)),
  286. Files.
  287. %% Find all the template indexes hiding in the rebar3 escript.
  288. find_escript_templates(Files) ->
  289. [{escript, Name}
  290. || {Name, _Bin} <- Files,
  291. re:run(Name, ?TEMPLATE_RE, [{capture, none}, unicode]) == match].
  292. find_localinstall_templates(_State) ->
  293. Templates = rebar_utils:find_files(code:priv_dir(rebar), ?TEMPLATE_RE),
  294. %% Pretend we're still running escripts; should work transparently.
  295. [{builtin, F} || F <- Templates].
  296. %% Fetch template indexes that sit on disk in the user's HOME
  297. find_disk_templates(State) ->
  298. OtherTemplates = find_other_templates(State),
  299. HomeFiles = rebar_utils:find_files(rebar_dir:template_dir(State),
  300. ?TEMPLATE_RE, true), % recursive
  301. [{file, F} || F <- OtherTemplates ++ HomeFiles].
  302. %% Fetch template indexes that sit on disk in custom areas
  303. find_other_templates(State) ->
  304. case rebar_state:get(State, template_dir, undefined) of
  305. undefined ->
  306. [];
  307. TemplateDir ->
  308. rebar_utils:find_files(TemplateDir, ?TEMPLATE_RE, true) % recursive
  309. end.
  310. %% Fetch template indexes that sit on disk in plugins
  311. find_plugin_templates(State) ->
  312. [{plugin, File}
  313. || App <- rebar_state:all_plugin_deps(State),
  314. Priv <- [rebar_app_info:priv_dir(App)],
  315. Priv =/= undefined,
  316. File <- rebar_utils:find_files(Priv, ?TEMPLATE_RE)]
  317. ++ %% and add global plugins too
  318. [{plugin, File}
  319. || PSource <- rebar_state:get(State, {plugins, global}, []),
  320. Plugin <- [plugin_provider(PSource)],
  321. is_atom(Plugin),
  322. Priv <- [code:priv_dir(Plugin)],
  323. Priv =/= undefined,
  324. File <- rebar_utils:find_files(Priv, ?TEMPLATE_RE)].
  325. plugin_provider(P) when is_atom(P) -> P;
  326. plugin_provider(T) when is_tuple(T) -> element(1, T).
  327. %% Take an existing list of templates and tag them by name the way
  328. %% the user would enter it from the CLI
  329. tag_names(List) ->
  330. [{filename:basename(File, ".template"), Type, File}
  331. || {Type, File} <- List].
  332. %% If multiple templates share the same name, those in the escript (built-in)
  333. %% take precedence. Otherwise, the on-disk order is the one to win.
  334. prioritize_templates([], Acc) -> Acc;
  335. prioritize_templates([{Name, Type, File} | Rest], Valid) ->
  336. case lists:keyfind(Name, 1, Valid) of
  337. false ->
  338. prioritize_templates(Rest, [{Name, Type, File} | Valid]);
  339. {_, escript, _} ->
  340. ?DEBUG("Skipping template ~p, due to presence of a built-in "
  341. "template with the same name", [Name]),
  342. prioritize_templates(Rest, Valid);
  343. {_, builtin, _} ->
  344. ?DEBUG("Skipping template ~p, due to presence of a built-in "
  345. "template with the same name", [Name]),
  346. prioritize_templates(Rest, Valid);
  347. {_, plugin, _} ->
  348. ?DEBUG("Skipping template ~p, due to presence of a plugin "
  349. "template with the same name", [Name]),
  350. prioritize_templates(Rest, Valid);
  351. {_, file, _} ->
  352. ?DEBUG("Skipping template ~p, due to presence of a custom "
  353. "template at ~ts", [Name, File]),
  354. prioritize_templates(Rest, Valid)
  355. end.
  356. %% Read the contents of a file from the appropriate source
  357. load_file(Files, escript, Name) ->
  358. {Name, Bin} = lists:keyfind(Name, 1, Files),
  359. Bin;
  360. load_file(_Files, builtin, Name) ->
  361. {ok, Bin} = file:read_file(Name),
  362. Bin;
  363. load_file(_Files, plugin, Name) ->
  364. {ok, Bin} = file:read_file(Name),
  365. Bin;
  366. load_file(_Files, file, Name) ->
  367. {ok, Bin} = file:read_file(Name),
  368. Bin.
  369. write_file(Output, Data, Force) ->
  370. %% determine if the target file already exists
  371. FileExists = filelib:is_regular(Output),
  372. %% perform the function if we're allowed,
  373. %% otherwise just process the next template
  374. case Force orelse FileExists =:= false of
  375. true ->
  376. ok = filelib:ensure_dir(Output),
  377. case {Force, FileExists} of
  378. {true, true} ->
  379. ?INFO("Writing ~ts (forcibly overwriting)",
  380. [Output]);
  381. _ ->
  382. ?INFO("Writing ~ts", [Output])
  383. end,
  384. case file:write_file(Output, Data) of
  385. ok ->
  386. ok;
  387. {error, Reason} ->
  388. ?ABORT("Failed to write output file ~p: ~p\n",
  389. [Output, Reason])
  390. end;
  391. false ->
  392. {error, exists}
  393. end.
  394. %% Render a binary to a string, using mustache and the specified context
  395. render(Bin, Context) ->
  396. bbmustache:render(
  397. rebar_utils:to_binary(Bin),
  398. Context,
  399. [{key_type, atom},
  400. {escape_fun, fun(X) -> X end}] % disable HTML-style escaping
  401. ).