Pocket BEAM
Compile an Elixir CLI into one executable. Copy it to another compatible machine and run it—no installed OTP, no release directory, no runtime extraction.
Pocket is an experimental Mix frontend for
Wojtek Mach's elixiraotc. It uses the
existing BEAM execution model and the backend's ahead-of-time code generation.
It is not a Rust runtime or an escript wrapper.
Try it
You need Elixir 1.20+ to run the Mix frontend and curl for the initial toolchain
download. The application itself is compiled under the pinned toolchain, not
your host OTP.
To run the example from a source checkout:
git clone https://github.com/jeregrine/pocket.git
cd pocket
cd examples/hello
mix pocket.build --install
./dist/hello
./dist/hello --version
printf 'hello from stdin\n' | ./dist/hello --echo
--install explicitly permits the first toolchain download. Alternatively,
install it separately with mix pocket.toolchain, then use mix pocket.build.
Subsequent builds reuse the verified cache.
The result is dist/hello, plus dist/hello.manifest.json for auditing. Only
the executable is needed to run the program. The manifest is a build inventory,
not a runtime dependency or a complete standards-compliant SBOM.
Add it to a project
Install the experimental prerelease from Hex:
def project do
[
app: :my_tool,
version: "0.1.0",
pocket: [main: MyTool.CLI],
deps: [{:pocket, "~> 0.1.0-alpha.1", runtime: false}]
]
end
runtime: false matters: the builder must not become a dependency of the executable.
For local development on Pocket itself, use {:pocket, path: "../pocket", runtime: false} instead.
defmodule MyTool.CLI do
def main(args) do
IO.puts("Arguments: #{inspect(args)}")
:ok
end
end
mix deps.get
mix pocket.build --install
./dist/my_tool one two
Pocket never runs deps.get for you. Fetch and review dependencies separately,
then commit mix.lock and the generated JSON pocket.lock.
Configuration
pocket: [
main: MyTool.CLI, # required: module or {module, function, args}
name: "my-tool", # optional: defaults to the application name
shutdown_timeout: 5_000 # optional: milliseconds, from 1 to 60_000
]
The module form calls MyTool.CLI.main(argv). Alternatively, choose a function
and optional additional arguments:
pocket: [main: {MyTool.CLI, :run, []}]
This calls MyTool.CLI.run(argv). Pocket always passes CLI arguments first,
followed by the configured additional arguments. For example, to call
MyTool.CLI.run(argv, "prefix", verbose: true):
pocket: [main: {MyTool.CLI, :run, ["prefix", [verbose: true]]}]
The invocation is apply(module, function, [argv | args]), not a bare apply/3
of the configured tuple. CLI arguments are also available through System.argv/0.
Both forms use the same exit-status and cleanup contract.
The option is now named main, replacing the prototype's main_module.
mix pocket.build --output dist/my-tool
mix pocket.build --offline
--offline prohibits Pocket's toolchain downloads. It is not a network sandbox
for macros, dependency build scripts, or application code.
CLI behavior
- CLI arguments are strings, passed as the first argument to either entry-point
form and available through
System.argv/0. Ordinary arguments are not VM flags. - The project's runtime applications start before the entry point.
- Returning
:okexits successfully;{:error, 1..255}selects a nonzero exit. - An uncaught exception produces a diagnostic on stderr and exits with status 1.
- Standard input works in pipes. Standard output/error use Unicode encoding.
- Returning ends the command even if it spawned background BEAM processes.
- Shutdown runs OTP application cleanup with a bounded deadline, including Logger shutdown. Cleanup may be cut short if it exceeds that deadline.
- Ctrl-C terminates the process instead of showing the Erlang BREAK menu.
- The CLI profile currently uses one normal BEAM scheduler and does not enable distribution. This is a deliberate small-CLI default, not a server profile.
How a build works
- Validate the project and
pocket.lock. - Download if explicitly permitted, then verify the native toolchain's SHA-256.
- Run a small build worker under that toolchain.
- Compile the project and dependencies in production mode under
_build/pocket/<toolchain>/<target>, separate from normal development builds. - Resolve the runtime application closure from OTP
.appmetadata. - Package its modules, consolidated protocols, compile-time application configuration, and a small CLI entry-point module.
- Record AOT code and build the native executable with the embedded release.
The recording pass does not start the application or invoke the entry point. Compilation still executes macros and build scripts, as ordinary Elixir compilation does.
Mix, Hex, IEx, and the Pocket builder are excluded from application executables. The backend compiler contains build tools; the artifacts it produces here do not. The first version trims applications, not individual modules or functions.
The bootstrap currently contains OTP 29 / ERTS 17.0.6 and Elixir 1.21.0-dev.
The exact upstream artifact and SHA-256 for each platform are pinned in
priv/toolchain.json; generated build manifests record the actual runtime versions.
Toolchain ownership and updates
Pocket consumes toolchain releases; it does not maintain OTP patches or build OTP
in its normal CI. Source-build and release automation belong in elixiraotc.
Until a reviewed release is available, the bootstrap pins existing upstream
artifacts at an immutable Git commit.
pocket.lock must match the reviewed manifest compiled into this Pocket version.
An edited lock file cannot redirect downloads to an arbitrary server. Cached
toolchains are verified before execution and stored separately by toolchain ID.
Corrupt cache entries fail closed instead of being run or silently replaced.
When upgrading to a Pocket version with a new toolchain, review its manifest,
remove the old pocket.lock, and run mix pocket.toolchain to generate the new
lock and fetch the new toolchain. Commit the lock diff. Nothing follows a mutable
latest URL.
Supported scope and security limits
This is a working prototype, not a production-hardened runtime.
- Native targets: macOS and Linux, ARM64 and x86-64. No cross-compilation or Windows.
- A single executable is not a promise of universal static linking. System-library and OS compatibility still matter; Linux bootstrap builds come from Ubuntu 24.04.
- Third-party
priv/assets and native libraries are rejected, not extracted or silently dropped. Packaging custom NIFs and filesystem resources is future work. - Only OTP applications available in the chosen toolchain can be used. The bootstrap is not a full OTP installation; missing runtime applications fail the build.
runtime.exs, custom release definitions, and umbrella projects are rejected. Compile-time configuration is embedded; read runtime secrets from the environment in your application, not from build-time config.- Dynamic dependency installation is unsupported. Dependencies must be in the build's application closure; no compatibility claim is made for runtime module discovery or arbitrary code loading.
- The upstream native launcher reserves
-erlaot:*arguments, including recording and helper modes, before Elixir starts. Those arguments are not ordinary application arguments. Do not use these executables as privileged/setuid programs or expose their argument vector as an untrusted execution service. - The experimental native archive loader has not been audited here. Hash pinning establishes artifact identity, not the safety of its code or its build provenance.
- Builds are not sandboxes. Untrusted projects and dependencies need isolated, unprivileged build environments without secrets.
- Bundled runtime vulnerabilities require rebuilding and redistributing your CLI.
dist/*.manifest.jsoninventories application versions, packaged paths, and artifact/toolchain digests. It is not a complete license, transitive native-library, vulnerability, or source-provenance report.
Development
mix format --check-formatted
mix compile --warnings-as-errors
mix test
# Explicitly install the example's toolchain before offline integration tests.
(cd examples/hello && mix pocket.toolchain)
mix test --include integration
Integration tests copy just the executable to a fresh directory and check production configuration, startup/shutdown callbacks, stdin/stdout/stderr, exit codes, Ctrl-C, Unicode arguments, bounded cleanup, background processes, and absence of runtime extraction or bundled Mix/Hex.
License
Apache-2.0. See LICENSE. The separately downloaded OTP/Elixir toolchain has its own upstream license notices.