MEP-46 research note 10, Build system: rebar3, mix, escript, releases
Author: research pass for MEP-46. Date: 2026-05-23 (GMT+7).
This note specifies how mochi build --target=beam-... integrates with the OTP build ecosystem: when to drive erlc directly, when to emit a rebar3 project, when a mix.exs, how to package as escript vs release, and how to handle deps.
1. The OTP build ecosystem
Three main build tools coexist:
erlc: the OTP compiler driver. Single-file, single-pass. The lowest level.rebar3(Tristan Sloughter et al., since 2014, v3.24 in 2024): the de facto Erlang build tool. Hex.pm integration, plugins, profile system, release support viarelx(now built-in). Most production Erlang uses rebar3.mix(Elixir, since 2014): the Elixir build tool. Hex.pm integration. Works for Erlang projects via themix new --module ...anddef depssettings. Heavier than rebar3; usually only used for Elixir projects.
The MEP-46 build driver targets rebar3 as the OTP integration; Mochi-built artifacts include a rebar.config so Erlang devs can use the result naturally.
2. The mochi build --target=beam-... matrix
| Target | Output |
|---|---|
beam-erlc | A directory of .beam files + .app resource. The "library" output. |
beam-escript | A single executable file (the escript). |
beam-release | A release tarball (.tar.gz) or directory. |
beam-rebar3-project | A directory layout rebar3 can build from. |
beam-mix-project | A directory layout mix can build from. |
beam-atomvm | A directory of .beam files + a packaging script for AtomVM. |
The default for mochi build on a Mochi project's main.mochi is beam-escript. For a Mochi project with a mochi.toml declaring mode = "service", the default is beam-release.
3. Direct erlc path (no project)
For a single-file Mochi program:
$ mochi build --target=beam-erlc main.mochi
The build driver:
- Compiles
main.mochito Core Erlang viacerl(see [[05-codegen-design]]). - Writes intermediate
.corefiles to_build/beam/core/. - Calls
compile:forms({c_module, ...}, [from_core, debug_info, return_errors, return_warnings])via an embeddederl(the build driver spawns a one-shoterl -noshell -eval ... -s init stop). - Writes
.beamfiles to_build/beam/ebin/. - Writes
mochi_user_main.app(the application resource file).
Output: _build/beam/ebin/*.beam.
4. rebar3 project layout
For full projects, mochi build --target=beam-rebar3-project emits a standard rebar3 directory:
my_proj_beam/
├── rebar.config
├── rebar.lock
├── src/
│ ├── my_proj.app.src % Application resource (templated)
│ ├── mochi_user_<m>.erl % One per Mochi module
│ └── ...
├── priv/ % Static assets (Mochi `bundle` data)
└── README.md
rebar.config lists Mochi runtime as a Hex dep:
{deps, [
{mochi, "0.1.0"} % from Hex
]}.
{escript_main_app, my_proj}.
{escript_emu_args, "%%! -escript main mochi_escript_main\n"}.
{relx, [
{release, {my_proj, "0.1.0"}, [my_proj, mochi, sasl]},
{dev_mode, false},
{include_erts, true},
{extended_start_script, true}
]}.
{profiles, [
{prod, [{relx, [{dev_mode, false}, {include_erts, true}]}]}
]}.
The user then runs:
rebar3 compileto build all.beam.rebar3 escriptizeto build the escript.rebar3 as prod releaseto build a release.rebar3 dialyzerto type-check (Dialyzer + Mochi-emitted-spec).
This does not require Mochi to be installed on the build machine: once the rebar3 project is emitted, it's standard Erlang. Mochi developers can hand off projects to Erlang teams seamlessly.
5. .erl source emission for handoff
The codegen produces Core Erlang via cerl. When emitting to a rebar3 project, we also pretty-print to .erl alongside the .beam files. Reasons:
- Erlang devs reading the rebar3 project can see source.
rebar3 dialyzerparses.erl, not.beam's debug_info chunks (it can do either; .erl is the canonical path).- Stack traces show source lines mapped from the
Linechunk, and the.erllets the user click through.
The pretty-printer uses OTP's erl_prettypr for the abstract format; we round-trip Core Erlang to abstract format via cerl:to_records and erl_syntax:revert. This is a known-stable path used by Concuerror and other Core Erlang tools.
6. escript packaging
mochi build --target=beam-escript produces a single-file executable:
#!/usr/bin/env escript
%%! -escript main mochi_escript_main
%%
%% Generated by mochi 0.1.0
%%
PK\x03\x04... (a zip archive containing .beam files)
Composition:
- A shebang line (
#!/usr/bin/env escript). - A
%%!line with emulator args, including-escript main <Module>pointing at the entry module. - A zip archive (BEAM
escripts use zip as the inner format) containing:- All user
mochi_user_*.beammodules. - All Mochi runtime
mochi_*.beammodules (linked statically). - All third-party deps'
.beamfiles.
- All user
Tooling: escript:create("output", [shebang, comment, {emu_args, "..."}, {archive, ArchiveData, []}]).
Size: 2-10 MB depending on user code and deps.
Startup: ~50ms cold (unzips into a tmp dir, loads modules), ~10ms warm (escript cache).
The escript does not include ERTS; the user must have erl on $PATH. For a self-contained escript, use the release target.
7. Release packaging
mochi build --target=beam-release uses relx (now bundled with rebar3):
$ mochi build --target=beam-release
# Internally: rebar3 as prod release
Output: _build/prod/rel/my_proj/:
my_proj/
├── bin/
│ ├── my_proj % Start script (sh)
│ ├── my_proj.cmd % Start script (Windows)
│ └── ...
├── erts-15.0/ % Bundled ERTS
├── lib/ % All OTP apps + user app
│ ├── kernel-9.0/
│ ├── stdlib-5.0/
│ ├── sasl-4.2/
│ ├── mochi-0.1.0/
│ └── my_proj-0.1.0/
├── releases/
│ └── 0.1.0/
│ ├── my_proj.rel
│ ├── sys.config
│ ├── vm.args
│ ├── start.boot
│ └── start_clean.boot
└── VERSION
Tarball: _build/prod/rel/my_proj-0.1.0.tar.gz (~30-80 MB depending on apps).
Startup: ~300ms cold, ~50ms warm. Includes SASL boot, the user's supervision tree, telemetry.
8. Dependency management
Mochi's package manager (mochi pkg) handles Mochi-source dependencies. For BEAM-target builds, additional dependency surfaces:
- Erlang/Elixir Hex packages: usable via
extern "Erlang" mod <name>declarations in Mochi source. The build driver adds them torebar.config'sdeps. - System libs: NIFs that need C libs (we avoid these by default; see [[04-runtime]] §9).
Hex.pm is the canonical registry for Erlang/Elixir; rebar3 has Hex support built in. The MEP-46 publishes the mochi runtime library to Hex as mochi (Apache-2.0 license, matching the rest of the project).
9. mix integration
For users who prefer Elixir tooling, mochi build --target=beam-mix-project emits:
my_proj_mix/
├── mix.exs
├── lib/
│ ├── mochi_user_*.ex % Pretty-printed as Elixir...
│ └── (or .erl files; mix supports both)
├── test/
└── README.md
mix.exs:
defmodule MyProj.MixProject do
use Mix.Project
def project, do: [app: :my_proj, version: "0.1.0", deps: deps()]
def application, do: [mod: {:mochi_user_main, []}, extra_applications: [:logger]]
defp deps, do: [{:mochi, "~> 0.1"}]
end
This path is less polished than rebar3 (mix expects Elixir source primarily) but unblocks users in Elixir-heavy organisations.
10. AtomVM packaging
mochi build --target=beam-atomvm:
- Compile to
.beamas usual. - Lint the modules against the AtomVM compat profile (see [[07-erlang-target-portability]] §4). Reject any use of
pg,httpc,cryptobeyond AtomVM's supported subset. - Bundle into an
.avm(AtomVM archive) using theatomvm_packbeamtool (Python script bundled with AtomVM). - Optionally flash to a connected device (
mochi build ... --flash esp32callsatomvm_packbeam --boot ...).
Output: _build/atomvm/my_proj.avm (~50-500 KB).
11. Profiles
Like rebar3 profiles, Mochi has build profiles:
dev(default): debug_info, no optimisation, sourcemaps.test: dev + eunit/common_test enabled, dialyzer on.prod: full optimisation, debug_info kept (always; see [[02-design-philosophy]]).min: prod minus debug_info (for size-constrained deployments only; sacrifices stack traces).
Selected via mochi build --profile prod.
12. Watch mode
mochi build --watch runs the build incrementally, recompiling changed files. The driver:
- Tracks file mtimes of
.mochisources. - Maintains a dependency graph (Mochi module imports).
- On change, recompiles affected modules to
.beamand (if release-mode) hot-loads them into a running BEAM viacode:load_file/1.
The hot-load path is the same as the OTP sync library and is the basis for mix.recompile and rebar3 ct --auto.
13. Tests
Mochi test "..." { ... } blocks compile to eunit test functions (see [[11-testing-gates]]). The build driver emits a mochi_test_*_SUITE.erl (Common Test format) for property tests and a mochi_test_*_eunit.erl for unit tests.
mochi test --target=beam calls rebar3 ct or rebar3 eunit under the hood.
14. Documentation
Mochi doc "..." and /// blocks compile to OTP 27 -doc attributes. rebar3 docs then produces an HTML doc site via ex_doc (the de facto OTP/Elixir doc generator).
Mochi-emitted docs include cross-references to the Mochi source line that produced each declaration, using a custom [mochi-src:<file>:<line>] link rewritten by ex_doc's post-processing hook.
15. CI integration
The Mochi build driver emits a .github/workflows/beam.yml template:
name: BEAM CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-22.04
strategy:
matrix:
otp: ['27.0', '27.latest', '28.latest']
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
otp-version: ${{ matrix.otp }}
rebar3-version: '3.24'
- run: rebar3 compile
- run: rebar3 dialyzer
- run: rebar3 eunit
- run: rebar3 ct
This is dropped into the rebar3-project output if --ci=github is set.
16. Release upgrades
Release upgrades (relup/appup) are a Phase 5+ feature. The build driver in v0.1 produces fresh releases only; no upgrade scripts. Users upgrade by stopping the old release and starting the new one.
For v0.2+, the driver will diff exported functions between releases and generate appup files automatically.
17. Cross-compilation note
Release builds bundle ERTS, which is platform-specific. To build a Linux release on macOS, use Docker:
$ mochi build --target=beam-release --docker linux/amd64
Internally, this runs docker run --rm -v $PWD:/work erlang:27-alpine bash -c "cd /work && rebar3 as prod release". The Mochi runtime image (mochilang/mochi:beam-otp27) has rebar3 pre-installed.
18. Tooling integration: LSP
The Mochi LSP server (mochi lsp) reads Mochi source, compiles to Core Erlang in memory, and exposes diagnostics from both the Mochi type checker and OTP's erl_lint warnings. Hover and go-to-definition use the Dbgi chunk, which the in-memory compile populates.
For BEAM-specific diagnostics (e.g. "uses pg not available on AtomVM"), the LSP runs the AtomVM compat lint when the target is set to beam-atomvm.
19. Verification: round-trip with rebar3
For every new MEP-46 codegen feature, the test suite runs the full round-trip:
- Compile Mochi →
.beamvia the in-process build. - Emit a rebar3 project.
- Run
rebar3 compilefrom scratch on the emitted project. - Verify both
.beamfiles are bit-identical.
This catches drift between the in-process and rebar3-driven paths.
20. Summary
The build system reuses the OTP ecosystem at every level:
- erlc for one-off compiles.
- rebar3 for everything multi-file.
- mix as a secondary integration for Elixir-heavy orgs.
- escripts for CLI tools.
- relx releases for daemons.
- AtomVM packaging for embedded.
- ex_doc for docs.
- Dialyzer + eqWAlizer for type checking.
- eunit + common_test for tests.
We add Mochi-specific glue (mochi.toml, the Mochi compiler driver) but contribute nothing new to the BEAM build space; that's the point.
Sources
rebar3docs. https://rebar3.orgmixdocs. https://hexdocs.pm/mix/relxdocs. https://erlware.github.io/relx/escriptreference. https://www.erlang.org/doc/man/escript.htmlex_doc. https://hexdocs.pm/ex_doc/- Tristan Sloughter, "rebar3 internals" Code BEAM 2022.
- Saša Jurić, "Releases without tears." Code BEAM 2024.
- AtomVM packaging. https://www.atomvm.net/doc/main/build-instructions.html
- erlef/setup-beam GitHub Action. https://github.com/erlef/setup-beam