ソースを参照

Redo templates with docs and whatnot

This totally breaks compatibility with rebar2, and maybe it shouldn't
have.
pull/3/head
Fred Hebert 10年前
コミット
c832b567db
19個のファイルの変更535行の追加354行の削除
  1. +182
    -0
      doc/templates.md
  2. +1
    -0
      include/rebar.hrl
  3. +1
    -1
      priv/templates/LICENSE.dtl
  4. +1
    -1
      priv/templates/README.md.dtl
  5. +11
    -0
      priv/templates/app.template
  6. +5
    -1
      priv/templates/gitignore.dtl
  7. +11
    -0
      priv/templates/lib.template
  8. +2
    -2
      priv/templates/mod.erl.dtl
  9. +1
    -2
      priv/templates/otp_app.app.src.dtl
  10. +0
    -7
      priv/templates/otp_app.template
  11. +2
    -5
      priv/templates/otp_lib.app.src.dtl
  12. +0
    -7
      priv/templates/otp_lib.template
  13. +0
    -11
      priv/templates/otp_rel.template
  14. +24
    -17
      priv/templates/plugin.erl.dtl
  15. +11
    -7
      priv/templates/plugin.template
  16. +14
    -1
      priv/templates/plugin_README.md.dtl
  17. +15
    -0
      priv/templates/release.template
  18. +57
    -6
      src/rebar_prv_new.erl
  19. +197
    -286
      src/rebar_templater.erl

+ 182
- 0
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.
~

+ 1
- 0
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().

+ 1
- 1
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

+ 1
- 1
priv/templates/README.md.dtl ファイルの表示

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

+ 11
- 0
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"}.

+ 5
- 1
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

+ 11
- 0
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"}.

+ 2
- 2
priv/templates/mod.erl.dtl ファイルの表示

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

+ 1
- 2
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', []}}

+ 0
- 7
priv/templates/otp_app.template ファイルの表示

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

+ 2
- 5
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, []}
]}.

+ 0
- 7
priv/templates/otp_lib.template ファイルの表示

@ -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 ファイルの表示

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

+ 24
- 17
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}.

+ 11
- 7
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"}.

+ 14
- 1
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>

+ 15
- 0
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"}.

+ 57
- 6
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]).

+ 197
- 286
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

読み込み中…
キャンセル
保存