- %% -*- 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]).
-
-
- -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 rebar_string:consult(binary_to_list(load_file(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 overriden 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 = rebar_string:consult(binary_to_list(load_file(Files, Type, File))),
- Vars = drop_var_docs(override_vars(UserVars, get_template_vars(TemplateTerms, State))),
- 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_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.
-
- 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]),
- Data = load_file(Files, Type, filename:join(Cwd, From)),
- 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]),
- Out = expand_path(To, Vars),
- Tpl = load_file(Files, Type, filename:join(Cwd, From)),
- 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) ->
- {ok, Bin} = file:read_file(Name),
- Bin.
-
- 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
- ).
|