构建插件
Rebar3 的系统基于提供程序的概念。一个提供程序有三个回调函数
init(State) -> {ok, NewState}
,用于帮助设置所需的状态、状态依赖项等。do(State) -> {ok, NewState} | {error, Error}
,执行实际工作。format_error(Error) -> String
,在发生错误时打印错误,并过滤掉状态中的敏感元素。
提供程序也应该是一个 OTP 库应用程序,可以像任何其他 Erlang 依赖项一样获取,但它是为 Rebar3 而不是您自己的系统或应用程序获取的。
本文档包含以下内容
使用插件
要使用插件,请将其添加到 rebar.config 中
{plugins, [
{plugin_name, {git, "git@host:user/name-of-plugin.git", {tag, "1.0.0"}}}
]}.
然后你可以直接调用它
$ rebar3 plugin_name
===> Fetching plugin_name
===> Compiling plugin_name
<PLUGIN OUTPUT>
参考
提供程序接口
每个提供程序都有以下可用选项
- name:任务的“用户友好”名称。
- module:任务的模块实现。
- hooks:用于前置和后置钩子的提供程序名称的二元组 (
{Pre, Post}
)。 - bare:指示任务是否可以由用户运行。应为
true
。 - deps:依赖项列表,需要在此提供程序之前运行的提供程序。您无需包含依赖项的依赖项。
- desc:任务的描述,由
rebar3 help
使用。 - short_desc:任务的一行简短描述,用于提供程序列表中。
- example:任务用法的示例,例如
"rebar3 my-provider args"
- opts:任务需要/理解的选项列表。每个选项的形式为
{Key, $Character, "StringName", Spec, HelpText}
,其中Key
是一个原子,用于稍后获取值;$Character
是选项的简写形式。因此,如果命令要输入为-c Arg
,则$c
是此字段的值。Spec
可以是类型 (atom
、binary
、boolean
、float
、integer
或string
),带默认值的类型 ({Type, Val}
) 或原子undefined
。
- profiles:提供程序要使用的配置文件。默认为
[default]
。 - namespace:提供程序注册的命名空间。默认为
default
,即主命名空间。
这些选项需要在创建提供程序时添加到其中。
提供程序具有以下实现
-module(provider_template).
-behaviour(provider).
-export([init/1, do/1, format_error/1]).
%% ===================================================================
%% Public API
%% ===================================================================
%% Called when rebar3 first boots, before even parsing the arguments
%% or commands to be run. Purely initiates the provider, and nothing
%% else should be done here.
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
Provider = providers:create([Options]),
{ok, rebar_state:add_provider(State, Provider)}.
%% Run the code for the plugin. The command line argument are parsed
%% and dependencies have been run.
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
{ok, State}.
%% When an exception is raised or a value returned as
%% `{error, {?MODULE, Reason}}` will see the `format_error(Reason)`
%% function called for them, so a string can be formatted explaining
%% the issue.
-spec format_error(any()) -> iolist().
format_error(Reason) ->
io_lib:format("~p", [Reason]).
可能的依赖项列表
所有依赖项都在默认命名空间中,除非另有说明
名称 | 功能 | 配置文件 | 还依赖于 |
---|---|---|---|
app_discovery | 探索用户应用程序并加载其配置。 | default | |
clean | 删除应用程序中编译后的 beam 文件。 | default | app_discovery |
compile | 编译应用程序的 .app.src 和 .erl 文件。 | default | lock |
cover | 分析覆盖率编译的文件。 | default | lock |
ct | 运行通用测试套件。 | test | compile |
deps | 列出依赖项。 | default | app_discovery |
dialyzer | 在项目上运行 Dialyzer 分析器。 | default | compile |
edoc | 使用 edoc 生成文档。 | default | app_discovery |
eunit | 运行 EUnit 测试。 | test | compile |
help | 显示任务列表或给定任务或子任务的帮助。 | default | |
install_deps | 下载依赖项。 | default | app_discovery |
lock | 锁定依赖项并添加 rebar.lock。 | default | install_deps |
new | 从模板创建新项目。 | default | |
pkgs | 列出可用的包。 | default | |
release | 构建项目的发布版本。 | default | compile |
report | 提供崩溃报告以发送到 rebar3 问题页面。 | default | |
shell | 在路径中使用项目应用程序和依赖项运行 shell。 | default | compile |
tar | 项目构建的发布版本的 tar 归档文件。 | default | compile |
update | 更新包索引。 | default | |
upgrade | 升级依赖项。 | default | |
version | 打印 rebar 和当前 Erlang 的版本。 | default | |
xref | 运行交叉引用分析。 | default | compile |
请注意,您可以依赖多个提供程序,但它们必须位于同一个命名空间中。
Rebar API
Rebar 带有一个名为 rebar_api
的模块,在编写提供程序时导出常用函数。函数包括
功能 | 用法 |
---|---|
abort() | 中断程序流程。 |
abort(FormatString, Args) | 中断程序流程;同时显示 ERROR 消息。 相当于调用 rebar_api:error(FormatString, Args) 然后调用 rebar_api:abort()。 |
console(FormatString, Args) | 打印到控制台。 |
info(FormatString, Args) | 使用严重性 INFO 记录日志。 |
warn(FormatString, Args) | 使用严重性 WARNING 记录日志。 |
error(FormatString, Args) | 使用严重性 ERROR 记录日志。 |
debug(FormatString, Args) | 使用严重性 DEBUG 记录日志。 |
expand_env_variable(InStr, VarName, RawVarValue) | 给定环境变量 FOO,我们希望扩展 InStr 中所有对它的引用。 引用可以有两种形式:$FOO 和 ${FOO}。$FOO 的形式以空格字符或行尾 (eol) 为界。 |
get_arch() | 返回“体系结构”作为形式为“$OTP_VSN-$SYSTEM_$ARCH-WORDSIZE”的字符串。 最终字符串将类似于“17-x86_64-apple-darwin13.4.0-8”或“17-x86_64-unknown-linux-gnu-8”。 |
wordsize() | 返回模拟器的真实字长,即指针的大小(以字节为单位),作为字符串。 |
add_deps_to_path(RebarState) | 项目的依赖项将添加到代码路径中。当调用工具并且需要对库具有全局状态访问权限时很有用。 |
restore_code_path(RebarState) | 将代码路径恢复为仅包含运行 Rebar3 及其插件所需的库。这是 Rebar3 的理想状态,以避免与用户提供的工具发生冲突。 |
ssl_opts(Url) | 返回要与 httpc 一起使用的ssl选项,以进行安全且经过验证的 HTTP 请求。 |
请注意,所有日志记录函数都会自动向记录的每个表达式添加换行符 (~n
)。
Rebar 状态操作
传递给插件提供程序的 State
参数可以通过 rebar_state
模块使用以下接口进行操作
功能 | 用法 |
---|---|
get(State, Key, [DefaultValue]) -> Value | 当 rebar.config 元素的形式为 {Key, Value} 时,获取其值。 |
set(State, Key, Value) -> NewState | 向 rebar 状态添加配置值。 |
lock(State) -> ListOfLocks | 返回已锁定依赖项的列表。 |
escript_path(State) -> Path | 返回 Rebar3 escript 的位置。 |
command_args(State) -> RawArgs | 返回传递给 rebar3 的参数。 |
command_parsed_args(State) -> Args | 返回传递给 rebar3 的参数,已解析。 |
deps_names(State) -> DepsNameList | 返回依赖项名称的列表。 |
project_apps(State) -> AppList | 返回应用程序列表。可以使用 rebar_app_info 处理这些应用程序。 |
all_deps(State) -> DepsList | 返回依赖项列表。可以使用 rebar_app_info 处理这些依赖项。 |
add_provider(State, Provider) -> NewState | 注册一个新的提供程序,其中 Provider 是调用 providers:create(Options) 的结果。要生效,此函数必须作为提供程序的 init/1 函数的一部分调用。它可以被多次调用,允许插件注册多个命令。 |
add_resource(State, {Key, Module}) -> NewState | 使用用于处理它的模块注册新的资源类型(例如 git、hg 等)。该资源必须实现 rebar_resource 行为。要生效,此函数必须作为提供程序的 init/1 函数的一部分调用。 |
操作应用程序状态
每个正在构建的应用程序(项目应用程序和依赖项)。所有 AppInfo 记录都可以在 State 中找到,并且可以通过 project_apps/1
和 all_deps/1
访问。
功能 | 用法 |
---|---|
get(AppInfo, Key, [DefaultValue]) -> Value | 获取应用程序 AppInfo 中定义的 Key 的值。 |
set(AppInfo, Key, Value) -> NewState | 向应用程序的记录添加配置值。 |
命名空间
对于可能需要多个命令且所有命令都适用于单一类型的任务(例如实现除 Erlang 之外的 BEAM 语言的工具套件)的插件,而不是让多个命令污染命令空间或需要 rebar3 mylang_compile
之类的前缀,rebar3 引入了对命名空间的支持。
插件可以声明为属于给定的命名空间。例如,ErlyDTL 编译器插件 在 erlydtl
命名空间下引入了 compile
命令。因此,它可以调用为 rebar3 erlydtl compile
。如果 erlydtl
命名空间有其他命令,例如 clean
,则可以将其链接为 rebar3 erlydtl clean, compile
。
从其他方面来说,命名空间的作用类似于 do
(rebar3 do compile, edoc
),但作用于非默认的命令集。
要声明命名空间,提供程序只需要在其配置列表中使用 {namespace, Namespace}
选项即可。该提供程序将自动注册新的命名空间,并且可以在此术语下使用。
`🚧
命名空间也适用于提供程序依赖项和钩子。
如果提供程序是给定命名空间的一部分,则其依赖项将在同一命名空间中搜索。因此,如果
rebar3 mytool rebuild
依赖于compile
,则将在mytool
命名空间中查找compile
命令。要使用默认的
compile
命令,依赖项必须声明为{default, compile}
,或者更一般地声明为{NameSpace, Command}
。相同的机制也适用于钩子。
教程
第一个版本
在本教程中,我们将演示如何从头开始编写一个基本的插件。该插件非常简单:它将在注释中查找 TODO:
行的实例,并将它们报告为警告。插件的最终代码可以在 bitbucket 上找到。
第一步是创建一个新的 OTP 应用,用于包含插件。
→ rebar3 new plugin todo desc="example rebar3 plugin"
...
→ cd todo
→ git init
Initialized empty Git repository in /Users/ferd/code/self/todo/.git/
src/todo.erl
文件将用于调用所有命令的初始化。目前我们只有一个 todo
命令。打开包含命令实现的 src/todo_prv.erl
文件,并确保你已准备好以下框架。
-module(todo_prv).
-behaviour(provider).
-export([init/1, do/1, format_error/1]).
-define(PROVIDER, todo).
-define(DEPS, [app_discovery]).
%% ===================================================================
%% Public API
%% ===================================================================
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
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 provider_todo"}, % How to use the plugin
{opts, []} % list of options understood by the plugin
{short_desc, "example rebar3 plugin"},
{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()) -> iolist().
format_error(Reason) ->
io_lib:format("~p", [Reason]).
这展示了所有基本内容。请注意,我们将 DEPS
宏保留为 app_discovery
值,表示插件至少应找到项目的源代码(不包括依赖项)。
在这种情况下,我们只需要在 init/1
中进行少量更改。以下是新的提供程序描述。
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 todo"}, % How to use the plugin
{opts, []}, % list of options understood by the plugin
{short_desc, "Reports TODOs in source code"},
{desc, "Scans top-level application source and find "
"instances of TODO: in commented out content "
"to report it to the user."}
]),
相反,大部分工作需要直接在 do/1
中完成。我们将使用 rebar_state
模块来获取我们需要的所有应用程序。可以通过调用 project_apps/1
函数来实现,该函数返回项目顶级应用程序的列表。
do(State) ->
lists:foreach(fun check_todo_app/1, rebar_state:project_apps(State)),
{ok, State}.
从高层次来看,这意味着我们将一次检查每个顶级应用程序(在处理版本时,通常可能有多个顶级应用程序)。
其余部分是特定于插件的填充代码,负责读取每个应用程序路径,并在其中读取代码,并在代码中的注释中查找“TODO:”的实例。
check_todo_app(App) ->
Paths = rebar_dir:src_dirs(rebar_app_info:opts(App)),
Mods = find_source_files(Paths),
case lists:foldl(fun check_todo_mod/2, [], Mods) of
[] -> ok;
Instances -> display_todos(rebar_app_info:name(App), Instances)
end.
find_source_files(Paths) ->
find_source_files(Paths, []).
find_source_files([], Files) ->
Files;
find_source_files([Path | Rest], Files) ->
find_source_files(Rest, [filename:join(Path, Mod) || Mod <- filelib:wildcard("*.erl", Path)] ++ Files).
check_todo_mod(ModPath, Matches) ->
{ok, Bin} = file:read_file(ModPath),
case find_todo_lines(Bin) of
[] -> Matches;
Lines -> [{ModPath, Lines} | Matches]
end.
find_todo_lines(File) ->
case re:run(File, "%+.*(TODO:.*)", [{capture, all_but_first, binary}, global, caseless]) of
{match, DeepBins} -> lists:flatten(DeepBins);
nomatch -> []
end.
display_todos(_, []) -> ok;
display_todos(App, FileMatches) ->
io:format("Application ~s~n",[App]),
[begin
io:format("\t~s~n",[Mod]),
[io:format("\t ~s~n",[TODO]) || TODO <- TODOs]
end || {Mod, TODOs} <- FileMatches],
ok.
仅使用 io:format/2
输出就可以了。
要测试插件,请将其推送到某个源代码存储库。选择其中一个项目,并在 rebar.config
中添加一些内容。
{plugins, [
{todo, {git, "[email protected]:ferd/rebar3-todo-plugin.git", {branch, "master"}}}
]}.
然后你可以直接调用它
→ rebar3 todo
===> Fetching todo
===> Compiling todo
Application merklet
/Users/ferd/code/self/merklet/src/merklet.erl
todo: consider endianness for absolute portability
Rebar3 将下载并安装插件,并确定何时运行它。编译完成后,可以随时再次运行它。
可选搜索依赖项
让我们稍微扩展一下。也许有时(在发布版本时),我们希望确保我们的任何依赖项都不包含“TODO:”。
为此,我们需要稍微解析一下命令行参数,并更改我们的执行模型。?DEPS
宏现在需要指定 todo
提供程序只能在依赖项安装之后运行。
-define(DEPS, [install_deps]).
我们可以在用于在 init/1
中配置提供程序的列表中添加选项。
{opts, [ % list of options understood by the plugin
{deps, $d, "deps", undefined, "also run against dependencies"}
]},
然后我们可以实现开关来确定要搜索的内容。
do(State) ->
Apps = case discovery_type(State) of
project -> rebar_state:project_apps(State);
deps -> rebar_state:project_apps(State) ++ lists:usort(rebar_state:all_deps(State))
end,
lists:foreach(fun check_todo_app/1, Apps),
{ok, State}.
[...]
discovery_type(State) ->
{Args, _} = rebar_state:command_parsed_args(State),
case proplists:get_value(deps, Args) of
undefined -> project;
_ -> deps
end.
使用 rebar_state:command_parsed_args(State)
找到 deps
选项,它将返回命令行上“todo”之后的术语属性列表,并将负责验证标志是否被接受。其余部分可以保持不变。
推送插件的新代码,并在具有依赖项的项目上再次尝试。
===> Fetching todo
===> Compiling todo
===> Fetching bootstrap
===> Fetching file_monitor
===> Fetching recon
[...]
Application dirmon
/Users/ferd/code/self/figsync/apps/dirmon/src/dirmon_tracker.erl
TODO: Peeranha should expose the UUID from a node.
Application meck
/Users/ferd/code/self/figsync/_deps/meck/src/meck_proc.erl
TODO: What to do here?
TODO: What to do here?
Rebar3 现在将在运行插件之前选择依赖项。
你还可以看到帮助信息将为你自动完成。
→ rebar3 help todo
Scans top-level application source and find instances of TODO: in commented out content to report it to the user.
Usage: rebar todo [-d]
就是这样,todo 插件现在已经完成了!它已准备好发布并包含在其他存储库中。
添加更多命令
要向同一个插件添加更多命令,只需在主模块的 init
函数中添加条目即可。
-module(todo).
-export([init/1]).
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
%% initialize all commands here
{ok, State1} = todo_prv:init(State),
{ok, State2} = todo_other_prv:init(State1),
{ok, State2}.
Rebar3 将从那里获取它。