summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFred Hebert <mononcqc@ferd.ca>2014-11-10 19:03:25 +0000
committerFred Hebert <mononcqc@ferd.ca>2014-11-10 22:37:09 +0000
commitc832b567dba73ddbed961b265f2018b3f2cb45ae (patch)
treee82b6bba0a1e6e68c17adeb185cb5fad399b3a4c
parente202367322b7ce155379196f42a009cf5bfa7e22 (diff)
Redo templates with docs and whatnot
This totally breaks compatibility with rebar2, and maybe it shouldn't have.
-rw-r--r--doc/templates.md182
-rw-r--r--include/rebar.hrl1
-rw-r--r--priv/templates/LICENSE.dtl2
-rw-r--r--priv/templates/README.md.dtl2
-rw-r--r--priv/templates/app.template11
-rw-r--r--priv/templates/gitignore.dtl6
-rw-r--r--priv/templates/lib.template11
-rw-r--r--priv/templates/mod.erl.dtl4
-rw-r--r--priv/templates/otp_app.app.src.dtl3
-rw-r--r--priv/templates/otp_app.template7
-rw-r--r--priv/templates/otp_lib.app.src.dtl7
-rw-r--r--priv/templates/otp_lib.template7
-rw-r--r--priv/templates/otp_rel.template11
-rw-r--r--priv/templates/plugin.erl.dtl41
-rw-r--r--priv/templates/plugin.template18
-rw-r--r--priv/templates/plugin_README.md.dtl15
-rw-r--r--priv/templates/release.template15
-rw-r--r--src/rebar_prv_new.erl63
-rw-r--r--src/rebar_templater.erl483
19 files changed, 535 insertions, 354 deletions
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 `<plugin_name> (custom): <description>`.
+
+Details for each individual plugin can be obtained by calling `rebar3 new help <plugin>`:
+
+ → ./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}}
+ <Plugin Output>
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 -> "<missing description>";
+ _ -> 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