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 的testprofile 打开,本地开发保持宽松,避免因未使用变量阻塞开发节奏。
四、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_proper | PropEr 属性测试 |
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/。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。