diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/rebar_templater.erl | 194 |
1 files changed, 186 insertions, 8 deletions
diff --git a/src/rebar_templater.erl b/src/rebar_templater.erl index 3c7c05f..329df7c 100644 --- a/src/rebar_templater.erl +++ b/src/rebar_templater.erl @@ -26,16 +26,71 @@ %% ------------------------------------------------------------------- -module(rebar_templater). --export([create/2]). +-export(['create-app'/2, + create/2]). -include("rebar.hrl"). +-define(TEMPLATE_RE, ".*\\.template\$"). + %% =================================================================== %% Public API %% =================================================================== +'create-app'(Config, File) -> + %% Alias for create w/ template=simpleapp + rebar_config:set_global(template, "simpleapp"), + create(Config, File). + create(_Config, _) -> - ok. + %% Load a list of all the files in the escript -- cache it in the pdict + %% since we'll potentially need to walk it several times over the course + %% of a run. + cache_escript_files(), + + %% Build a list of available templates + AvailTemplates = find_disk_templates() ++ find_escript_templates(), + ?DEBUG("Available templates: ~p\n", [AvailTemplates]), + + %% 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, template_id()), + + %% Load the template definition as is and get the list of variables the + %% template requires. + TemplateTerms = consult(load_file(Type, Template)), + case lists:keysearch(variables, 1, TemplateTerms) of + {value, {variables, Vars}} -> + case parse_vars(Vars, dict:new()) of + {error, Entry} -> + Context0 = undefined, + ?ABORT("Failed while processing variables from template ~p. Variable definitions " + "must follow form of [{atom(), term()}]. Failed at: ~p\n", + [template_id(), Entry]); + Context0 -> + ok + end; + false -> + ?WARN("No variables section found in template ~p; using empty context.\n", + [template_id()]), + Context0 = dict:new() + end, + + %% For each variable, see if it's defined in global vars -- if it is, prefer that + %% value over the defaults + Context = update_vars(dict:fetch_keys(Context0), Context0), + ?DEBUG("Template ~p context: ~p\n", [template_id(), 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(Type, Template), Context)), + ?DEBUG("Final template def ~p: ~p\n", [template_id(), FinalTemplate]), + + %% Execute the instructions in the finalized template + execute_template(FinalTemplate, Type, Template, Context). + @@ -43,9 +98,132 @@ create(_Config, _) -> %% Internal functions %% =================================================================== -% execute_template([]) -> -% ok; -% execute_template([{file, Input, Output} | Rest]) -> -% ok; -% execute_template([{dir, Name} | Rest]) -> -% ok. +%% +%% Scan the current escript for available files and cache in pdict. +%% +cache_escript_files() -> + {ok, Files} = escript:foldl(fun(Name, _, GetBin, Acc) -> [{Name, GetBin()} | Acc] end, + [], escript:script_name()), + erlang:put(escript_files, Files). + + +template_id() -> + case rebar_config:get_global(template, undefined) of + undefined -> + ?ABORT("No template specified.\n", []); + TemplateId -> + TemplateId + end. + +find_escript_templates() -> + [{escript, Name} || {Name, _Bin} <- erlang:get(escript_files), + re:run(Name, ?TEMPLATE_RE, [{capture, none}]) == match]. + +find_disk_templates() -> + Files = rebar_utils:find_files(filename:join(os:getenv("HOME"), ".rebar/templates"), ?TEMPLATE_RE), + [{file, F} || F <- Files]. + +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}; + false -> + select_template(Rest, Template) + end. + +%% +%% Read the contents of a file from the appropriate source +%% +load_file(escript, Name) -> + {Name, Bin} = lists:keyfind(Name, 1, erlang:get(escript_files)), + Bin; +load_file(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([], Dict) -> + Dict; +update_vars([Key | Rest], Dict) -> + Value = rebar_config:get_global(Key, dict:fetch(Key, Dict)), + update_vars(Rest, dict:store(Key, Value, Dict)). + + +%% +%% Given a string or binary, parse it into a list of terms, ala file:consult/0 +%% +consult(Str) when is_list(Str) -> + consult([], Str, []); +consult(Bin) when is_binary(Bin)-> + consult([], binary_to_list(Bin), []). + +consult(Cont, Str, Acc) -> + case erl_scan:tokens(Cont, Str, 0) of + {done, Result, Remaining} -> + case Result of + {ok, Tokens, _} -> + {ok, Term} = erl_parse:parse_term(Tokens), + consult([], Remaining, [Term | Acc]); + {eof, _Other} -> + lists:reverse(Acc); + {error, Info, _} -> + {error, Info} + end; + {more, Cont1} -> + consult(Cont1, eof, Acc) + end. + + +%% +%% Render a binary to a string, using mustache and the specified context +%% +render(Bin, Context) -> + %% Be sure to escape any double-quotes before rendering... + Str = re:replace(Bin, "\"", "\\\\\"", [global, {return,list}]), + mustache:render(Str, Context). + +%% +%% Execute each instruction in a template definition file. +%% +execute_template([], _TemplateType, _TemplateName, _Context) -> + ok; +execute_template([{file, Input, Output} | Rest], TemplateType, TemplateName, Context) -> + InputName = filename:join(filename:dirname(TemplateName), Input), + filelib:ensure_dir(Output), + case file:write_file(Output, render(load_file(TemplateType, InputName), Context)) of + ok -> + execute_template(Rest, TemplateType, TemplateName, Context); + {error, Reason} -> + ?ABORT("Failed to write output file ~p: ~p\n", [Output, Reason]) + end; +execute_template([{dir, Name} | Rest], TemplateType, TemplateName, Context) -> + case filelib:ensure_dir(filename:join(Name, "dummy")) of + ok -> + execute_template(Rest, TemplateType, TemplateName, Context); + {error, Reason} -> + ?ABORT("Failed while processing template instruction {dir, ~s}: ~p\n", + [Name, Reason]) + end; +execute_template([{variables, _} | Rest], TemplateType, TemplateName, Context) -> + execute_template(Rest, TemplateType, TemplateName, Context); +execute_template([Other | Rest], TemplateType, TemplateName, Context) -> + ?WARN("Skipping unknown template instruction: ~p\n", [Other]), + execute_template(Rest, TemplateType, TemplateName, Context). + |