rebar3 构建与发布:Erlang 工程化的完整工具箱

系统讲解 rebar3 的项目结构、依赖与锁文件、profiles 配置体系、escript 与 release 打包、插件生态与 CI 集成,构建可复现的 Erlang 交付流水线。

rebar3 是 Erlang 生态的事实标准构建工具,它把「编译、拉依赖、跑测试、打包、生成 release、发布」串成一条可复现的流水线。在 OTP 世界里,一个项目往往包含多个应用(application)、若干依赖、不同环境的配置差异以及需要交付给运维的 release 包,如果没有统一工具,这些工作会退化成一堆 Makefile 和手工脚本。rebar3 的价值在于:它既是构建系统,也是依赖管理器,还是 release 生成器,并且通过插件机制可以无限扩展。本文将围绕项目结构、依赖与锁、profiles、打包与插件六个维度,给出一套可直接落地的工程实践。

一、rebar3 与 Erlang 构建体系

1.1 构建工具要解决什么问题

Erlang 的编译单元是模块(.erl → .beam),但生产交付的最小单元是应用(application),而部署的最小单元通常是 release(若干应用 + 运行时)。这三级结构决定了构建工具必须处理:

  • 模块依赖顺序与 -include 路径解析;
  • 应用之间的 applications 依赖声明与启动顺序;
  • 第三方依赖的获取、版本锁定与传递依赖;
  • 不同环境(开发/测试/生产)下的编译参数与配置差异;
  • 最终 release 的目录布局、启动脚本与运行时参数。

1.2 rebar3 的核心能力

能力命令说明
编译rebar3 compile编译 src/ 下所有应用
依赖rebar3 get-deps拉取并锁定依赖
测试rebar3 eunit / rebar3 ct单元测试与 Common Test
覆盖率rebar3 cover生成 cover 覆盖率报告
类型检查rebar3 dialyzer构建 PLT 并做 success typing 分析
打包rebar3 escriptize生成单文件可执行 escript
发布rebar3 release生成可部署的 release 目录
静态分析rebar3 xref未定义函数、废弃调用检查

1.3 安装与版本管理

%% 方式一:从源码构建(推荐锁定版本)
git clone https://github.com/erlang/rebar3.git
cd rebar3 && ./bootstrap

%% 方式二:用已安装的 rebar3 自举
rebar3 local install && export PATH=$HOME/.cache/rebar3/bin:$PATH
rebar3 version

工程建议:在项目根目录提交一个 rebar3 脚本包装(或 .tool-versions),让 CI 与本地使用同一版本。rebar3 版本差异会直接影响 release 的目录布局与默认 erl_opts。

二、项目结构与依赖管理

2.1 标准目录布局

my_service/
├── apps/                    % umbrella 多应用布局
│   ├── my_service_core/
│   │   ├── src/
│   │   │   ├── my_service_core.app.src
│   │   │   ├── my_service_core_app.erl
│   │   │   └── my_service_core_sup.erl
│   │   ├── include/
│   │   ├── priv/            % 运行时资源(模板、证书、静态文件)
│   │   └── test/
│   └── my_service_web/
├── config/
│   ├── sys.config
│   └── vm.args
├── rebar.config
├── rebar.lock
└── _build/                  % 构建产物(勿提交)

apps/ 下每个子目录都是一个独立 OTP 应用,可以单独编译、单独复用。单应用项目则把 src/ 直接放在根目录。

2.2 依赖声明

%% rebar.config
{deps, [
    %% Hex 上的包,指定版本约束
    {cowboy, "2.10.0"},
    {jsx, "3.1.0"},
    {poolboy, "1.5.2"},
    %% git 依赖,可锁定 tag 或 commit
    {recon, {git, "https://github.com/ferd/recon.git", {tag, "2.5.3"}}},
    %% 本地路径依赖(开发期)
    {my_lib, {path, "../my_lib"}}
]}.
依赖类型语法适用场景
Hex 包{name, "1.2.3"}公开库,版本语义清晰
git tag{git, Url, {tag, "v1.0"}}未发布到 Hex 的库
git branch{git, Url, {branch, "main"}}跟踪上游最新(慎用)
git ref{git, Url, {ref, "abc123"}}锁定到具体提交
本地路径{path, "..."}monorepo 内部库

2.3 锁文件与可复现构建

rebar.lock 记录了每个依赖解析后的精确版本与哈希,是构建可复现的关键:

%% rebar.lock(自动生成,必须提交到版本控制)
{"1.2.0",
[{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.10.0">>},0},
 {<<"cowpboy">>,{pkg,<<"cowlib">>,<<"2.12.1">>},1},
 {<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},1}]}.
[{pkg_hash,[
 {<<"cowboy">>, <<"1E7B...">>},
 {<<"cowlib">>, <<"A1C2...">>}]}].
%% 升级单个依赖并更新锁文件
rebar3 upgrade cowboy

%% 升级所有依赖(危险,需回归测试)
rebar3 upgrade --all

%% 用锁文件精确还原(CI 必用)
rebar3 compile

踩坑:rebar3 upgrade 会重写 rebar.lock,若在 CI 中执行会导致构建结果漂移。CI 只应执行 compile / release,绝不执行 upgrade。

2.4 依赖冲突与覆盖

当两个依赖引用同一库的不同版本时,rebar3 采用「顶层优先」策略——rebar.config 中直接声明的版本胜出:

%% 强制覆盖传递依赖的版本
{overrides, [
    {override, jsx, [{deps, [{jsx, "3.1.0"}]}]},
    {add, my_dep, [{erl_opts, [debug_info]}]}
]}.

三、profiles 与配置体系

3.1 profile 的继承与合并

profile 是 rebar3 最强大的配置机制:它允许为不同场景定义覆盖式配置,未覆盖的项自动继承默认 profile。

%% rebar.config
{erl_opts, [debug_info, warnings_as_errors]}.

{profiles, [
    {test, [
        {erl_opts, [nowarn_export_all]},
        {deps, [{meck, "0.9.2"}]}
    ]},
    {prod, [
        {erl_opts, [no_debug_info, compressed]},
        {relx, [{dev_mode, false}, {include_erts, true}]}
    ]},
    {dev, [
        {erl_opts, [debug_info, {parse_transform, lager_transform}]}
    ]}
]}.

调用方式:rebar3 as prod release、rebar3 as test eunit。

profile用途关键差异
default日常开发debug_info 开启
test测试引入 meck,关闭部分告警
prod生产发布剥离调试信息,内嵌 ERTS
dev本地调试保留全部符号,热加载友好

3.2 多 profile 叠加

profile 可以叠加,后写的覆盖先写的:

rebar3 as prod,test ct        % 先 prod 后 test,test 优先

3.3 sys.config 与配置分层

%% config/sys.config —— 生产环境配置
[
 {my_service, [
     {port, 8080},
     {db_host, "10.0.1.5"},
     {pool_size, 32}
 ]},
 {kernel, [
     {logger_level, info}
 ]},
 {sasl, [
     {sasl_error_logger, false}
 ]}
].

3.4 erl_opts 关键选项

{erl_opts, [
    debug_info,                    % 保留调试信息(dialyzer/热更新必需)
    warnings_as_errors,            % 警告即错误,CI 强制
    {platform_define, "^2[4-6]", 'OTP_24_PLUS'},
    {i, "include"},                % 追加 include 路径
    {parse_transform, lager_transform}
]}.

warnings_as_errors 建议只在 CI 的 test profile 打开,本地开发保持宽松,避免因未使用变量阻塞开发节奏。

四、escript 与 release 打包

4.1 escript:单文件命令行工具

escript 把 BEAM 文件与 ERTS 打包成一个可直接执行的脚本,适合运维小工具:

%% rebar.config
{escript_name, my_cli}.
{escript_main_module, my_cli_main}.
{escript_incl_apps, [my_service_core]}.

%% src/my_cli_main.erl
-module(my_cli_main).
-export([main/1]).

main(["migrate" | _Args]) ->
    io:format("running migrations...~n"),
    ok = my_service_core:migrate();
main(["--help"]) ->
    io:format("usage: my_cli <migrate|status>~n");
main(Args) ->
    io:format("unknown command: ~p~n", [Args]),
    halt(1).
rebar3 escriptize
./_build/default/bin/my_cli migrate

4.2 release 结构与 relx

release 把多个应用与 ERTS 组装成自包含目录,无需目标机器安装 Erlang:

{relx, [
    {release, {my_service, "1.0.0"}, [
        my_service_core,
        my_service_web,
        sasl
    ]},
    {mode, prod},
    {include_erts, true},          % 内嵌运行时
    {include_src, false},
    {sys_config, "./config/sys.config"},
    {vm_args, "./config/vm.args"},
    {extended_start_script, true}  % 生成增强启动脚本
]}.

生成的目录结构:

_build/prod/rel/my_service/
├── bin/my_service          % 启动脚本
├── bin/my_service-1.0.0    % 版本化脚本(支持滚动升级)
├── lib/                    % 各应用 .beam + priv
├── releases/
│   ├── 1.0.0/
│   │   ├── sys.config
│   │   ├── vm.args
│   │   └── my_service.rel
│   └── RELEASES            % 版本清单
└── erts-14.2/              % 内嵌 ERTS

4.3 vm.args 与运行时调优

%% config/vm.args
-name my_service@127.0.0.1
-setcookie my_secret_cookie
+K true
+A 128
+P 1048576
+sbwt very_short
+swt very_low
+MBas aobf
参数含义建议
+K true启用 kernel poll高连接数必开
+A 128异步线程池按 IO 密度调
+P 1048576最大进程数长连接服务放大
+MBas aobf二进制分配器策略减少碎片

4.4 启动脚本与热升级衔接

bin/my_service start          % 后台启动
bin/my_service foreground     % 前台运行(容器首选)
bin/my_service console        % 带 shell 启动
bin/my_service ping           % 探活
bin/my_service stop           % 优雅停止
%% 热升级:上传新版本 release 目录后
bin/my_service versions && bin/my_service upgrade "1.1.0"
bin/my_service downgrade "1.0.0"

热升级的 .appup 与 code_change/3 细节可参见 https://plumephp.com/erlang-hot-code-upgrade/,release 打包与部署策略另见 https://plumephp.com/erlang-otp-application-design/。

五、插件生态与 CI 集成

5.1 常用插件

{plugins, [
    {rebar3_lint, "3.2.0"},          % elvis 风格检查
    {rebar3_hex, "7.0.7"},           % Hex 发布
    {rebar3_eqc, "1.1.0"},           % QuickCheck
    {rebar3_ex_doc, "0.2.20"}        % 文档生成
]}.
插件作用
rebar3_lint基于 elvis 的代码规范检查
rebar3_hex打包并发布到 Hex.pm
rebar3_coveralls覆盖率上报
rebar3_properPropEr 属性测试

5.2 自定义插件

-module(rebar3_myplugin).
-export([init/1]).

init(State) ->
    Provider = providers:create([
        {name, deploy},
        {module, ?MODULE},
        {bare, true},
        {deps, [app_discovery]},
        {example, "rebar3 deploy"},
        {opts, []},
        {short_desc, "Deploy to remote host"},
        {desc, "Package release and rsync to target"}
    ]),
    {ok, rebar_state:add_provider(State, Provider)}.

5.3 CI 流水线

# .github/workflows/ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: erlef/setup-beam@v1
        with:
          otp-version: '26.2'
          rebar3-version: '3.23'
      - name: Compile
        run: rebar3 as test compile
      - name: Test
        run: rebar3 as test do eunit, ct
      - name: Dialyzer
        run: rebar3 dialyzer
      - name: Xref
        run: rebar3 xref
      - name: Release
        run: rebar3 as prod release
      - uses: actions/upload-artifact@v4
        with:
          name: release
          path: _build/prod/rel/my_service

CI 缓存:缓存 _build/default/lib 与 PLT 文件可显著提速。PLT 构建耗时长,务必缓存 _build/default/rebar3_*_plt。

六、总结

rebar3 把 Erlang 工程从「手工 Makefile 时代」带入了可复现构建时代。掌握它的关键路径是:

  • 项目结构:apps/ 多应用布局让代码可拆分、可复用,priv/ 承载运行时资源;
  • 依赖与锁:rebar.lock 必须提交,CI 只 compile 不 upgrade,用 overrides 化解版本冲突;
  • profiles:用 test / prod 分层覆盖,warnings_as_errors 只在 CI 打开;
  • 打包:escript 适合小工具,release 适合服务交付,include_erts 让部署免装 Erlang;
  • 插件与 CI:把 lint、dialyzer、xref、release 串成一条流水线,产出可验证的制品。

把这五件事做扎实,Erlang 项目的构建与交付就能像任何现代语言一样工程化。构建产物的运行期表现如何,则取决于对 BEAM 运行时与调度器的理解,可继续阅读 https://plumephp.com/erlang-process-scheduling-beam/ 与 https://plumephp.com/erlang-production-cases/。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「erlang」更多文章

  1. RabbitMQ 与消息中间件:AMQP 模型与可靠投递
  2. Erlang 数据库集成与连接池实战
  3. Dialyzer 与类型规范:Erlang 静态分析的工程实践