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

How a build works

  1. Validate the project and pocket.lock.
  2. Download if explicitly permitted, then verify the native toolchain's SHA-256.
  3. Run a small build worker under that toolchain.
  4. Compile the project and dependencies in production mode under _build/pocket/<toolchain>/<target>, separate from normal development builds.
  5. Resolve the runtime application closure from OTP .app metadata.
  6. Package its modules, consolidated protocols, compile-time application configuration, and a small CLI entry-point module.
  7. 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.

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.