Elpe documentation
Elpe is a universal package installer and build system: it consumes unmodified
packages from conventional distributions (Ubuntu .deb, Homebrew bottles, PyPI
wheels, npm tarballs, crates, …) and builds your code against them in strict,
reproducible isolation — the immutable-store ideas of Nix without bootstrapping a
whole distribution. Build recipes are typed OCaml programs; the backend that runs
them is Rust.
At a glance — two frontends feed a federation of peer daemons. Every node runs
the same backend-agnostic server (Elpe::serve) paired with one isolation
backend; the daemon a frontend dials places each build across the
compilation farm and streams the content-addressed output
back. “Coordinator” is a role, not a separate tier — there is no single “backend”
box:
```diagram id=farm-topology caption=The elpe CLI dials exactly one node of a fully-meshed peer federation; that node coordinates and places the build. Every machine runs the same backend-agnostic server (Elpe::serve) — there is one node type, and “coordinator” is just the role the dialed node plays. Peers share a BLAKE3 content-addressed store, action cache, and copy-closure over every edge. Builds are portable by default, so any peer can emit any target; Apple targets are cross-built against the Apple SDK fetched once over SSH from the Mac and then resident in the store, so the Linux farm builds them without touching a Mac. Bazel is not a frontend: it is one builder a recipe can call (Bazel.from_targets runs bazel aquery and replays each action as a native derivation), like the Cargo/npm/PyPI builders.
FRONTEND elpe CLI — OCaml recipe (build.ml / shell.ml)
builders a recipe can call: Cargo · npm · PyPI · Bazel · …
│ gRPC · dials any ONE node (that node then coordinates)
▼
────────────────────────────────────────────────────────────────────────────
PEER One node type — every node runs the SAME server (Elpe::serve).
FEDERATION Five peers, fully meshed (a clique): every edge carries Register/
Heartbeat + shared BLAKE3 store · action cache · copy-closure.
┌──────────────┐
│ Linux node │ ← the dialed node
│ x86_64-linux │ (coordinator now)
└──────────────┘
╱ ╲ ╳ ╱ ╲ every peer links
┌──────────────┐ ┌──────────────┐ to every other
│ Linux node │───│ Linux node │
│ x86_64-linux │ ╳ │ aarch64-linux│
└──────────────┘ └──────────────┘
│ ╲ ╳ ╱ │
┌──────────────┐ ┌──────────────┐
│ Linux VM │───│ Mac │ Apple SDK ──┐
│ x86_64-linux │ │ aarch64-darwin│ over SSH │
└──────────────┘ └──────────────┘ → store ────┘
──────────────────────────────────────────────────────────────────────────── Portable by default → any peer emits ANY target (native or cross): TARGETS x86_64-linux aarch64-linux aarch64-darwin aarch64-apple-ios (macOS) (iPhone) └── cross-built vs the SSH-fetched Apple SDK, no Mac in the loop ──┘
The pieces, at a glance:
- [`elpe shell`](get-started/shell.md) — ephemeral dev shells derived from a recipe.
- [`elpe build`](get-started/build.md) — build an artifact, addressed by content in the store.
- A recipe is a `build.ml` / `shell.ml` (or `project.elpe`) — a typed OCaml program.
- Packages come straight from Ubuntu/Homebrew/PyPI/npm/crates.io, consumed unmodified.
- [Whole bootable systems](images-packaging/images.md) are just an `Image.*` build, installed and updated with `elpe-install`/`elpe-update`/`elpe switch`.
- Builds distribute over the [compilation farm](operations/farm.md).
- The lock is an [SBOM with a verifiable SWHID per dependency and a Software Heritage kill-switch](operations/software-heritage.md).
- Bazel is just another builder inside a recipe: `Bazel.from_targets` runs `bazel aquery` and replays each action as a native derivation (`elpe/lib/bazel.ml`) — Elpe owns the build, keeping repro/audit/store-link.
## Guides
1. [**`elpe shell`** — ephemeral dev shells](get-started/shell.md), Linux and macOS.
2. [**`elpe build`** — build a package](get-started/build.md), the simple cases.
3. [**Authoring derivations**](authoring-recipes/derivations.md) for the supported ecosystems —
Rust/Cargo, Python/PyPI, npm/pnpm, Bazel, Debian, opam — and lockfiles.
4. [**Images and the installer**](images-packaging/images.md) — build a `qcow2`, and a bootable
**ElpeOS** live installer that lays a real, self-updating system onto a disk.
5. [**The compilation farm**](operations/farm.md) — coordinator/node federation, macOS
nodes, CPU placement, and an instrumented node with Prometheus + Loki.
6. [**Design**](internals/design.md) — the crates, the OCaml library, the embedded VM, and
the Rust↔OCaml boundary.
7. [**macOS / Apple Silicon**](macos/darwin.md) — the Darwin infrastructure: the test
Mac, the SSH-fetched Apple SDK, the Virtualization.framework isolation VM, and
deployment. Consuming Homebrew is [darwin-homebrew.md](macos/darwin-homebrew.md).
### Supply chain & interop
8. [**Software Heritage** — SWHID-in-SBOM & the kill-switch](operations/software-heritage.md):
the lock *is* a CycloneDX SBOM, every entry carries an intrinsic, verifiable
SWHID (ISO/IEC 18670), and a dead upstream is transparently recovered from the
Software Heritage archive. See also the [deb kill-switch](operations/deb-killswitch.md)
(recover Debian *source* and rebuild the `.deb`).
## Read the docs in a browser (`serve.sh`)
These Markdown guides also render as a small, self-contained website — a retro
"90s web" look with modern-browser niceties (flex nav, dark mode, accessible
focus, a table of contents). One command, all inside an `elpe shell` that
provisions `pandoc` + `python3`, so there is nothing to install:
```console
ELPE=/home/pe/Projets/elpe/target/release/elpe doc/serve.sh # → http://localhost:8080/
PORT=9000 doc/serve.sh picks another port. The recipe is doc/serve.ml,
the renderer/driver is doc/serve.sh, and the stylesheet is
doc/serve.css.
Runnable examples (doctest.py)
Every example in these docs that is meant to work is a real, executable
artifact, à la Rust’s doctests. A small harness — doc/doctest.py
— extracts the tagged code blocks, wraps each into a complete recipe (the
equivalent of rustdoc synthesising a main), and runs it against a real elpe
binary. A doc that rots therefore fails the harness.
# from the repo root, with a built elpe on hand:
ELPE=/home/pe/Projets/elpe/target/release/elpe doc/doctest.py
# or a single guide:
ELPE=…/elpe doc/doctest.py doc/get-started/build.md
Without $ELPE the harness still parses, wraps, and validates every block
(catching malformed recipes and missing preambles) but reports execution as
skip. Pass --require-elpe to make a missing binary a hard failure (CI).
How a block is marked
The harness keys off the fenced-block info string (the text after the opening
```). Two kinds of block run.
Elpe recipes — an ocaml block carrying the elpe directive:
```ocaml elpe
let () =
build
(object
inherit std_derivation
method name = "demo"
method! build_inputs = Lwt.return [ ubuntu "coreutils" ]
method! src = Lwt.return (local_src ".")
end
:> derivation)
```
The harness prepends the standard preamble (open Elpe / open Lwt.Syntax)
unless the block already opens Elpe, so snippets stay focused on the
derivation. If a block contains no build/build_lwt call, the whole block is
treated as a single expression of type derivation and wrapped in
let () = build ( … :> derivation) — that is the “adds a main()” convenience.
The temp file it writes is never named elpe.ml (that would make the
recipe’s own module shadow the Elpe library and break open Elpe).
Directives after elpe (comma- or space-separated):
| directive | effect |
|---|---|
elpe=build (default) | run elpe build on the assembled recipe |
elpe=shell | run elpe shell (use with --export-style snippets) |
elpe=lock | run elpe lock |
no_run | assemble and write the recipe, but do not invoke the backend — for recipes whose build needs a VM/network the doctest host lacks; still proves the recipe compiles-as-assembled |
cwd=<path> | run from this directory (for local_src/native-lockfile recipes that need a project tree), relative to the repo root |
ignore | skip entirely (pure illustration / pseudo-code) |
Shell transcripts — a console (or sh) block carrying test:
```console test cwd=examples/rust-hello
elpe build build.ml
```
Lines run under bash -euo pipefail; a leading $ prompt is stripped;
cwd=<path> sets the working directory. Without test a console block is
illustrative and never executed.