From c832b567dba73ddbed961b265f2018b3f2cb45ae Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Mon, 10 Nov 2014 19:03:25 +0000 Subject: Redo templates with docs and whatnot This totally breaks compatibility with rebar2, and maybe it shouldn't have. --- doc/templates.md | 182 ++++++++++++++ include/rebar.hrl | 1 + priv/templates/LICENSE.dtl | 2 +- priv/templates/README.md.dtl | 2 +- priv/templates/app.template | 11 + priv/templates/gitignore.dtl | 6 +- priv/templates/lib.template | 11 + priv/templates/mod.erl.dtl | 4 +- priv/templates/otp_app.app.src.dtl | 3 +- priv/templates/otp_app.template | 7 - priv/templates/otp_lib.app.src.dtl | 7 +- priv/templates/otp_lib.template | 7 - priv/templates/otp_rel.template | 11 - priv/templates/plugin.erl.dtl | 41 +-- priv/templates/plugin.template | 18 +- priv/templates/plugin_README.md.dtl | 15 +- priv/templates/release.template | 15 ++ src/rebar_prv_new.erl | 63 ++++- src/rebar_templater.erl | 483 +++++++++++++++--------------------- 19 files changed, 535 insertions(+), 354 deletions(-) create mode 100644 doc/templates.md create mode 100644 priv/templates/app.template create mode 100644 priv/templates/lib.template delete mode 100644 priv/templates/otp_app.template delete mode 100644 priv/templates/otp_lib.template delete mode 100644 priv/templates/otp_rel.template create mode 100644 priv/templates/release.template diff --git a/doc/templates.md b/doc/templates.md new file mode 100644 index 0000000..6dd9907 --- /dev/null +++ b/doc/templates.md @@ -0,0 +1,182 @@ +# Templates # + +- [Default Variables](#default-variables) +- [Global Variables](#global-variables) +- [Batteries-Included Templates](#batteries-included-templates) +- [Custom Templates](#custom-templates) + +## Default Variables + +- `date`: defaults to today's date, under universal time, printed according to RFC 8601 (for example, `"2014-03-11"`) +- `datetime`: defaults to today's date and time, under universal time, printed according to RFC 8601 (for example, `"2014-03-11T16:06:02+00:00"`). +- `author_name`: Defaults to `"Anonymous"` +- `author_email`: Defaults to `"anonymous@example.org"` +- `apps_dir`: Directory where OTP applications should be created in release projects. Defaults to `"apps/"`. +- `copyright_year`: Defaults to the current year, under universal time. + + +## Global Variables + +Global variables can be set by editing the file at `$HOME/.rebar3/templates/globals`: + + {variables, [ + {author_name, "My Name Is A String"}, + {copyright_year, "2014-2022", "The year or range of years for copyright"}, + {my_custom_var, "hello there"} + ]}. + +This will let you define variables for all templates. + +Variables left undefined will be ignored and revert to the default value. + +The override order for these variables will be: Defaults < $HOME/.rebar3/templates/globals < command line invocation. + +## Batteries-Included Templates ## + +Rebar3 ships with a few templates installed, which can be listed by calling `rebar3 new`: + + → ./rebar3 new + app (built-in): OTP Application + lib (built-in): OTP Library application (no processes) + release (built-in): OTP Release structure for executable programs + plugin (built-in): Rebar3 plugin + +Any custom plugins would be followed as ` (custom): `. + +Details for each individual plugin can be obtained by calling `rebar3 new help `: + + → ./rebar3 new help plugin + plugin: + built-in template + Description: Rebar3 plugin + Variables: + appid="myplugin" (Name of the plugin) + desc="A rebar plugin" (Short description of the plugin's purpose) + date="2014-11-10" + datetime="2014-11-10T18:29:41+00:00" + author_name="Anonymous" + author_email="anonymous@example.org" + copyright_year="2014" + apps_dir="apps/" (Directory where applications will be created if needed) + +All the variables there have their default values shown, and an optional explanation in parentheses. + +The variables can also be [overriden globally](#global-variables). + +## Custom Templates ## + +Custom templates can be added in `$HOME/.rebar3/templates/`. Each template is at least two files: + +- `my_template.dtl`: There can be many of these files. They are regular Erlang files using the django template syntax for variable replacements. +- `my_template.template`; Called the *template index*, there is one per template callable from `rebar3`. This one will be visible when calling `rebar3 new my_template`. This file regroups the different \*.dtl files into a more cohesive template. + +### File Syntax ### + +#### Template Index #### + +The following options are available: + + {description, "This template does a thing"}. + {variables, [ + {var1, "default value"}, + {var2, "default", "explain what this does in help files"}, + {app_dir, ".", "The directory where the application goes"} + ]}. + {dir, "{{appdir}}/src"}. + {file, "mytemplate_README", "README"}. + {chmod, "README", 8#644}. + {template, "myapp/myapp.app.src.dtl", "{{appdir}}/src/{{name}}.app.src"}. + +Specifically: + +- `description`: takes a string explaining what the template is for. +- `variables`: takes a list of variables in two forms: + - `{Name, DefaultString, HelpString}`; + - `{Name, DefaultString}`. +- `{dir, TemplatablePathString}`: creates a given directory. Variable names can be used in the path name. +- `{file, FilePath, DestFilePath}`: copies a file literally to its destination. +- `{template, DtlFilePath, TemplatablePathString}`: evaluates a given template. The `DtlFilePath` is relative to the template index. +- `{chmod, FilePath, Int}`: changes the permission of a file, using the integer value specified. Octal values can be entered by doing `8#640`. + +### Example ### + +As an example, we'll create a template for Common Test test suites. Create the directory structure `~/.rebar/templates/` and then go in there. + +We'll start with an index for our template, called `ct_suite.template`: + +```erlang +{description, "A basic Common Test suite for an OTP application"}. +{variables, [ + {suite, "suite", "Name of the suite, prepended to the standard _SUITE suffix"} +]}. + +{dir, "test"}. +{template, "ct_suite.erl.dtl", "test/{{suite}}_SUITE.erl"}. +``` + +This tells rebar3 to create the test directory and to evaluate an [ErlyDTL](https://github.com/erlydtl/erlydtl) template. All the paths are relative to the current working directory. + +Let's create the template file: + +```erlang +-module({{suite}}_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). % Eunit macros for convenience + +-export([all/0 + ,groups/0 + %,init_per_suite/1, end_per_suite/1 + %,init_per_group/2, end_per_group/2 + ,init_per_testcase/2, end_per_testcase/2 + ]). + +-export([fail/1]). + +all() -> [fail]. + +groups() -> []. + +init_per_testcase(_Name, Config) -> Config. + +end_per_testcase(_Name, _Config) -> ok. + +fail(_Config) -> + ?assert(false). +``` + +This one does very simple variable substitution for the name (using `{{suite}}`) and that's all it needs. + +Let's get to any existing project you have and try it: + + → ./rebar3 new + app (built-in): OTP Application + ct_suite (custom): A basic Common Test suite for an OTP application + lib (built-in): OTP Library application (no processes) + release (built-in): OTP Release structure for executable programs + plugin (built-in): Rebar3 plugin + +The first line shows that our `ct_suite` temlate has been detected and is usable. +Let's look at the details: + + → ./rebar3 new help ct_suite + ct_suite: + custom template (/home/ferd/.rebar3/templates/ct_suite.template) + Description: A basic Common Test suite for an OTP application + Variables: + suite="suite" (Name of the suite, prepended to the standard _SUITE suffix) + date="2014-11-10" + datetime="2014-11-10T18:46:33+00:00" + author_name="Anonymous" + author_email="anonymous@example.org" + copyright_year="2014" + apps_dir="apps/" (Directory where applications will be created if needed) + +The documentation from variables and the description are well in place. To apply the template, go to any of your OTP application's top-level directory: + + → ./rebar3 new ct_suite suite=demo + ===> Writing test/demo_SUITE.erl + +And you will see the code in place. + +~ diff --git a/include/rebar.hrl b/include/rebar.hrl index 78a3f02..10e21d4 100644 --- a/include/rebar.hrl +++ b/include/rebar.hrl @@ -20,6 +20,7 @@ -define(DEFAULT_TEST_DEPS_DIR, "_tdeps"). -define(DEFAULT_CONFIG_FILE, "rebar.config"). -define(LOCK_FILE, "rebar.lock"). +-define(HOME_DIR, ".rebar3"). -ifdef(namespaced_types). -type rebar_dict() :: dict:dict(). diff --git a/priv/templates/LICENSE.dtl b/priv/templates/LICENSE.dtl index 5ce77ce..41588ab 100644 --- a/priv/templates/LICENSE.dtl +++ b/priv/templates/LICENSE.dtl @@ -1,4 +1,4 @@ -Copyright (c) {{copyright_year}}, {{copyright_holder}} <{{author_email}}>. +Copyright (c) {{copyright_year}}, {{author_name}} <{{author_email}}>. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/priv/templates/README.md.dtl b/priv/templates/README.md.dtl index b2435a8..900fedb 100644 --- a/priv/templates/README.md.dtl +++ b/priv/templates/README.md.dtl @@ -1,7 +1,7 @@ {{appid}} ===== -An Erlang {{appid}} library. +{{desc}} Build ----- diff --git a/priv/templates/app.template b/priv/templates/app.template new file mode 100644 index 0000000..39ec14a --- /dev/null +++ b/priv/templates/app.template @@ -0,0 +1,11 @@ +{description, "OTP Application"}. +{variables, [ + {appid, "mylib", "Name of the OTP application"}, + {desc, "An OTP application", "Short description of the app"} +]}. +{template, "app.erl.dtl", "src/{{appid}}_app.erl"}. +{template, "otp_app.app.src.dtl", "src/{{appid}}.app.src"}. +{template, "rebar.config.dtl", "rebar.config"}. +{template, "gitignore.dtl", ".gitignore"}. +{template, "LICENSE.dtl", "LICENSE"}. +{template, "README.md.dtl", "README.md"}. diff --git a/priv/templates/gitignore.dtl b/priv/templates/gitignore.dtl index 23123d4..9e09bf1 100644 --- a/priv/templates/gitignore.dtl +++ b/priv/templates/gitignore.dtl @@ -1,13 +1,17 @@ _* .eunit -deps *.o *.beam *.plt +*.swp +*.swo .erlang.cookie ebin log erl_crash.dump .rebar _rel +_deps +_plugins +_tdeps logs diff --git a/priv/templates/lib.template b/priv/templates/lib.template new file mode 100644 index 0000000..3f35945 --- /dev/null +++ b/priv/templates/lib.template @@ -0,0 +1,11 @@ +{description, "OTP Library application (no processes)"}. +{variables, [ + {appid, "mylib", "Name of the OTP library application"}, + {desc, "An OTP library", "Short description of the app"} +]}. +{template, "mod.erl.dtl", "src/{{appid}}.erl"}. +{template, "otp_lib.app.src.dtl", "src/{{appid}}.app.src"}. +{template, "rebar.config.dtl", "rebar.config"}. +{template, "gitignore.dtl", ".gitignore"}. +{template, "LICENSE.dtl", "LICENSE"}. +{template, "README.md.dtl", "README.md"}. diff --git a/priv/templates/mod.erl.dtl b/priv/templates/mod.erl.dtl index 1be8186..2453366 100644 --- a/priv/templates/mod.erl.dtl +++ b/priv/templates/mod.erl.dtl @@ -1,10 +1,10 @@ -module({{appid}}). -%% Application callbacks +%% API exports -export([]). %%==================================================================== -%% API +%% API functions %%==================================================================== diff --git a/priv/templates/otp_app.app.src.dtl b/priv/templates/otp_app.app.src.dtl index 0af909f..cd5ac89 100644 --- a/priv/templates/otp_app.app.src.dtl +++ b/priv/templates/otp_app.app.src.dtl @@ -1,6 +1,5 @@ {application, {{appid}}, - [ - {description, "{{appid}}"} + [{description, "{{desc}}"} ,{vsn, "0.1.0"} ,{registered, []} ,{mod, {'{{appid}}_app', []}} diff --git a/priv/templates/otp_app.template b/priv/templates/otp_app.template deleted file mode 100644 index db31b31..0000000 --- a/priv/templates/otp_app.template +++ /dev/null @@ -1,7 +0,0 @@ -{variables, []}. -{template, "app.erl", "src/{{appid}}_app.erl"}. -{template, "otp_app.app.src", "src/{{appid}}.app.src"}. -{template, "rebar.config", "rebar.config"}. -{template, "gitignore", ".gitignore"}. -{template, "LICENSE", "LICENSE"}. -{template, "README.md", "README.md"}. diff --git a/priv/templates/otp_lib.app.src.dtl b/priv/templates/otp_lib.app.src.dtl index 5192af7..53ebbb0 100644 --- a/priv/templates/otp_lib.app.src.dtl +++ b/priv/templates/otp_lib.app.src.dtl @@ -1,12 +1,9 @@ {application, {{appid}}, - [ - {description, "{{appid}}"} + [{description, "{{desc}}"} ,{vsn, "0.1.0"} ,{registered, []} ,{applications, - [kernel - ,stdlib - ]} + [kernel,stdlib]} ,{env,[]} ,{modules, []} ]}. diff --git a/priv/templates/otp_lib.template b/priv/templates/otp_lib.template deleted file mode 100644 index 19d7593..0000000 --- a/priv/templates/otp_lib.template +++ /dev/null @@ -1,7 +0,0 @@ -{variables, []}. -{template, "mod.erl", "src/{{appid}}.erl"}. -{template, "otp_lib.app.src", "src/{{appid}}.app.src"}. -{template, "rebar.config", "rebar.config"}. -{template, "gitignore", ".gitignore"}. -{template, "LICENSE", "LICENSE"}. -{template, "README.md", "README.md"}. diff --git a/priv/templates/otp_rel.template b/priv/templates/otp_rel.template deleted file mode 100644 index b75c1da..0000000 --- a/priv/templates/otp_rel.template +++ /dev/null @@ -1,11 +0,0 @@ -{variables, []}. -{template, "app.erl", "apps/{{appid}}/src/{{appid}}_app.erl"}. -{template, "sup.erl", "apps/{{appid}}/src/{{appid}}_sup.erl"}. -{template, "otp_app.app.src", "apps/{{appid}}/src/{{appid}}.app.src"}. -{template, "rebar.config", "rebar.config"}. -{template, "relx.config", "relx.config"}. -{template, "sys.config", "config/sys.config"}. -{template, "vm.args", "config/vm.args"}. -{template, "gitignore", ".gitignore"}. -{template, "LICENSE", "LICENSE"}. -{template, "README.md", "README.md"}. diff --git a/priv/templates/plugin.erl.dtl b/priv/templates/plugin.erl.dtl index 80a03bb..e51763b 100644 --- a/priv/templates/plugin.erl.dtl +++ b/priv/templates/plugin.erl.dtl @@ -1,29 +1,36 @@ -module({{appid}}). +-behaviour(provider). --behaviour(rebar_provider). +-export([init/1, do/1, format_error/2]). --export([init/1, - do/1]). +-include_lib("rebar3/include/rebar.hrl"). --define(PROVIDER, {{appid}}). --define(DEPS, []). +-define(PROVIDER, todo). +-define(DEPS, [app_discovery]). %% =================================================================== %% Public API %% =================================================================== - -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> - State1 = rebar_state:add_provider(State, rebar_provider:create([{name, ?PROVIDER}, - {provider_impl, ?MODULE}, - {bare, false}, - {deps, ?DEPS}, - {example, "rebar {{appid}}"}, - {short_desc, "{{appid}} plugin."}, - {desc, ""}, - {opts, []}])), - {ok, State1}. - --spec do(rebar_state:t()) -> {ok, rebar_state:t()}. + Provider = providers:create([ + {name, ?PROVIDER}, % The 'user friendly' name of the task + {module, ?MODULE}, % The module implementation of the task + {bare, true}, % The task can be run by the user, always true + {deps, ?DEPS}, % The list of dependencies + {example, "rebar {{appid}}"}, % How to use the plugin + {opts, []} % list of options understood by the plugin + {short_desc, {{desc}}}, + {desc, ""} + ]), + {ok, rebar_state:add_provider(State, Provider)}. + + +-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> {ok, State}. + +-spec format_error(any(), rebar_state:t()) -> {iolist(), rebar_state:t()}. +format_error(Reason, State) -> + {io_lib:format("~p", [Reason]), State}. + diff --git a/priv/templates/plugin.template b/priv/templates/plugin.template index bc44863..7235b60 100644 --- a/priv/templates/plugin.template +++ b/priv/templates/plugin.template @@ -1,7 +1,11 @@ -{variables, []}. -{template, "plugin.erl", "src/{{appid}}.erl"}. -{template, "otp_lib.app.src", "src/{{appid}}.app.src"}. -{template, "rebar.config", "rebar.config"}. -{template, "gitignore", ".gitignore"}. -{template, "LICENSE", "LICENSE"}. -{template, "plugin_README.md", "README.md"}. +{description, "Rebar3 plugin"}. +{variables, [ + {appid, "myplugin", "Name of the plugin"}, + {desc, "A rebar plugin", "Short description of the plugin's purpose"} +]}. +{template, "plugin.erl.dtl", "src/{{appid}}.erl"}. +{template, "otp_lib.app.src.dtl", "src/{{appid}}.app.src"}. +{template, "rebar.config.dtl", "rebar.config"}. +{template, "gitignore.dtl", ".gitignore"}. +{template, "LICENSE.dtl", "LICENSE"}. +{template, "plugin_README.md.dtl", "README.md"}. diff --git a/priv/templates/plugin_README.md.dtl b/priv/templates/plugin_README.md.dtl index 19990f5..c784324 100644 --- a/priv/templates/plugin_README.md.dtl +++ b/priv/templates/plugin_README.md.dtl @@ -1,7 +1,7 @@ {{appid}} ===== -Rebar3 plugin +{{desc}} Build ----- @@ -11,4 +11,17 @@ Build Use --- +Add the plugin to your rebar config: + + {plugins, [ + { {{appid}}, ".*", {git, "git@host:user/{{appid}}.git", {tag, "0.1.0"}}} + ]}. + +Then just call your plugin directly in an existing application: + + $ rebar3 {{appid}} + ===> Fetching {{appid}} + Cloning into '.tmp_dir539136867963'... + ===> Compiling {{appid}} + diff --git a/priv/templates/release.template b/priv/templates/release.template new file mode 100644 index 0000000..5e3ba1a --- /dev/null +++ b/priv/templates/release.template @@ -0,0 +1,15 @@ +{description, "OTP Release structure for executable programs"}. +{variables, [ + {appid, "myapp", "Name of the OTP release. An app with this name will also be created."}, + {desc, "An OTP application", "Short description of the release's main app's purpose"} +]}. +{template, "app.erl.dtl", "{{apps_dir}}/{{appid}}/src/{{appid}}_app.erl"}. +{template, "sup.erl.dtl", "{{apps_dir}}/{{appid}}/src/{{appid}}_sup.erl"}. +{template, "otp_app.app.src.dtl", "{{apps_dir}}/{{appid}}/src/{{appid}}.app.src"}. +{template, "rebar.config.dtl", "rebar.config"}. +{template, "relx.config.dtl", "relx.config"}. +{template, "sys.config.dtl", "config/sys.config"}. +{template, "vm.args.dtl", "config/vm.args"}. +{template, "gitignore.dtl", ".gitignore"}. +{template, "LICENSE.dtl", "LICENSE"}. +{template, "README.md.dtl", "README.md"}. diff --git a/src/rebar_prv_new.erl b/src/rebar_prv_new.erl index 698b14e..e22625c 100644 --- a/src/rebar_prv_new.erl +++ b/src/rebar_prv_new.erl @@ -30,15 +30,17 @@ init(State) -> -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> case rebar_state:command_args(State) of - [TemplateName] -> - Template = list_to_atom(TemplateName), - rebar_templater:new(Template, "", State), + ["help", TemplateName] -> + case lists:keyfind(TemplateName, 1, rebar_templater:list_templates(State)) of + false -> io:format("template not found.~n"); + Term -> show_template(Term) + end, {ok, State}; - [TemplateName, DirName] -> - Template = list_to_atom(TemplateName), - rebar_templater:new(Template, DirName, State), + [TemplateName | Opts] -> + ok = rebar_templater:new(TemplateName, parse_opts(Opts), State), {ok, State}; [] -> + show_short_templates(rebar_templater:list_templates(State)), {ok, State} end. @@ -56,3 +58,52 @@ info() -> "~n" "Valid command line options:~n" " template= [var=foo,...]~n", []). + +parse_opts([]) -> []; +parse_opts([Opt|Opts]) -> [parse_opt(Opt, "") | parse_opts(Opts)]. + +%% We convert to atoms dynamically. Horrible in general, but fine in a +%% build system's templating tool. +parse_opt("", Acc) -> {list_to_atom(lists:reverse(Acc)), "true"}; +parse_opt("="++Rest, Acc) -> {list_to_atom(lists:reverse(Acc)), Rest}; +parse_opt([H|Str], Acc) -> parse_opt(Str, [H|Acc]). + +show_short_templates(List) -> + lists:map(fun show_short_template/1, lists:sort(List)). + +show_short_template({Name, Type, _Location, Description, _Vars}) -> + io:format("~s (~s): ~s~n", + [Name, + format_type(Type), + format_description(Description)]). + +show_template({Name, Type, Location, Description, Vars}) -> + io:format("~s:~n" + "\t~s~n" + "\tDescription: ~s~n" + "\tVariables:~n~s~n", + [Name, + format_type(Type, Location), + format_description(Description), + format_vars(Vars)]). + +format_type(escript) -> "built-in"; +format_type(file) -> "custom". + +format_type(escript, _) -> + "built-in template"; +format_type(file, Loc) -> + io_lib:format("custom template (~s)", [Loc]). + +format_description(Description) -> + case Description of + undefined -> ""; + _ -> Description + end. + +format_vars(Vars) -> [format_var(Var) || Var <- Vars]. + +format_var({Var, Default}) -> + io_lib:format("\t\t~p=~p~n",[Var, Default]); +format_var({Var, Default, Doc}) -> + io_lib:format("\t\t~p=~p (~s)~n", [Var, Default, Doc]). diff --git a/src/rebar_templater.erl b/src/rebar_templater.erl index f5b1bd5..57915f4 100644 --- a/src/rebar_templater.erl +++ b/src/rebar_templater.erl @@ -27,8 +27,7 @@ -module(rebar_templater). -export([new/3, - list_templates/1, - create/1]). + 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, 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) + 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) -> + 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). + +%% 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) -> + execute_template(Terms, Files, Template, Vars); +%% We can't execute variables +execute_template([{variables, _} | Terms], Files, Template, Vars) -> + execute_template(Terms, Files, Template, Vars); +%% Create a directory +execute_template([{dir, Path} | Terms], Files, Template, Vars) -> + ?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); +%% Change permissions on a file +execute_template([{chmod, File, Perm} | Terms], Files, Template, Vars) -> + Path = expand_path(File, Vars), + case file:change_mode(Path, Perm) of + ok -> + execute_template(Terms, Files, Template, Vars); + {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) -> + ?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, false) of + ok -> ok; + {error, exists} -> ?INFO("File ~p already exists.~n", [Out]) + end, + execute_template(Terms, Files, {Template, Type, Cwd}, Vars); +%% Operate on a django template +execute_template([{template, From, To} | Terms], Files, {Template, Type, Cwd}, Vars) -> + ?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, false) of + ok -> ok; + {error, exists} -> ?INFO("File ~p already exists~n", [Out]) + end, + execute_template(Terms, Files, {Template, Type, Cwd}, Vars); +%% Unknown +execute_template([Instruction|Terms], Files, Tpl={Template,_,_}, Vars) -> + ?WARN("Unknown template instruction ~p in template ~s", + [Instruction, Template]), + execute_template(Terms, Files, Tpl, Vars). + +%% 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,13 +340,6 @@ 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), @@ -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 -- cgit v1.1 From 4ed1c4ef581cb780a4b6e2da7cb4e8243b20a370 Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Tue, 11 Nov 2014 00:28:05 +0000 Subject: Allow to forcibly overwrite files with templates --- src/rebar_prv_new.erl | 29 ++++++++++++++++++++--------- src/rebar_templater.erl | 48 ++++++++++++++++++++++++------------------------ 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/rebar_prv_new.erl b/src/rebar_prv_new.erl index e22625c..93adeb4 100644 --- a/src/rebar_prv_new.erl +++ b/src/rebar_prv_new.erl @@ -17,14 +17,17 @@ -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> - State1 = rebar_state:add_provider(State, providers:create([{name, ?PROVIDER}, - {module, ?MODULE}, - {bare, false}, - {deps, ?DEPS}, - {example, "rebar new