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.

322 lines
12 KiB

  1. %% -*- tab-width: 4;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(['create-app'/2,
  29. 'create-node'/2,
  30. 'list-templates'/2,
  31. create/2]).
  32. -include("rebar.hrl").
  33. -define(TEMPLATE_RE, ".*\\.template\$").
  34. %% ===================================================================
  35. %% Public API
  36. %% ===================================================================
  37. 'create-app'(Config, File) ->
  38. %% Alias for create w/ template=simpleapp
  39. rebar_config:set_global(template, "simpleapp"),
  40. create(Config, File).
  41. 'create-node'(Config, File) ->
  42. %% Alias for create w/ template=simplenode
  43. rebar_config:set_global(template, "simplenode"),
  44. create(Config, File).
  45. 'list-templates'(_Config, _File) ->
  46. %% Load a list of all the files in the escript -- cache it in the pdict
  47. %% since we'll potentially need to walk it several times over the course
  48. %% of a run.
  49. cache_escript_files(),
  50. %% Build a list of available templates
  51. AvailTemplates = find_disk_templates() ++ find_escript_templates(),
  52. ?CONSOLE("Available templates:\n", []),
  53. _ = [begin
  54. BaseName = filename:basename(F, ".template"),
  55. ?CONSOLE("\t* ~s: ~s (~p)\n", [BaseName, F, Type])
  56. end || {Type, F} <- AvailTemplates],
  57. ok.
  58. create(_Config, _) ->
  59. %% Load a list of all the files in the escript -- cache it in the pdict
  60. %% since we'll potentially need to walk it several times over the course
  61. %% of a run.
  62. cache_escript_files(),
  63. %% Build a list of available templates
  64. AvailTemplates = find_disk_templates() ++ find_escript_templates(),
  65. ?DEBUG("Available templates: ~p\n", [AvailTemplates]),
  66. %% Using the specified template id, find the matching template file/type.
  67. %% Note that if you define the same template in both ~/.rebar/templates
  68. %% that is also present in the escript, the one on the file system will
  69. %% be preferred.
  70. {Type, Template} = select_template(AvailTemplates, template_id()),
  71. %% Load the template definition as is and get the list of variables the
  72. %% template requires.
  73. TemplateTerms = consult(load_file(Type, Template)),
  74. case lists:keyfind(variables, 1, TemplateTerms) of
  75. {variables, Vars} ->
  76. case parse_vars(Vars, dict:new()) of
  77. {error, Entry} ->
  78. Context0 = undefined,
  79. ?ABORT("Failed while processing variables from template ~p."
  80. "Variable definitions must follow form of "
  81. "[{atom(), term()}]. Failed at: ~p\n",
  82. [template_id(), Entry]);
  83. Context0 ->
  84. ok
  85. end;
  86. false ->
  87. ?WARN("No variables section found in template ~p; using empty context.\n",
  88. [template_id()]),
  89. Context0 = dict:new()
  90. end,
  91. %% For each variable, see if it's defined in global vars -- if it is, prefer that
  92. %% value over the defaults
  93. Context = update_vars(dict:fetch_keys(Context0), Context0),
  94. ?DEBUG("Template ~p context: ~p\n", [template_id(), dict:to_list(Context)]),
  95. %% Now, use our context to process the template definition -- this permits us to
  96. %% use variables within the definition for filenames.
  97. FinalTemplate = consult(render(load_file(Type, Template), Context)),
  98. ?DEBUG("Final template def ~p: ~p\n", [template_id(), FinalTemplate]),
  99. %% Execute the instructions in the finalized template
  100. Force = rebar_config:get_global(force, "0"),
  101. execute_template(FinalTemplate, Type, Template, Context, Force, []).
  102. %% ===================================================================
  103. %% Internal functions
  104. %% ===================================================================
  105. %%
  106. %% Scan the current escript for available files and cache in pdict.
  107. %%
  108. cache_escript_files() ->
  109. {ok, Files} = rebar_utils:escript_foldl(
  110. fun(Name, _, GetBin, Acc) ->
  111. [{Name, GetBin()} | Acc]
  112. end,
  113. [], rebar_config:get_global(escript, undefined)),
  114. erlang:put(escript_files, Files).
  115. template_id() ->
  116. case rebar_config:get_global(template, undefined) of
  117. undefined ->
  118. ?ABORT("No template specified.\n", []);
  119. TemplateId ->
  120. TemplateId
  121. end.
  122. find_escript_templates() ->
  123. [{escript, Name} || {Name, _Bin} <- erlang:get(escript_files),
  124. re:run(Name, ?TEMPLATE_RE, [{capture, none}]) == match].
  125. find_disk_templates() ->
  126. OtherTemplates = find_other_templates(),
  127. HomeFiles = rebar_utils:find_files(filename:join(os:getenv("HOME"),
  128. ".rebar/templates"), ?TEMPLATE_RE),
  129. LocalFiles = rebar_utils:find_files(".", ?TEMPLATE_RE),
  130. [{file, F} || F <- OtherTemplates ++ HomeFiles ++ LocalFiles].
  131. find_other_templates() ->
  132. case rebar_config:get_global(template_dir, undefined) of
  133. undefined ->
  134. [];
  135. TemplateDir ->
  136. rebar_utils:find_files(TemplateDir, ?TEMPLATE_RE)
  137. end.
  138. select_template([], Template) ->
  139. ?ABORT("Template ~s not found.\n", [Template]);
  140. select_template([{Type, Avail} | Rest], Template) ->
  141. case filename:basename(Avail, ".template") == Template of
  142. true ->
  143. {Type, Avail};
  144. false ->
  145. select_template(Rest, Template)
  146. end.
  147. %%
  148. %% Read the contents of a file from the appropriate source
  149. %%
  150. load_file(escript, Name) ->
  151. {Name, Bin} = lists:keyfind(Name, 1, erlang:get(escript_files)),
  152. Bin;
  153. load_file(file, Name) ->
  154. {ok, Bin} = file:read_file(Name),
  155. Bin.
  156. %%
  157. %% Parse/validate variables out from the template definition
  158. %%
  159. parse_vars([], Dict) ->
  160. Dict;
  161. parse_vars([{Key, Value} | Rest], Dict) when is_atom(Key) ->
  162. parse_vars(Rest, dict:store(Key, Value, Dict));
  163. parse_vars([Other | _Rest], _Dict) ->
  164. {error, Other};
  165. parse_vars(Other, _Dict) ->
  166. {error, Other}.
  167. %%
  168. %% Given a list of keys in Dict, see if there is a corresponding value defined
  169. %% in the global config; if there is, update the key in Dict with it
  170. %%
  171. update_vars([], Dict) ->
  172. Dict;
  173. update_vars([Key | Rest], Dict) ->
  174. Value = rebar_config:get_global(Key, dict:fetch(Key, Dict)),
  175. update_vars(Rest, dict:store(Key, Value, Dict)).
  176. %%
  177. %% Given a string or binary, parse it into a list of terms, ala file:consult/0
  178. %%
  179. consult(Str) when is_list(Str) ->
  180. consult([], Str, []);
  181. consult(Bin) when is_binary(Bin)->
  182. consult([], binary_to_list(Bin), []).
  183. consult(Cont, Str, Acc) ->
  184. case erl_scan:tokens(Cont, Str, 0) of
  185. {done, Result, Remaining} ->
  186. case Result of
  187. {ok, Tokens, _} ->
  188. {ok, Term} = erl_parse:parse_term(Tokens),
  189. consult([], Remaining, [Term | Acc]);
  190. {eof, _Other} ->
  191. lists:reverse(Acc);
  192. {error, Info, _} ->
  193. {error, Info}
  194. end;
  195. {more, Cont1} ->
  196. consult(Cont1, eof, Acc)
  197. end.
  198. %%
  199. %% Render a binary to a string, using mustache and the specified context
  200. %%
  201. render(Bin, Context) ->
  202. %% Be sure to escape any double-quotes before rendering...
  203. Str = re:replace(Bin, "\"", "\\\\\"", [global, {return,list}]),
  204. mustache:render(Str, Context).
  205. write_file(Output, Data, Force) ->
  206. %% determine if the target file already exists
  207. FileExists = filelib:is_file(Output),
  208. %% perform the function if we're allowed,
  209. %% otherwise just process the next template
  210. if
  211. Force =:= "1"; FileExists =:= false ->
  212. ok = filelib:ensure_dir(Output),
  213. if
  214. {Force, FileExists} =:= {"1", true} ->
  215. ?CONSOLE("Writing ~s (forcibly overwriting)~n",
  216. [Output]);
  217. true ->
  218. ?CONSOLE("Writing ~s~n", [Output])
  219. end,
  220. case file:write_file(Output, Data) of
  221. ok ->
  222. ok;
  223. {error, Reason} ->
  224. ?ABORT("Failed to write output file ~p: ~p\n",
  225. [Output, Reason])
  226. end;
  227. true ->
  228. {error, exists}
  229. end.
  230. %%
  231. %% Execute each instruction in a template definition file.
  232. %%
  233. execute_template([], _TemplateType, _TemplateName, _Context, _Force, ExistingFiles) ->
  234. case ExistingFiles of
  235. [] ->
  236. ok;
  237. _ ->
  238. Msg = lists:flatten([io_lib:format("\t* ~p~n", [F]) || F <- lists:reverse(ExistingFiles)]),
  239. Help = "To force overwriting, specify force=1 on the command line.\n",
  240. ?ERROR("One or more files already exist on disk and were not generated:~n~s~s", [Msg , Help])
  241. end;
  242. execute_template([{template, Input, Output} | Rest], TemplateType, TemplateName, Context, Force, ExistingFiles) ->
  243. InputName = filename:join(filename:dirname(TemplateName), Input),
  244. case write_file(Output, render(load_file(TemplateType, InputName), Context), Force) of
  245. ok ->
  246. execute_template(Rest, TemplateType, TemplateName, Context,
  247. Force, ExistingFiles);
  248. {error, exists} ->
  249. execute_template(Rest, TemplateType, TemplateName, Context,
  250. Force, [Output|ExistingFiles])
  251. end;
  252. execute_template([{file, Input, Output} | Rest], TemplateType, TemplateName, Context, Force, ExistingFiles) ->
  253. InputName = filename:join(filename:dirname(TemplateName), Input),
  254. case write_file(Output, load_file(TemplateType, InputName), Force) of
  255. ok ->
  256. execute_template(Rest, TemplateType, TemplateName, Context,
  257. Force, ExistingFiles);
  258. {error, exists} ->
  259. execute_template(Rest, TemplateType, TemplateName, Context,
  260. Force, [Output|ExistingFiles])
  261. end;
  262. execute_template([{dir, Name} | Rest], TemplateType, TemplateName, Context, Force, ExistingFiles) ->
  263. case filelib:ensure_dir(filename:join(Name, "dummy")) of
  264. ok ->
  265. execute_template(Rest, TemplateType, TemplateName, Context, Force, ExistingFiles);
  266. {error, Reason} ->
  267. ?ABORT("Failed while processing template instruction {dir, ~s}: ~p\n",
  268. [Name, Reason])
  269. end;
  270. execute_template([{chmod, Mod, File} | Rest], TemplateType, TemplateName, Context, Force, ExistingFiles) when is_integer(Mod) ->
  271. case file:change_mode(File, Mod) of
  272. ok ->
  273. execute_template(Rest, TemplateType, TemplateName, Context, Force, ExistingFiles);
  274. {error, Reason} ->
  275. ?ABORT("Failed while processing template instruction {cmod, ~b, ~s}: ~p~n",
  276. [Mod, File, Reason])
  277. end;
  278. execute_template([{variables, _} | Rest], TemplateType, TemplateName, Context, Force, ExistingFiles) ->
  279. execute_template(Rest, TemplateType, TemplateName, Context, Force, ExistingFiles);
  280. execute_template([Other | Rest], TemplateType, TemplateName, Context, Force, ExistingFiles) ->
  281. ?WARN("Skipping unknown template instruction: ~p\n", [Other]),
  282. execute_template(Rest, TemplateType, TemplateName, Context, Force, ExistingFiles).