Parcourir la source

Merge pull request #28 from tsloughter/templates-redux

Redo templates with docs and whatnot
pull/3/head
Tristan Sloughter il y a 10 ans
Parent
révision
022ffdf6cb
24 fichiers modifiés avec 592 ajouts et 383 suppressions
  1. +191
    -0
      doc/templates.md
  2. +1
    -0
      include/rebar.hrl
  3. +1
    -1
      priv/templates/LICENSE.dtl
  4. +2
    -2
      priv/templates/README.md.dtl
  5. +3
    -3
      priv/templates/app.erl.dtl
  6. +11
    -0
      priv/templates/app.template
  7. +5
    -1
      priv/templates/gitignore.dtl
  8. +11
    -0
      priv/templates/lib.template
  9. +3
    -3
      priv/templates/mod.erl.dtl
  10. +3
    -4
      priv/templates/otp_app.app.src.dtl
  11. +0
    -7
      priv/templates/otp_app.template
  12. +3
    -6
      priv/templates/otp_lib.app.src.dtl
  13. +0
    -7
      priv/templates/otp_lib.template
  14. +0
    -11
      priv/templates/otp_rel.template
  15. +25
    -18
      priv/templates/plugin.erl.dtl
  16. +11
    -7
      priv/templates/plugin.template
  17. +16
    -3
      priv/templates/plugin_README.md.dtl
  18. +15
    -0
      priv/templates/release.template
  19. +2
    -2
      priv/templates/relx.config.dtl
  20. +2
    -2
      priv/templates/sup.erl.dtl
  21. +1
    -1
      priv/templates/sys.config.dtl
  22. +2
    -2
      priv/templates/vm.args.dtl
  23. +84
    -14
      src/rebar_prv_new.erl
  24. +200
    -289
      src/rebar_templater.erl

+ 191
- 0
doc/templates.md Voir le fichier

@ -0,0 +1,191 @@
# 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:
name="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).
A template can be run by calling:
→ ./rebar3 new plugin name=demo author_name="Fred H."
...
Alternatively, the `name` variable is special -- if the first argument to a template has no key associated with it, `name` is automatically added. The call above is therefore equivalent to:
→ ./rebar3 new plugin demo author_name="Fred H."
...
## 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, [
{name, "suite", "Name of the suite, prepended to the standard _SUITE suffix"}
]}.
{dir, "test"}.
{template, "ct_suite.erl.dtl", "test/{{name}}_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({{name}}_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 `{{name}}`) 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:
name="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 demo
===> Writing test/demo_SUITE.erl
And you will see the code in place.

+ 1
- 0
include/rebar.hrl Voir le fichier

@ -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().

+ 1
- 1
priv/templates/LICENSE.dtl Voir le fichier

@ -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

+ 2
- 2
priv/templates/README.md.dtl Voir le fichier

@ -1,7 +1,7 @@
{{appid}}
{{name}}
=====
An Erlang {{appid}} library.
{{desc}}
Build
-----

+ 3
- 3
priv/templates/app.erl.dtl Voir le fichier

@ -1,9 +1,9 @@
%%%-------------------------------------------------------------------
%% @doc {{appid}} public API
%% @doc {{name}} public API
%% @end
%%%-------------------------------------------------------------------
-module({{appid}}_app).
-module({{name}}_app).
-behaviour(application).
@ -16,7 +16,7 @@
%%====================================================================
start(_StartType, _StartArgs) ->
{{appid}}_sup:start_link().
{{name}}_sup:start_link().
%%--------------------------------------------------------------------
stop(_State) ->

+ 11
- 0
priv/templates/app.template Voir le fichier

@ -0,0 +1,11 @@
{description, "OTP Application"}.
{variables, [
{name, "mylib", "Name of the OTP application"},
{desc, "An OTP application", "Short description of the app"}
]}.
{template, "app.erl.dtl", "src/{{name}}_app.erl"}.
{template, "otp_app.app.src.dtl", "src/{{name}}.app.src"}.
{template, "rebar.config.dtl", "rebar.config"}.
{template, "gitignore.dtl", ".gitignore"}.
{template, "LICENSE.dtl", "LICENSE"}.
{template, "README.md.dtl", "README.md"}.

+ 5
- 1
priv/templates/gitignore.dtl Voir le fichier

@ -1,13 +1,17 @@
_*
.eunit
deps
*.o
*.beam
*.plt
*.swp
*.swo
.erlang.cookie
ebin
log
erl_crash.dump
.rebar
_rel
_deps
_plugins
_tdeps
logs

+ 11
- 0
priv/templates/lib.template Voir le fichier

@ -0,0 +1,11 @@
{description, "OTP Library application (no processes)"}.
{variables, [
{name, "mylib", "Name of the OTP library application"},
{desc, "An OTP library", "Short description of the app"}
]}.
{template, "mod.erl.dtl", "src/{{name}}.erl"}.
{template, "otp_lib.app.src.dtl", "src/{{name}}.app.src"}.
{template, "rebar.config.dtl", "rebar.config"}.
{template, "gitignore.dtl", ".gitignore"}.
{template, "LICENSE.dtl", "LICENSE"}.
{template, "README.md.dtl", "README.md"}.

+ 3
- 3
priv/templates/mod.erl.dtl Voir le fichier

@ -1,10 +1,10 @@
-module({{appid}}).
-module({{name}}).
%% Application callbacks
%% API exports
-export([]).
%%====================================================================
%% API
%% API functions
%%====================================================================

+ 3
- 4
priv/templates/otp_app.app.src.dtl Voir le fichier

@ -1,9 +1,8 @@
{application, {{appid}},
[
{description, "{{appid}}"}
{application, {{name}},
[{description, "{{desc}}"}
,{vsn, "0.1.0"}
,{registered, []}
,{mod, {'{{appid}}_app', []}}
,{mod, {'{{name}}_app', []}}
,{applications,
[kernel
,stdlib

+ 0
- 7
priv/templates/otp_app.template Voir le fichier

@ -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"}.

+ 3
- 6
priv/templates/otp_lib.app.src.dtl Voir le fichier

@ -1,12 +1,9 @@
{application, {{appid}},
[
{description, "{{appid}}"}
{application, {{name}},
[{description, "{{desc}}"}
,{vsn, "0.1.0"}
,{registered, []}
,{applications,
[kernel
,stdlib
]}
[kernel,stdlib]}
,{env,[]}
,{modules, []}
]}.

+ 0
- 7
priv/templates/otp_lib.template Voir le fichier

@ -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"}.

+ 0
- 11
priv/templates/otp_rel.template Voir le fichier

@ -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"}.

+ 25
- 18
priv/templates/plugin.erl.dtl Voir le fichier

@ -1,29 +1,36 @@
-module({{appid}}).
-module({{name}}).
-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 {{name}}"}, % 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}.

+ 11
- 7
priv/templates/plugin.template Voir le fichier

@ -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, [
{name, "myplugin", "Name of the plugin"},
{desc, "A rebar plugin", "Short description of the plugin's purpose"}
]}.
{template, "plugin.erl.dtl", "src/{{name}}.erl"}.
{template, "otp_lib.app.src.dtl", "src/{{name}}.app.src"}.
{template, "rebar.config.dtl", "rebar.config"}.
{template, "gitignore.dtl", ".gitignore"}.
{template, "LICENSE.dtl", "LICENSE"}.
{template, "plugin_README.md.dtl", "README.md"}.

+ 16
- 3
priv/templates/plugin_README.md.dtl Voir le fichier

@ -1,7 +1,7 @@
{{appid}}
{{name}}
=====
Rebar3 plugin
{{desc}}
Build
-----
@ -11,4 +11,17 @@ Build
Use
---
$ rebar3 {{appid}}
Add the plugin to your rebar config:
{plugins, [
{ {{name}}, ".*", {git, "git@host:user/{{name}}.git", {tag, "0.1.0"}}}
]}.
Then just call your plugin directly in an existing application:
$ rebar3 {{name}}
===> Fetching {{name}}
Cloning into '.tmp_dir539136867963'...
===> Compiling {{name}}
<Plugin Output>

+ 15
- 0
priv/templates/release.template Voir le fichier

@ -0,0 +1,15 @@
{description, "OTP Release structure for executable programs"}.
{variables, [
{name, "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}}/{{name}}/src/{{name}}_app.erl"}.
{template, "sup.erl.dtl", "{{apps_dir}}/{{name}}/src/{{name}}_sup.erl"}.
{template, "otp_app.app.src.dtl", "{{apps_dir}}/{{name}}/src/{{name}}.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"}.

+ 2
- 2
priv/templates/relx.config.dtl Voir le fichier

@ -1,6 +1,6 @@
%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
{release, {'{{appid}}', "0.1.0"},
[{{appid}},
{release, {'{{name}}', "0.1.0"},
[{{name}},
sasl]}.
{sys_config, "./config/sys.config"}.

+ 2
- 2
priv/templates/sup.erl.dtl Voir le fichier

@ -1,9 +1,9 @@
%%%-------------------------------------------------------------------
%% @doc {{appid}} top level supervisor.
%% @doc {{name}} top level supervisor.
%% @end
%%%-------------------------------------------------------------------
-module({{appid}}_sup).
-module({{name}}_sup).
-behaviour(supervisor).

+ 1
- 1
priv/templates/sys.config.dtl Voir le fichier

@ -1,3 +1,3 @@
[
{'{{appid}}', []}
{'{{name}}', []}
].

+ 2
- 2
priv/templates/vm.args.dtl Voir le fichier

@ -1,6 +1,6 @@
-name {{appid}}
-name {{name}}
-setcookie {{appid}}_cookie
-setcookie {{name}}_cookie
+K true
+A30

+ 84
- 14
src/rebar_prv_new.erl Voir le fichier

@ -17,28 +17,34 @@
-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 <template>"},
{short_desc, "Create new project from templates."},
{desc, info()},
{opts, []}])),
Provider = providers:create([
{name, ?PROVIDER},
{module, ?MODULE},
{bare, false},
{deps, ?DEPS},
{example, "rebar new <template>"},
{short_desc, "Create new project from templates."},
{desc, info()},
{opts, [{force, $f, "force", undefined, "overwrite existing files"}]}
]),
State1 = rebar_state:add_provider(State, Provider),
{ok, State1}.
-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] ->
Force = is_forced(State),
ok = rebar_templater:new(TemplateName, parse_opts(Opts), Force, State),
{ok, State};
[] ->
show_short_templates(rebar_templater:list_templates(State)),
{ok, State}
end.
@ -56,3 +62,67 @@ info() ->
"~n"
"Valid command line options:~n"
" template= [var=foo,...]~n", []).
is_forced(State) ->
{Args, _} = rebar_state:command_parsed_args(State),
case proplists:get_value(force, Args) of
undefined -> false;
_ -> true
end.
parse_opts([]) -> [];
parse_opts([Opt|Opts]) -> [parse_first_opt(Opt, "") | parse_opts1(Opts)].
parse_opts1([]) -> [];
parse_opts1([Opt|Opts]) -> [parse_opt(Opt, "") | parse_opts1(Opts)].
%% If the first argument meets no '=', we got a default 'name' argument
parse_first_opt("", Acc) -> {name, lists:reverse(Acc)};
parse_first_opt("="++Rest, Acc) -> parse_opt("="++Rest, Acc);
parse_first_opt([H|Str], Acc) -> parse_first_opt(Str, [H|Acc]).
%% 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]).

+ 200
- 289
src/rebar_templater.erl Voir le fichier

@ -26,9 +26,8 @@
%% -------------------------------------------------------------------
-module(rebar_templater).
-export([new/3,
list_templates/1,
create/1]).
-export([new/4,
list_templates/1]).
%% API for other utilities that need templating functionality
-export([resolve_variables/2,
@ -43,37 +42,24 @@
%% Public API
%% ===================================================================
new(app, DirName, State) ->
create1(State, DirName, "otp_app");
new(lib, DirName, State) ->
create1(State, DirName, "otp_lib");
new(plugin, DirName, State) ->
create1(State, DirName, "plugin");
new(rel, DirName, State) ->
create1(State, DirName, "otp_rel").
%% Apply a template
new(Template, Vars, Force, State) ->;
{AvailTemplates, Files} = find_templates(State),
?DEBUG("Looking for ~p~n", [Template]),
case lists:keyfind(Template, 1, AvailTemplates) of
false -> {not_found, Template};
TemplateTup -> create(TemplateTup, Files, Vars, Force)
end.
%% Give a list of templates with their expanded content
list_templates(State) ->
{AvailTemplates, Files} = find_templates(State),
?DEBUG("Available templates: ~p\n", [AvailTemplates]),
[list_template(Files, Template) || Template <- AvailTemplates].
lists:foreach(
fun({Type, F}) ->
BaseName = filename:basename(F, ".template"),
TemplateTerms = consult(load_file(Files, Type, F)),
{_, VarList} = lists:keyfind(variables, 1, TemplateTerms),
Vars = lists:foldl(fun({V,_}, Acc) ->
[atom_to_list(V) | Acc]
end, [], VarList),
?INFO(" * ~s: ~s (~p) (variables: ~p)\n",
[BaseName, F, Type, string:join(Vars, ", ")])
end, AvailTemplates),
ok.
create(State) ->
TemplateId = template_id(State),
create1(State, "", TemplateId).
%% ===================================================================
%% Rendering API / legacy?
%% ===================================================================
%%
%% Given a list of key value pairs, for each string value attempt to
%% render it using Dict as the context. Storing the result in Dict as Key.
%%
@ -97,96 +83,176 @@ render(Template, Context) ->
Module = list_to_atom(Template++"_dtl"),
Module:render(Context).
%% ===================================================================
%% Internal functions
%% Internal Functions
%% ===================================================================
create1(State, AppDir, TemplateId) ->
ec_file:mkdir_p(AppDir),
file:set_cwd(AppDir),
{AvailTemplates, Files} = find_templates(State),
?DEBUG("Available templates: ~p\n", [AvailTemplates]),
%% Expand a single template's value
list_template(Files, {Name, Type, File}) ->
TemplateTerms = consult(load_file(Files, Type, File)),
{Name, Type, File,
get_template_description(TemplateTerms),
get_template_vars(TemplateTerms)}.
%% Load up the template description out from a list of attributes read in
%% a .template file.
get_template_description(TemplateTerms) ->
case lists:keyfind(description, 1, TemplateTerms) of
{_, Desc} -> Desc;
false -> undefined
end.
%% Using the specified template id, find the matching template file/type.
%% Note that if you define the same template in both ~/.rebar/templates
%% that is also present in the escript, the one on the file system will
%% be preferred.
{Type, Template} = select_template(AvailTemplates, TemplateId),
%% Load the template definition as is and get the list of variables the
%% template requires.
Context0 = dict:from_list([{appid, AppDir}]),
TemplateTerms = consult(load_file(Files, Type, Template)),
case lists:keyfind(variables, 1, TemplateTerms) of
{variables, Vars} ->
case parse_vars(Vars, Context0) of
{error, Entry} ->
Context1 = undefined,
?ABORT("Failed while processing variables from template ~p."
"Variable definitions must follow form of "
"[{atom(), term()}]. Failed at: ~p\n",
[TemplateId, Entry]);
Context1 ->
ok
end;
false ->
?WARN("No variables section found in template ~p; "
"using empty context.\n", [TemplateId]),
Context1 = Context0
%% Load up the variables out from a list of attributes read in a .template file
%% and return them merged with the globally-defined and default variables.
get_template_vars(TemplateTerms) ->
Vars = case lists:keyfind(variables, 1, TemplateTerms) of
{_, Value} -> Value;
false -> []
end,
override_vars(Vars, override_vars(global_variables(), default_variables())).
%% Provide a way to merge a set of variables with another one. The left-hand
%% set of variables takes precedence over the right-hand set.
%% In the case where left-hand variable description contains overriden defaults, but
%% the right-hand one contains additional data such as documentation, the resulting
%% variable description will contain the widest set of information possible.
override_vars([], General) -> General;
override_vars([{Var, Default} | Rest], General) ->
case lists:keytake(Var, 1, General) of
{value, {Var, _Default, Doc}, NewGeneral} ->
[{Var, Default, Doc} | override_vars(Rest, NewGeneral)];
{value, {Var, _Default}, NewGeneral} ->
[{Var, Default} | override_vars(Rest, NewGeneral)];
false ->
[{Var, Default} | override_vars(Rest, General)]
end;
override_vars([{Var, Default, Doc} | Rest], General) ->
[{Var, Default, Doc} | override_vars(Rest, lists:keydelete(Var, 1, General))].
%% Default variables, generated dynamically.
default_variables() ->
{{Y,M,D},{H,Min,S}} = calendar:universal_time(),
[{date, lists:flatten(io_lib:format("~4..0w-~2..0w-~2..0w",[Y,M,D]))},
{datetime, lists:flatten(io_lib:format("~4..0w-~2..0w-~2..0wT~2..0w:~2..0w:~2..0w+00:00",[Y,M,D,H,Min,S]))},
{author_name, "Anonymous"},
{author_email, "anonymous@example.org"},
{copyright_year, integer_to_list(Y)},
{apps_dir, "apps/", "Directory where applications will be created if needed"}].
%% Load variable definitions from the 'Globals' file in the home template
%% directory
global_variables() ->
{ok, [[Home]]} = init:get_argument(home),
GlobalFile = filename:join([Home, ?HOME_DIR, "templates", "globals"]),
case file:consult(GlobalFile) of
{error, enoent} -> [];
{ok, Data} -> proplists:get_value(variables, Data, [])
end.
%% Load variables from disk file, if provided
Context2 = case rebar_state:get(State, template_vars, undefined) of
undefined ->
Context1;
File ->
case consult(load_file([], file, File)) of
{error, Reason} ->
?ABORT("Unable to load template_vars from ~s: ~p\n",
[File, Reason]);
Terms ->
%% TODO: Cleanup/merge with similar code in rebar_reltool
M = fun(_Key, _Base, Override) -> Override end,
dict:merge(M, Context1, dict:from_list(Terms))
end
end,
%% For each variable, see if it's defined in global vars -- if it is,
%% prefer that value over the defaults
Context3 = update_vars(State, dict:fetch_keys(Context2), Context1),
?DEBUG("Template ~p context: ~p\n", [TemplateId, dict:to_list(Context2)]),
%% Handle variables that possibly include other variables in their
%% definition
%Context = resolve_variables(dict:to_list(Context3), Context3),
%?DEBUG("Resolved Template ~p context: ~p\n",
%[TemplateId, dict:to_list(Context)]),
%% Now, use our context to process the template definition -- this
%% permits us to use variables within the definition for filenames.
%FinalTemplate = consult(render(load_file(Files, Type, Template), Context)),
%?DEBUG("Final template def ~p: ~p\n", [TemplateId, FinalTemplate]),
%% Execute the instructions in the finalized template
Force = rebar_state:get(State, force, "0"),
execute_template([], TemplateTerms, Type, TemplateId, Context3, Force, []).
%% drop the documentation for variables when present
drop_var_docs([]) -> [];
drop_var_docs([{K,V,_}|Rest]) -> [{K,V} | drop_var_docs(Rest)];
drop_var_docs([{K,V}|Rest]) -> [{K,V} | drop_var_docs(Rest)].
%% Load the template index, resolve all variables, and then execute
%% the template.
create({Template, Type, File}, Files, UserVars, Force) ->
TemplateTerms = consult(load_file(Files, Type, File)),
Vars = drop_var_docs(override_vars(UserVars, get_template_vars(TemplateTerms))),
TemplateCwd = filename:dirname(File),
execute_template(TemplateTerms, Files, {Template, Type, TemplateCwd}, Vars, Force).
%% Run template instructions one at a time.
execute_template([], _, {Template,_,_}, _, _) ->
?DEBUG("Template ~s applied~n", [Template]),
ok;
%% We can't execute the description
execute_template([{description, _} | Terms], Files, Template, Vars, Force) ->
execute_template(Terms, Files, Template, Vars, Force);
%% We can't execute variables
execute_template([{variables, _} | Terms], Files, Template, Vars, Force) ->
execute_template(Terms, Files, Template, Vars, Force);
%% Create a directory
execute_template([{dir, Path} | Terms], Files, Template, Vars, Force) ->
?DEBUG("Creating directory ~p~n", [Path]),
case ec_file:mkdir_p(expand_path(Path, Vars)) of
ok ->
ok;
{error, Reason} ->
?ABORT("Failed while processing template instruction "
"{dir, ~p}: ~p~n", [Path, Reason])
end,
execute_template(Terms, Files, Template, Vars, Force);
%% Change permissions on a file
execute_template([{chmod, File, Perm} | Terms], Files, Template, Vars, Force) ->
Path = expand_path(File, Vars),
case file:change_mode(Path, Perm) of
ok ->
execute_template(Terms, Files, Template, Vars, Force);
{error, Reason} ->
?ABORT("Failed while processing template instruction "
"{chmod, ~.8#, ~p}: ~p~n", [Perm, File, Reason])
end;
%% Create a raw untemplated file
execute_template([{file, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) ->
?DEBUG("Creating file ~p~n", [To]),
Data = load_file(Files, Type, filename:join(Cwd, From)),
Out = expand_path(To,Vars),
case write_file(Out, Data, Force) of
ok -> ok;
{error, exists} -> ?INFO("File ~p already exists.~n", [Out])
end,
execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force);
%% Operate on a django template
execute_template([{template, From, To} | Terms], Files, {Template, Type, Cwd}, Vars, Force) ->
?DEBUG("Executing template file ~p~n", [From]),
Out = expand_path(To, Vars),
Tpl = load_file(Files, Type, filename:join(Cwd, From)),
TplName = make_template_name("rebar_template", Out),
{ok, Mod} = erlydtl:compile_template(Tpl, TplName, ?ERLYDTL_COMPILE_OPTS),
{ok, Output} = Mod:render(Vars),
case write_file(Out, Output, Force) of
ok -> ok;
{error, exists} -> ?INFO("File ~p already exists~n", [Out])
end,
execute_template(Terms, Files, {Template, Type, Cwd}, Vars, Force);
%% Unknown
execute_template([Instruction|Terms], Files, Tpl={Template,_,_}, Vars, Force) ->
?WARN("Unknown template instruction ~p in template ~s",
[Instruction, Template]),
execute_template(Terms, Files, Tpl, Vars, Force).
%% Workaround to allow variable substitution in path names without going
%% through the ErlyDTL compilation step. Parse the string and replace
%% as we go.
expand_path([], _) -> [];
expand_path("{{"++Rest, Vars) -> replace_var(Rest, [], Vars);
expand_path([H|T], Vars) -> [H | expand_path(T, Vars)].
%% Actual variable replacement.
replace_var("}}"++Rest, Acc, Vars) ->
Var = lists:reverse(Acc),
Val = proplists:get_value(list_to_atom(Var), Vars, ""),
Val ++ expand_path(Rest, Vars);
replace_var([H|T], Acc, Vars) ->
replace_var(T, [H|Acc], Vars).
%% Load a list of all the files in the escript and on disk
find_templates(State) ->
%% Load a list of all the files in the escript -- cache them since
%% we'll potentially need to walk it several times over the course of
%% a run.
%% Cache the files since we'll potentially need to walk it several times
%% over the course of a run.
Files = cache_escript_files(State),
%% Build a list of available templates
AvailTemplates = find_disk_templates(State)
++ find_escript_templates(Files),
AvailTemplates = prioritize_templates(
tag_names(find_disk_templates(State)),
tag_names(find_escript_templates(Files))),
?DEBUG("Available templates: ~p\n", [AvailTemplates]),
{AvailTemplates, Files}.
%%
%% Scan the current escript for available files
%%
cache_escript_files(State) ->
{ok, Files} = rebar_utils:escript_foldl(
fun(Name, _, GetBin, Acc) ->
@ -195,27 +261,22 @@ cache_escript_files(State) ->
[], rebar_state:get(State, escript)),
Files.
template_id(State) ->
case rebar_state:get(State, template, undefined) of
undefined ->
?ABORT("No template specified.\n", []);
TemplateId ->
TemplateId
end.
%% Find all the template indexes hiding in the rebar3 escript.
find_escript_templates(Files) ->
[{escript, Name}
|| {Name, _Bin} <- Files,
re:run(Name, ?TEMPLATE_RE, [{capture, none}]) == match].
%% Fetch template indexes that sit on disk in the user's HOME
find_disk_templates(State) ->
OtherTemplates = find_other_templates(State),
HomeFiles = rebar_utils:find_files(filename:join([os:getenv("HOME"),
".rebar", "templates"]),
{ok, [[Home]]} = init:get_argument(home),
HomeFiles = rebar_utils:find_files(filename:join([Home, ?HOME_DIR, "templates"]),
?TEMPLATE_RE),
LocalFiles = rebar_utils:find_files(".", ?TEMPLATE_RE, true),
[{file, F} || F <- OtherTemplates ++ HomeFiles ++ LocalFiles].
%% Fetch template indexes that sit on disk in custom areas
find_other_templates(State) ->
case rebar_state:get(State, template_dir, undefined) of
undefined ->
@ -224,19 +285,31 @@ find_other_templates(State) ->
rebar_utils:find_files(TemplateDir, ?TEMPLATE_RE)
end.
select_template([], Template) ->
?ABORT("Template ~s not found.\n", [Template]);
select_template([{Type, Avail} | Rest], Template) ->
case filename:basename(Avail, ".template") == Template of
true ->
{Type, Avail};
%% Take an existing list of templates and tag them by name the way
%% the user would enter it from the CLI
tag_names(List) ->
[{filename:basename(File, ".template"), Type, File}
|| {Type, File} <- List].
%% If multiple templates share the same name, those in the escript (built-in)
%% take precedence. Otherwise, the on-disk order is the one to win.
prioritize_templates([], Acc) -> Acc;
prioritize_templates([{Name, Type, File} | Rest], Valid) ->
case lists:keyfind(Name, 1, Valid) of
false ->
select_template(Rest, Template)
prioritize_templates(Rest, [{Name, Type, File} | Valid]);
{_, escript, _} ->
?DEBUG("Skipping template ~p, due to presence of a built-in "
"template with the same name~n", [Name]),
prioritize_templates(Rest, Valid);
{_, file, _} ->
?DEBUG("Skipping template ~p, due to presence of a custom "
"template at ~s~n", [File]),
prioritize_templates(Rest, Valid)
end.
%%
%% Read the contents of a file from the appropriate source
%%
load_file(Files, escript, Name) ->
{Name, Bin} = lists:keyfind(Name, 1, Files),
Bin;
@ -244,32 +317,7 @@ load_file(_Files, file, Name) ->
{ok, Bin} = file:read_file(Name),
Bin.
%%
%% Parse/validate variables out from the template definition
%%
parse_vars([], Dict) ->
Dict;
parse_vars([{Key, Value} | Rest], Dict) when is_atom(Key) ->
parse_vars(Rest, dict:store(Key, Value, Dict));
parse_vars([Other | _Rest], _Dict) ->
{error, Other};
parse_vars(Other, _Dict) ->
{error, Other}.
%%
%% Given a list of keys in Dict, see if there is a corresponding value defined
%% in the global config; if there is, update the key in Dict with it
%%
update_vars(_State, [], Dict) ->
Dict;
update_vars(State, [Key | Rest], Dict) ->
Value = rebar_state:get(State, Key, dict:fetch(Key, Dict)),
update_vars(State, Rest, dict:store(Key, Value, Dict)).
%%
%% Given a string or binary, parse it into a list of terms, ala file:consult/1
%%
consult(Str) when is_list(Str) ->
consult([], Str, []);
consult(Bin) when is_binary(Bin)->
@ -281,7 +329,7 @@ consult(Cont, Str, Acc) ->
case Result of
{ok, Tokens, _} ->
{ok, Term} = erl_parse:parse_term(Tokens),
consult([], Remaining, [maybe_dict(Term) | Acc]);
consult([], Remaining, [Term | Acc]);
{eof, _Other} ->
lists:reverse(Acc);
{error, Info, _} ->
@ -292,24 +340,17 @@ consult(Cont, Str, Acc) ->
end.
maybe_dict({Key, {list, Dicts}}) ->
%% this is a 'list' element; a list of lists representing dicts
{Key, {list, [dict:from_list(D) || D <- Dicts]}};
maybe_dict(Term) ->
Term.
write_file(Output, Data, Force) ->
%% determine if the target file already exists
FileExists = filelib:is_regular(Output),
%% perform the function if we're allowed,
%% otherwise just process the next template
case Force =:= "1" orelse FileExists =:= false of
case Force orelse FileExists =:= false of
true ->
ok = filelib:ensure_dir(Output),
case {Force, FileExists} of
{"1", true} ->
{true, true} ->
?INFO("Writing ~s (forcibly overwriting)",
[Output]);
_ ->
@ -326,136 +367,6 @@ write_file(Output, Data, Force) ->
{error, exists}
end.
prepend_instructions(Instructions, Rest) when is_list(Instructions) ->
Instructions ++ Rest;
prepend_instructions(Instruction, Rest) ->
[Instruction|Rest].
%%
%% Execute each instruction in a template definition file.
%%
execute_template(_Files, [], _TemplateType, _TemplateName,
_Context, _Force, ExistingFiles) ->
case ExistingFiles of
[] ->
ok;
_ ->
Msg = lists:flatten([io_lib:format("\t* ~p~n", [F]) ||
F <- lists:reverse(ExistingFiles)]),
Help = "To force overwriting, specify -f/--force/force=1"
" on the command line.\n",
?ERROR("One or more files already exist on disk and "
"were not generated:~n~s~s", [Msg , Help])
end;
execute_template(Files, [{'if', Cond, True} | Rest], TemplateType,
TemplateName, Context, Force, ExistingFiles) ->
execute_template(Files, [{'if', Cond, True, []}|Rest], TemplateType,
TemplateName, Context, Force, ExistingFiles);
execute_template(Files, [{'if', Cond, True, False} | Rest], TemplateType,
TemplateName, Context, Force, ExistingFiles) ->
Instructions = case dict:find(Cond, Context) of
{ok, true} ->
True;
{ok, "true"} ->
True;
_ ->
False
end,
execute_template(Files, prepend_instructions(Instructions, Rest),
TemplateType, TemplateName, Context, Force,
ExistingFiles);
execute_template(Files, [{'case', Variable, Values, Instructions} | Rest], TemplateType,
TemplateName, Context, Force, ExistingFiles) ->
{ok, Value} = dict:find(Variable, Context),
Instructions2 = case lists:member(Value, Values) of
true ->
Instructions;
_ ->
[]
end,
execute_template(Files, prepend_instructions(Instructions2, Rest),
TemplateType, TemplateName, Context, Force,
ExistingFiles);
execute_template(Files, [{template, Input, Output} | Rest], TemplateType,
TemplateName, Context, Force, ExistingFiles) ->
_InputName = filename:join(filename:dirname(TemplateName), Input),
%File = load_file(Files, TemplateType, InputName),
OutputTemplateName = make_template_name("rebar_output_template", Output),
{ok, OutputTemplateName1} = erlydtl:compile_template(Output, OutputTemplateName, ?ERLYDTL_COMPILE_OPTS),
{ok, OutputRendered} = OutputTemplateName1:render(dict:to_list(Context)),
{ok, Rendered} = render(Input, dict:to_list(Context)),
case write_file(lists:flatten(io_lib:format("~s", [OutputRendered])), Rendered, Force) of
ok ->
execute_template(Files, Rest, TemplateType, TemplateName,
Context, Force, ExistingFiles);
{error, exists} ->
execute_template(Files, Rest, TemplateType, TemplateName,
Context, Force, [Output|ExistingFiles])
end;
execute_template(Files, [{file, Input, Output} | Rest], TemplateType,
TemplateName, Context, Force, ExistingFiles) ->
InputName = filename:join(filename:dirname(TemplateName), Input),
File = load_file(Files, TemplateType, InputName),
case write_file(Output, File, Force) of
ok ->
execute_template(Files, Rest, TemplateType, TemplateName,
Context, Force, ExistingFiles);
{error, exists} ->
execute_template(Files, Rest, TemplateType, TemplateName,
Context, Force, [Output|ExistingFiles])
end;
execute_template(Files, [{dir, Name} | Rest], TemplateType,
TemplateName, Context, Force, ExistingFiles) ->
case filelib:ensure_dir(filename:join(Name, "dummy")) of
ok ->
execute_template(Files, Rest, TemplateType, TemplateName,
Context, Force, ExistingFiles);
{error, Reason} ->
?ABORT("Failed while processing template instruction "
"{dir, ~s}: ~p\n", [Name, Reason])
end;
execute_template(Files, [{copy, Input, Output} | Rest], TemplateType,
TemplateName, Context, Force, ExistingFiles) ->
InputName = filename:join(filename:dirname(TemplateName), Input),
try rebar_file_utils:cp_r([InputName ++ "/*"], Output) of
ok ->
execute_template(Files, Rest, TemplateType, TemplateName,
Context, Force, ExistingFiles)
catch _:_ ->
?ABORT("Failed while processing template instruction "
"{copy, ~s, ~s}", [Input, Output])
end;
execute_template(Files, [{chmod, Mod, File} | Rest], TemplateType,
TemplateName, Context, Force, ExistingFiles)
when is_integer(Mod) ->
case file:change_mode(File, Mod) of
ok ->
execute_template(Files, Rest, TemplateType, TemplateName,
Context, Force, ExistingFiles);
{error, Reason} ->
?ABORT("Failed while processing template instruction "
"{chmod, ~b, ~s}: ~p", [Mod, File, Reason])
end;
execute_template(Files, [{symlink, Existing, New} | Rest], TemplateType,
TemplateName, Context, Force, ExistingFiles) ->
case file:make_symlink(Existing, New) of
ok ->
execute_template(Files, Rest, TemplateType, TemplateName,
Context, Force, ExistingFiles);
{error, Reason} ->
?ABORT("Failed while processing template instruction "
"{symlink, ~s, ~s}: ~p", [Existing, New, Reason])
end;
execute_template(Files, [{variables, _} | Rest], TemplateType,
TemplateName, Context, Force, ExistingFiles) ->
execute_template(Files, Rest, TemplateType, TemplateName,
Context, Force, ExistingFiles);
execute_template(Files, [Other | Rest], TemplateType, TemplateName,
Context, Force, ExistingFiles) ->
?WARN("Skipping unknown template instruction: ~p\n", [Other]),
execute_template(Files, Rest, TemplateType, TemplateName, Context,
Force, ExistingFiles).
-spec make_template_name(string(), term()) -> module().
make_template_name(Base, Value) ->
%% Seed so we get different values each time

Chargement…
Annuler
Enregistrer