%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ts=4 sw=4 et %% ------------------------------------------------------------------- %% %% rebar: Erlang Build Tools %% %% Copyright (c) 2009 Dave Smith (dizzyd@dizzyd.com) %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal %% in the Software without restriction, including without limitation the rights %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the Software is %% furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN %% THE SOFTWARE. %% ------------------------------------------------------------------- -module(rebar_templater). -export([new/4, list_templates/1, render/2]). -ifdef(TEST). -export([consult_template/3]). -endif. -include("rebar.hrl"). -define(TEMPLATE_RE, "^(?!\\._).*\\.template\$"). %% =================================================================== %% Public API %% =================================================================== %% Apply a template new(Template, Vars, Force, State) -> {AvailTemplates, Files} = find_templates(State), ?DEBUG("Looking for ~p", [Template]), case lists:keyfind(Template, 1, AvailTemplates) of false -> {not_found, Template}; TemplateTup -> create(TemplateTup, Files, Vars, Force, State) end. %% Give a list of templates with their expanded content list_templates(State) -> {AvailTemplates, Files} = find_templates(State), [list_template(Files, Template, State) || Template <- AvailTemplates]. %% =================================================================== %% Internal Functions %% =================================================================== %% Expand a single template's value list_template(Files, {Name, Type, File}, State) -> case consult_template(Files, Type, File) of {error, Reason} -> {error, {consult, File, Reason}}; TemplateTerms -> {Name, Type, File, get_template_description(TemplateTerms), get_template_vars(TemplateTerms, State)} end. %% Load up the template description out from a list of attributes read in %% a .template file. get_template_description(TemplateTerms) -> case lists:keyfind(description, 1, TemplateTerms) of {_, Desc} -> Desc; false -> undefined end. %% Load up the variables out from a list of attributes read in a .template file %% and return them merged with the globally-defined and default variables. get_template_vars(TemplateTerms, State) -> Vars = case lists:keyfind(variables, 1, TemplateTerms) of {_, Value} -> Value; false -> [] end, override_vars(Vars, override_vars(global_variables(State), default_variables())). %% Provide a way to merge a set of variables with another one. The left-hand %% set of variables takes precedence over the right-hand set. %% In the case where left-hand variable description contains overridden defaults, but %% the right-hand one contains additional data such as documentation, the resulting %% variable description will contain the widest set of information possible. override_vars([], General) -> General; override_vars([{Var, Default} | Rest], General) -> case lists:keytake(Var, 1, General) of {value, {Var, _Default, Doc}, NewGeneral} -> [{Var, Default, Doc} | override_vars(Rest, NewGeneral)]; {value, {Var, _Default}, NewGeneral} -> [{Var, Default} | override_vars(Rest, NewGeneral)]; false -> [{Var, Default} | override_vars(Rest, General)] end; override_vars([{Var, Default, Doc} | Rest], General) -> [{Var, Default, Doc} | override_vars(Rest, lists:keydelete(Var, 1, General))]. %% Default variables, generated dynamically. default_variables() -> {DefaultAuthor, DefaultEmail} = default_author_and_email(), {{Y,M,D},{H,Min,S}} = calendar:universal_time(), [{date, lists:flatten(io_lib:format("~4..0w-~2..0w-~2..0w",[Y,M,D]))}, {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]))}, {author_name, DefaultAuthor}, {author_email, DefaultEmail}, {copyright_year, integer_to_list(Y)}, {apps_dir, "apps", "Directory where applications will be created if needed"}]. default_author_and_email() -> %% See if we can get a git user and email to use as defaults case rebar_utils:sh("git config --global user.name", [return_on_error]) of {ok, Name} -> case rebar_utils:sh("git config --global user.email", [return_on_error]) of {ok, Email} -> {rebar_string:trim(Name, both, "\n"), rebar_string:trim(Email, both, "\n")}; {error, _} -> %% Use neither if one doesn't exist {"Anonymous", "anonymous@example.org"} end; {error, _} -> %% Ok, try mecurial case rebar_utils:sh("hg showconfig ui.username", [return_on_error]) of {ok, NameEmail} -> case re:run(NameEmail, "^(.*) <(.*)>$", [{capture, [1,2], list}, unicode]) of {match, [Name, Email]} -> {Name, Email}; _ -> {"Anonymous", "anonymous@example.org"} end; {error, _} -> {"Anonymous", "anonymous@example.org"} end end. %% Load variable definitions from the 'Globals' file in the home template %% directory global_variables(State) -> GlobalFile = rebar_dir:template_globals(State), case file:consult(GlobalFile) of {error, enoent} -> []; {ok, Data} -> proplists:get_value(variables, Data, []) end. %% drop the documentation for variables when present drop_var_docs([]) -> []; drop_var_docs([{K,V,_}|Rest]) -> [{K,V} | drop_var_docs(Rest)]; drop_var_docs([{K,V}|Rest]) -> [{K,V} | drop_var_docs(Rest)]. %% Load the template index, resolve all variables, and then execute %% the template. create({Template, Type, File}, Files, UserVars, Force, State) -> TemplateTerms = consult_template(Files, Type, File), Vars0 = drop_var_docs(override_vars(UserVars, get_template_vars(TemplateTerms, State))), Vars = maybe_handle_author_name(Vars0), maybe_warn_about_name(Vars), TemplateCwd = filename:dirname(File), Result = execute_template(TemplateTerms, Files, {Template, Type, TemplateCwd}, Vars, Force), maybe_print_final_message(proplists:get_value(message, TemplateTerms, undefined), Vars), Result. maybe_handle_author_name(Vars) -> case lists:keyfind(author_name, 1, Vars) of false -> Vars; {author_name, Name0} -> Name1 = unicode:characters_to_binary(Name0), lists:keyreplace(author_name, 1, Vars, {author_name, Name1}) end. maybe_print_final_message(undefined, _) -> ok; maybe_print_final_message(Message, Values) -> io:format("~s~n", [render(Message, Values)]). maybe_warn_about_name(Vars) -> Name = proplists:get_value(name, Vars, "valid"), case validate_atom(Name) of invalid -> ?WARN("The 'name' variable is often associated with Erlang " "module names and/or file names. The value submitted " "(~ts) isn't an unquoted Erlang atom. Templates " "generated may contain errors.", [Name]); valid -> ok end. maybe_warn_about_name_clash(File) -> case filename:extension(File) of ".erl" -> Module0 = re:replace(filename:basename(File), "\\.erl$", "", [{return, list}]), Module = list_to_atom(Module0), try Module:module_info() of _ -> ?WARN("The module definition of '~ts' in file ~ts " "will clash with an existing Erlang module.", [Module, File]) catch _:_ -> ok end; _ -> ok end. validate_atom(Str) -> case io_lib:fread("~a", unicode:characters_to_list(Str)) of {ok, [Atom], ""} -> case io_lib:write_atom(Atom) of "'" ++ _ -> invalid; % quoted _ -> valid % unquoted end; _ -> invalid end. %% Run template instructions one at a time. execute_template([], _, {Template,_,_}, _, _) -> ?DEBUG("Template ~ts applied", [Template]), ok; %% We can't execute the description execute_template([{description, _} | Terms], Files, Template, Vars, Force) -> execute_template(Terms, Files, Template, Vars, Force); %% We can't execute variables execute_template([{variables, _} | Terms], Files, Template, Vars, Force) -> execute_template(Terms, Files, Template, Vars, Force); %% We can't execute message execute_template([{message, _} | Terms], Files, Template, Vars, Force) -> execute_template(Terms, Files, Template, Vars, Force); %% Create a directory execute_template([{dir, Path} | Terms], Files, Template, Vars, Force) -> ?DEBUG("Creating directory ~p", [Path]), case ec_file:mkdir_p(expand_path(Path, Vars)) of ok -> ok; {error, Reason} -> ?ABORT("Failed while processing template instruction " "{dir, ~p}: ~p", [Path, Reason]) end, execute_template(Terms, Files, Template, Vars, Force); %% Change permissions on a file execute_template([{chmod, File, Perm} | Terms], Files, Template, Vars, Force) -> Path = expand_path(File, Vars), case file:change_mode(Path, Perm) of ok -> execute_template(Terms, Files, Template, Vars, Force); {error, Reason} -> ?ABORT("Failed while processing template instruction " "{chmod, ~.8#, ~p}: ~p", [Perm, File, Reason]) end; %% Create a raw untemplated file execute_template([{file, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) -> ?DEBUG("Creating file ~p", [To]), In = expand_path(From, Vars), Data = load_file(Files, Type, filename:join(Cwd, In)), Out = expand_path(To,Vars), case write_file(Out, Data, Force) of ok -> ok; {error, exists} -> ?INFO("File ~p already exists.", [Out]) end, execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force); %% Operate on a django template execute_template([{template, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) -> ?DEBUG("Executing template file ~p", [From]), In = expand_path(From, Vars), Out = expand_path(To, Vars), Tpl = load_file(Files, Type, filename:join(Cwd, In)), maybe_warn_about_name_clash(Out), case write_file(Out, render(Tpl, Vars), Force) of ok -> ok; {error, exists} -> ?INFO("File ~p already exists", [Out]) end, execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force); %% Unknown execute_template([Instruction|Terms], Files, Tpl={Template,_,_}, Vars, Force) -> ?WARN("Unknown template instruction ~p in template ~ts", [Instruction, Template]), execute_template(Terms, Files, Tpl, Vars, Force). %% Workaround to allow variable substitution in path names without going %% through the ErlyDTL compilation step. Parse the string and replace %% as we go. expand_path([], _) -> []; expand_path("{{"++Rest, Vars) -> replace_var(Rest, [], Vars); expand_path([H|T], Vars) -> [H | expand_path(T, Vars)]. %% Actual variable replacement. replace_var("}}"++Rest, Acc, Vars) -> Var = lists:reverse(Acc), Val = proplists:get_value(list_to_atom(Var), Vars, ""), Val ++ expand_path(Rest, Vars); replace_var([H|T], Acc, Vars) -> replace_var(T, [H|Acc], Vars). %% Load a list of all the files in the escript and on disk find_templates(State) -> DiskTemplates = find_disk_templates(State), PluginTemplates = find_plugin_templates(State), {MainTemplates, Files} = case rebar_state:escript_path(State) of undefined -> % running in local install {find_localinstall_templates(State), []}; _ -> %% Cache the files since we'll potentially need to walk it several times %% over the course of a run. F = cache_escript_files(State), {find_escript_templates(F), F} end, AvailTemplates = find_available_templates([MainTemplates, PluginTemplates, DiskTemplates]), ?DEBUG("Available templates: ~p\n", [AvailTemplates]), {AvailTemplates, Files}. find_available_templates(TemplateListList) -> AvailTemplates = prioritize_templates(TemplateListList), ?DEBUG("Available templates: ~p\n", [AvailTemplates]), AvailTemplates. prioritize_templates([TemplateList]) -> tag_names(TemplateList); prioritize_templates([TemplateList | TemplateListList]) -> prioritize_templates(tag_names(TemplateList), prioritize_templates(TemplateListList)). %% Scan the current escript for available files cache_escript_files(State) -> {ok, Files} = rebar_utils:escript_foldl( fun(Name, _, GetBin, Acc) -> [{Name, GetBin()} | Acc] end, [], rebar_state:escript_path(State)), Files. %% Find all the template indexes hiding in the rebar3 escript. find_escript_templates(Files) -> [{escript, Name} || {Name, _Bin} <- Files, re:run(Name, ?TEMPLATE_RE, [{capture, none}, unicode]) == match]. find_localinstall_templates(_State) -> Templates = rebar_utils:find_files(code:priv_dir(rebar), ?TEMPLATE_RE), %% Pretend we're still running escripts; should work transparently. [{builtin, F} || F <- Templates]. %% Fetch template indexes that sit on disk in the user's HOME find_disk_templates(State) -> OtherTemplates = find_other_templates(State), HomeFiles = rebar_utils:find_files(rebar_dir:template_dir(State), ?TEMPLATE_RE, true), % recursive [{file, F} || F <- OtherTemplates ++ HomeFiles]. %% Fetch template indexes that sit on disk in custom areas find_other_templates(State) -> case rebar_state:get(State, template_dir, undefined) of undefined -> []; TemplateDir -> rebar_utils:find_files(TemplateDir, ?TEMPLATE_RE, true) % recursive end. %% Fetch template indexes that sit on disk in plugins find_plugin_templates(State) -> [{plugin, File} || App <- rebar_state:all_plugin_deps(State), Priv <- [rebar_app_info:priv_dir(App)], Priv =/= undefined, File <- rebar_utils:find_files(Priv, ?TEMPLATE_RE)] ++ %% and add global plugins too [{plugin, File} || PSource <- rebar_state:get(State, {plugins, global}, []), Plugin <- [plugin_provider(PSource)], is_atom(Plugin), Priv <- [code:priv_dir(Plugin)], Priv =/= undefined, File <- rebar_utils:find_files(Priv, ?TEMPLATE_RE)]. plugin_provider(P) when is_atom(P) -> P; plugin_provider(T) when is_tuple(T) -> element(1, T). %% Take an existing list of templates and tag them by name the way %% the user would enter it from the CLI tag_names(List) -> [{filename:basename(File, ".template"), Type, File} || {Type, File} <- List]. %% If multiple templates share the same name, those in the escript (built-in) %% take precedence. Otherwise, the on-disk order is the one to win. prioritize_templates([], Acc) -> Acc; prioritize_templates([{Name, Type, File} | Rest], Valid) -> case lists:keyfind(Name, 1, Valid) of false -> prioritize_templates(Rest, [{Name, Type, File} | Valid]); {_, escript, _} -> ?DEBUG("Skipping template ~p, due to presence of a built-in " "template with the same name", [Name]), prioritize_templates(Rest, Valid); {_, builtin, _} -> ?DEBUG("Skipping template ~p, due to presence of a built-in " "template with the same name", [Name]), prioritize_templates(Rest, Valid); {_, plugin, _} -> ?DEBUG("Skipping template ~p, due to presence of a plugin " "template with the same name", [Name]), prioritize_templates(Rest, Valid); {_, file, _} -> ?DEBUG("Skipping template ~p, due to presence of a custom " "template at ~ts", [Name, File]), prioritize_templates(Rest, Valid) end. %% Read the contents of a file from the appropriate source load_file(Files, escript, Name) -> {Name, Bin} = lists:keyfind(Name, 1, Files), Bin; load_file(_Files, builtin, Name) -> {ok, Bin} = file:read_file(Name), Bin; load_file(_Files, plugin, Name) -> {ok, Bin} = file:read_file(Name), Bin; load_file(_Files, file, Name) -> case file:read_file(Name) of {ok, Bin} -> Bin; {error, Reason} -> ?ABORT("Failed to load file ~p: ~p\n", [Name, Reason]) end. write_file(Output, Data, Force) -> %% determine if the target file already exists FileExists = filelib:is_regular(Output), %% perform the function if we're allowed, %% otherwise just process the next template case Force orelse FileExists =:= false of true -> ok = filelib:ensure_dir(Output), case {Force, FileExists} of {true, true} -> ?INFO("Writing ~ts (forcibly overwriting)", [Output]); _ -> ?INFO("Writing ~ts", [Output]) end, case file:write_file(Output, Data) of ok -> ok; {error, Reason} -> ?ABORT("Failed to write output file ~p: ~p\n", [Output, Reason]) end; false -> {error, exists} end. %% Render a binary to a string, using mustache and the specified context render(Bin, Context) -> bbmustache:render( rebar_utils:to_binary(Bin), Context, [{key_type, atom}, {escape_fun, fun(X) -> X end}] % disable HTML-style escaping ). consult_template(Files, Type, File) -> TemplateBin = load_file(Files, Type, File), Encoding = case epp:read_encoding_from_binary(TemplateBin) of none -> epp:default_encoding(); X -> X end, rebar_string:consult(unicode:characters_to_list(TemplateBin, Encoding)).