diff options
Diffstat (limited to 'src/rebar_templater.erl')
-rw-r--r-- | src/rebar_templater.erl | 489 |
1 files changed, 200 insertions, 289 deletions
diff --git a/src/rebar_templater.erl b/src/rebar_templater.erl index f5b1bd5..40f3165 100644 --- a/src/rebar_templater.erl +++ b/src/rebar_templater.erl @@ -26,9 +26,8 @@ %% ------------------------------------------------------------------- -module(rebar_templater). --export([new/3, - list_templates/1, - create/1]). +-export([new/4, + list_templates/1]). %% API for other utilities that need templating functionality -export([resolve_variables/2, @@ -43,37 +42,24 @@ %% Public API %% =================================================================== -new(app, DirName, State) -> - create1(State, DirName, "otp_app"); -new(lib, DirName, State) -> - create1(State, DirName, "otp_lib"); -new(plugin, DirName, State) -> - create1(State, DirName, "plugin"); -new(rel, DirName, State) -> - create1(State, DirName, "otp_rel"). +%% Apply a template +new(Template, Vars, Force, State) -> + {AvailTemplates, Files} = find_templates(State), + ?DEBUG("Looking for ~p~n", [Template]), + case lists:keyfind(Template, 1, AvailTemplates) of + false -> {not_found, Template}; + TemplateTup -> create(TemplateTup, Files, Vars, Force) + end. +%% Give a list of templates with their expanded content list_templates(State) -> {AvailTemplates, Files} = find_templates(State), - ?DEBUG("Available templates: ~p\n", [AvailTemplates]), + [list_template(Files, Template) || Template <- AvailTemplates]. - lists:foreach( - fun({Type, F}) -> - BaseName = filename:basename(F, ".template"), - TemplateTerms = consult(load_file(Files, Type, F)), - {_, VarList} = lists:keyfind(variables, 1, TemplateTerms), - Vars = lists:foldl(fun({V,_}, Acc) -> - [atom_to_list(V) | Acc] - end, [], VarList), - ?INFO(" * ~s: ~s (~p) (variables: ~p)\n", - [BaseName, F, Type, string:join(Vars, ", ")]) - end, AvailTemplates), - ok. - -create(State) -> - TemplateId = template_id(State), - create1(State, "", TemplateId). +%% =================================================================== +%% Rendering API / legacy? +%% =================================================================== -%% %% Given a list of key value pairs, for each string value attempt to %% render it using Dict as the context. Storing the result in Dict as Key. %% @@ -97,96 +83,176 @@ render(Template, Context) -> Module = list_to_atom(Template++"_dtl"), Module:render(Context). + %% =================================================================== -%% Internal functions +%% Internal Functions %% =================================================================== -create1(State, AppDir, TemplateId) -> - ec_file:mkdir_p(AppDir), - file:set_cwd(AppDir), - {AvailTemplates, Files} = find_templates(State), - ?DEBUG("Available templates: ~p\n", [AvailTemplates]), +%% Expand a single template's value +list_template(Files, {Name, Type, File}) -> + TemplateTerms = consult(load_file(Files, Type, File)), + {Name, Type, File, + get_template_description(TemplateTerms), + get_template_vars(TemplateTerms)}. + +%% 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. - %% Using the specified template id, find the matching template file/type. - %% Note that if you define the same template in both ~/.rebar/templates - %% that is also present in the escript, the one on the file system will - %% be preferred. - {Type, Template} = select_template(AvailTemplates, TemplateId), - - %% Load the template definition as is and get the list of variables the - %% template requires. - Context0 = dict:from_list([{appid, AppDir}]), - TemplateTerms = consult(load_file(Files, Type, Template)), - case lists:keyfind(variables, 1, TemplateTerms) of - {variables, Vars} -> - case parse_vars(Vars, Context0) of - {error, Entry} -> - Context1 = undefined, - ?ABORT("Failed while processing variables from template ~p." - "Variable definitions must follow form of " - "[{atom(), term()}]. Failed at: ~p\n", - [TemplateId, Entry]); - Context1 -> - ok - end; - false -> - ?WARN("No variables section found in template ~p; " - "using empty context.\n", [TemplateId]), - Context1 = Context0 +%% 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) -> + Vars = case lists:keyfind(variables, 1, TemplateTerms) of + {_, Value} -> Value; + false -> [] end, + override_vars(Vars, override_vars(global_variables(), 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() -> + {{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, "Anonymous"}, + {author_email, "anonymous@example.org"}, + {copyright_year, integer_to_list(Y)}, + {apps_dir, "apps/", "Directory where applications will be created if needed"}]. + +%% Load variable definitions from the 'Globals' file in the home template +%% directory +global_variables() -> + {ok, [[Home]]} = init:get_argument(home), + GlobalFile = filename:join([Home, ?HOME_DIR, "templates", "globals"]), + case file:consult(GlobalFile) of + {error, enoent} -> []; + {ok, Data} -> proplists:get_value(variables, Data, []) + end. - %% Load variables from disk file, if provided - Context2 = case rebar_state:get(State, template_vars, undefined) of - undefined -> - Context1; - File -> - case consult(load_file([], file, File)) of - {error, Reason} -> - ?ABORT("Unable to load template_vars from ~s: ~p\n", - [File, Reason]); - Terms -> - %% TODO: Cleanup/merge with similar code in rebar_reltool - M = fun(_Key, _Base, Override) -> Override end, - dict:merge(M, Context1, dict:from_list(Terms)) - end - end, - - %% For each variable, see if it's defined in global vars -- if it is, - %% prefer that value over the defaults - Context3 = update_vars(State, dict:fetch_keys(Context2), Context1), - ?DEBUG("Template ~p context: ~p\n", [TemplateId, dict:to_list(Context2)]), - - %% Handle variables that possibly include other variables in their - %% definition - %Context = resolve_variables(dict:to_list(Context3), Context3), - - %?DEBUG("Resolved Template ~p context: ~p\n", - %[TemplateId, dict:to_list(Context)]), - - %% Now, use our context to process the template definition -- this - %% permits us to use variables within the definition for filenames. - %FinalTemplate = consult(render(load_file(Files, Type, Template), Context)), - %?DEBUG("Final template def ~p: ~p\n", [TemplateId, FinalTemplate]), - - %% Execute the instructions in the finalized template - Force = rebar_state:get(State, force, "0"), - execute_template([], TemplateTerms, Type, TemplateId, Context3, Force, []). - +%% 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) -> + TemplateTerms = consult(load_file(Files, Type, File)), + Vars = drop_var_docs(override_vars(UserVars, get_template_vars(TemplateTerms))), + TemplateCwd = filename:dirname(File), + execute_template(TemplateTerms, Files, {Template, Type, TemplateCwd}, Vars, Force). + +%% Run template instructions one at a time. +execute_template([], _, {Template,_,_}, _, _) -> + ?DEBUG("Template ~s applied~n", [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); +%% Create a directory +execute_template([{dir, Path} | Terms], Files, Template, Vars, Force) -> + ?DEBUG("Creating directory ~p~n", [Path]), + case ec_file:mkdir_p(expand_path(Path, Vars)) of + ok -> + ok; + {error, Reason} -> + ?ABORT("Failed while processing template instruction " + "{dir, ~p}: ~p~n", [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~n", [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~n", [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.~n", [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~n", [From]), + Out = expand_path(To, Vars), + Tpl = load_file(Files, Type, filename:join(Cwd, From)), + TplName = make_template_name("rebar_template", Out), + {ok, Mod} = erlydtl:compile_template(Tpl, TplName, ?ERLYDTL_COMPILE_OPTS), + {ok, Output} = Mod:render(Vars), + case write_file(Out, Output, Force) of + ok -> ok; + {error, exists} -> ?INFO("File ~p already exists~n", [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 ~s", + [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) -> - %% Load a list of all the files in the escript -- cache them since - %% we'll potentially need to walk it several times over the course of - %% a run. + %% Cache the files since we'll potentially need to walk it several times + %% over the course of a run. Files = cache_escript_files(State), %% Build a list of available templates - AvailTemplates = find_disk_templates(State) - ++ find_escript_templates(Files), + AvailTemplates = prioritize_templates( + tag_names(find_disk_templates(State)), + tag_names(find_escript_templates(Files))), + ?DEBUG("Available templates: ~p\n", [AvailTemplates]), {AvailTemplates, Files}. -%% %% Scan the current escript for available files -%% cache_escript_files(State) -> {ok, Files} = rebar_utils:escript_foldl( fun(Name, _, GetBin, Acc) -> @@ -195,27 +261,22 @@ cache_escript_files(State) -> [], rebar_state:get(State, escript)), Files. -template_id(State) -> - case rebar_state:get(State, template, undefined) of - undefined -> - ?ABORT("No template specified.\n", []); - TemplateId -> - TemplateId - end. - +%% 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}]) == match]. +%% 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(filename:join([os:getenv("HOME"), - ".rebar", "templates"]), + {ok, [[Home]]} = init:get_argument(home), + HomeFiles = rebar_utils:find_files(filename:join([Home, ?HOME_DIR, "templates"]), ?TEMPLATE_RE), LocalFiles = rebar_utils:find_files(".", ?TEMPLATE_RE, true), [{file, F} || F <- OtherTemplates ++ HomeFiles ++ LocalFiles]. +%% Fetch template indexes that sit on disk in custom areas find_other_templates(State) -> case rebar_state:get(State, template_dir, undefined) of undefined -> @@ -224,19 +285,31 @@ find_other_templates(State) -> rebar_utils:find_files(TemplateDir, ?TEMPLATE_RE) end. -select_template([], Template) -> - ?ABORT("Template ~s not found.\n", [Template]); -select_template([{Type, Avail} | Rest], Template) -> - case filename:basename(Avail, ".template") == Template of - true -> - {Type, Avail}; +%% 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 -> - select_template(Rest, Template) + prioritize_templates(Rest, [{Name, Type, File} | Valid]); + {_, escript, _} -> + ?DEBUG("Skipping template ~p, due to presence of a built-in " + "template with the same name~n", [Name]), + prioritize_templates(Rest, Valid); + {_, file, _} -> + ?DEBUG("Skipping template ~p, due to presence of a custom " + "template at ~s~n", [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; @@ -244,32 +317,7 @@ load_file(_Files, file, Name) -> {ok, Bin} = file:read_file(Name), Bin. -%% -%% Parse/validate variables out from the template definition -%% -parse_vars([], Dict) -> - Dict; -parse_vars([{Key, Value} | Rest], Dict) when is_atom(Key) -> - parse_vars(Rest, dict:store(Key, Value, Dict)); -parse_vars([Other | _Rest], _Dict) -> - {error, Other}; -parse_vars(Other, _Dict) -> - {error, Other}. - -%% -%% Given a list of keys in Dict, see if there is a corresponding value defined -%% in the global config; if there is, update the key in Dict with it -%% -update_vars(_State, [], Dict) -> - Dict; -update_vars(State, [Key | Rest], Dict) -> - Value = rebar_state:get(State, Key, dict:fetch(Key, Dict)), - update_vars(State, Rest, dict:store(Key, Value, Dict)). - - -%% %% Given a string or binary, parse it into a list of terms, ala file:consult/1 -%% consult(Str) when is_list(Str) -> consult([], Str, []); consult(Bin) when is_binary(Bin)-> @@ -281,7 +329,7 @@ consult(Cont, Str, Acc) -> case Result of {ok, Tokens, _} -> {ok, Term} = erl_parse:parse_term(Tokens), - consult([], Remaining, [maybe_dict(Term) | Acc]); + consult([], Remaining, [Term | Acc]); {eof, _Other} -> lists:reverse(Acc); {error, Info, _} -> @@ -292,24 +340,17 @@ consult(Cont, Str, Acc) -> end. -maybe_dict({Key, {list, Dicts}}) -> - %% this is a 'list' element; a list of lists representing dicts - {Key, {list, [dict:from_list(D) || D <- Dicts]}}; -maybe_dict(Term) -> - Term. - - 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 =:= "1" orelse FileExists =:= false of + case Force orelse FileExists =:= false of true -> ok = filelib:ensure_dir(Output), case {Force, FileExists} of - {"1", true} -> + {true, true} -> ?INFO("Writing ~s (forcibly overwriting)", [Output]); _ -> @@ -326,136 +367,6 @@ write_file(Output, Data, Force) -> {error, exists} end. -prepend_instructions(Instructions, Rest) when is_list(Instructions) -> - Instructions ++ Rest; -prepend_instructions(Instruction, Rest) -> - [Instruction|Rest]. - -%% -%% Execute each instruction in a template definition file. -%% -execute_template(_Files, [], _TemplateType, _TemplateName, - _Context, _Force, ExistingFiles) -> - case ExistingFiles of - [] -> - ok; - _ -> - Msg = lists:flatten([io_lib:format("\t* ~p~n", [F]) || - F <- lists:reverse(ExistingFiles)]), - Help = "To force overwriting, specify -f/--force/force=1" - " on the command line.\n", - ?ERROR("One or more files already exist on disk and " - "were not generated:~n~s~s", [Msg , Help]) - end; -execute_template(Files, [{'if', Cond, True} | Rest], TemplateType, - TemplateName, Context, Force, ExistingFiles) -> - execute_template(Files, [{'if', Cond, True, []}|Rest], TemplateType, - TemplateName, Context, Force, ExistingFiles); -execute_template(Files, [{'if', Cond, True, False} | Rest], TemplateType, - TemplateName, Context, Force, ExistingFiles) -> - Instructions = case dict:find(Cond, Context) of - {ok, true} -> - True; - {ok, "true"} -> - True; - _ -> - False - end, - execute_template(Files, prepend_instructions(Instructions, Rest), - TemplateType, TemplateName, Context, Force, - ExistingFiles); -execute_template(Files, [{'case', Variable, Values, Instructions} | Rest], TemplateType, - TemplateName, Context, Force, ExistingFiles) -> - {ok, Value} = dict:find(Variable, Context), - Instructions2 = case lists:member(Value, Values) of - true -> - Instructions; - _ -> - [] - end, - execute_template(Files, prepend_instructions(Instructions2, Rest), - TemplateType, TemplateName, Context, Force, - ExistingFiles); -execute_template(Files, [{template, Input, Output} | Rest], TemplateType, - TemplateName, Context, Force, ExistingFiles) -> - _InputName = filename:join(filename:dirname(TemplateName), Input), - %File = load_file(Files, TemplateType, InputName), - OutputTemplateName = make_template_name("rebar_output_template", Output), - {ok, OutputTemplateName1} = erlydtl:compile_template(Output, OutputTemplateName, ?ERLYDTL_COMPILE_OPTS), - {ok, OutputRendered} = OutputTemplateName1:render(dict:to_list(Context)), - {ok, Rendered} = render(Input, dict:to_list(Context)), - case write_file(lists:flatten(io_lib:format("~s", [OutputRendered])), Rendered, Force) of - ok -> - execute_template(Files, Rest, TemplateType, TemplateName, - Context, Force, ExistingFiles); - {error, exists} -> - execute_template(Files, Rest, TemplateType, TemplateName, - Context, Force, [Output|ExistingFiles]) - end; -execute_template(Files, [{file, Input, Output} | Rest], TemplateType, - TemplateName, Context, Force, ExistingFiles) -> - InputName = filename:join(filename:dirname(TemplateName), Input), - File = load_file(Files, TemplateType, InputName), - case write_file(Output, File, Force) of - ok -> - execute_template(Files, Rest, TemplateType, TemplateName, - Context, Force, ExistingFiles); - {error, exists} -> - execute_template(Files, Rest, TemplateType, TemplateName, - Context, Force, [Output|ExistingFiles]) - end; -execute_template(Files, [{dir, Name} | Rest], TemplateType, - TemplateName, Context, Force, ExistingFiles) -> - case filelib:ensure_dir(filename:join(Name, "dummy")) of - ok -> - execute_template(Files, Rest, TemplateType, TemplateName, - Context, Force, ExistingFiles); - {error, Reason} -> - ?ABORT("Failed while processing template instruction " - "{dir, ~s}: ~p\n", [Name, Reason]) - end; -execute_template(Files, [{copy, Input, Output} | Rest], TemplateType, - TemplateName, Context, Force, ExistingFiles) -> - InputName = filename:join(filename:dirname(TemplateName), Input), - try rebar_file_utils:cp_r([InputName ++ "/*"], Output) of - ok -> - execute_template(Files, Rest, TemplateType, TemplateName, - Context, Force, ExistingFiles) - catch _:_ -> - ?ABORT("Failed while processing template instruction " - "{copy, ~s, ~s}", [Input, Output]) - end; -execute_template(Files, [{chmod, Mod, File} | Rest], TemplateType, - TemplateName, Context, Force, ExistingFiles) - when is_integer(Mod) -> - case file:change_mode(File, Mod) of - ok -> - execute_template(Files, Rest, TemplateType, TemplateName, - Context, Force, ExistingFiles); - {error, Reason} -> - ?ABORT("Failed while processing template instruction " - "{chmod, ~b, ~s}: ~p", [Mod, File, Reason]) - end; -execute_template(Files, [{symlink, Existing, New} | Rest], TemplateType, - TemplateName, Context, Force, ExistingFiles) -> - case file:make_symlink(Existing, New) of - ok -> - execute_template(Files, Rest, TemplateType, TemplateName, - Context, Force, ExistingFiles); - {error, Reason} -> - ?ABORT("Failed while processing template instruction " - "{symlink, ~s, ~s}: ~p", [Existing, New, Reason]) - end; -execute_template(Files, [{variables, _} | Rest], TemplateType, - TemplateName, Context, Force, ExistingFiles) -> - execute_template(Files, Rest, TemplateType, TemplateName, - Context, Force, ExistingFiles); -execute_template(Files, [Other | Rest], TemplateType, TemplateName, - Context, Force, ExistingFiles) -> - ?WARN("Skipping unknown template instruction: ~p\n", [Other]), - execute_template(Files, Rest, TemplateType, TemplateName, Context, - Force, ExistingFiles). - -spec make_template_name(string(), term()) -> module(). make_template_name(Base, Value) -> %% Seed so we get different values each time |