构建插件

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 可以是类型 (atombinarybooleanfloatintegerstring),带默认值的类型 ({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/1all_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 将从那里获取它。

最后修改时间:2024年3月10日:shell hooks env vars (ba9462f)