Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

mode name
drwxr-xr-x .well-known/
drwxr-xr-x darwinvm/
drwxr-xr-x design/
drwxr-xr-x doc/
drwxr-xr-x elpe/
drwxr-xr-x examples/
drwxr-xr-x grafana/
drwxr-xr-x scripts/
drwxr-xr-x web/
-rw-r--r-- .gitignore
-rw-r--r-- .ignore
-rw-r--r-- LICENSE
-rw-r--r-- NOTICE
-rw-r--r-- README.md
-rw-r--r-- TRADEMARK.md
-rw-r--r-- design.org
-rw-r--r-- pijul.toml
-rw-r--r-- project.elpe
-rw-r--r-- wait_qcow2.sh
README

Elpe — the universal package installer

Elpe borrows from immutable distributions (Nix, Silverblue) and from conventional ones (Debian, Ubuntu) to give you:

  • Ephemeral shells with packages from a real distribution, installed only for the life of the shell.
  • Isolated, reproducible builds — your code compiles in a container with only its declared inputs in scope.
  • Cross-language lockfiles — a single CycloneDX SBOM that doubles as a lockfile, derived from the native lock your ecosystem already produces (uv.lock, Cargo.lock, pnpm-lock.yaml, …).
  • Bootable images — reproducible qcow2 / installer images from the same recipes.
  • macOS artifacts (experimental) — cross-built and signed from Linux, using a real Apple Silicon Mac only as the pinned SDK source and isolation host.

Recipes are ordinary OCaml (fast to compile, sound types, no brain-teasers); the build backend is Rust. The two talk over gRPC. The store is a flat, content-addressed /opt/elpe — the same prefix on every platform.

Status: experimental — not for production. Security is a primary goal (see Security), but the surface is still moving.


Quick start

Everything below is one CLI, elpe, resolving a recipe (project.elpe, build.ml, or shell.ml) in the current directory. See doc/ for the full guides and examples/ for ~45 runnable recipes.

# Ephemeral shell with a distro package (no install, no daemon needed)
$ elpe shell -p cowsay -c "cowsay 'hello from elpe'"

# Build a C program in strict isolation
$ elpe build examples/hello/build.ml

# Build a Rust crate (rustc from Ubuntu, crates pinned by SHA256)
$ elpe build examples/rust-hello/build.ml

# Derive the CycloneDX lock from a committed native lock — offline, no network
$ elpe lock examples/pypi-native-lock/app.elpe     # emits elpe.<target>.lock

# Assemble a signed .deb + a small repo
$ elpe build examples/deb/build.ml

# Build a bootable qcow2 image
$ elpe build examples/vm/build.ml

Verbs: build · lock · update · shell · switch · gc. elpe -p pkg… is shorthand for elpe shell -p pkg…; elpe file.elpe for elpe build file.elpe.


The CycloneDX lock is a real lockfile

Elpe’s lock (elpe.<target>.lock) is a CycloneDX SBOM, and it is derived from the native lockfile your build already consumes — so it cannot drift from it. Every component carries a pinned source hash. At build time the backend fetches each source and verifies it against that pin: a source whose bytes no longer hash to the pin is rejected, never silently substituted.

$ elpe build examples/syft/build.ml
Error: Hash mismatch: expected d2755869…c432, got 9f10…a1b3
  → a pinned hash drifted; re-pin with `elpe update` (build outputs)
    or `elpe lock` (sources)

Because outputs are content-addressed too (elpe update records input-hash → output-hash pins in elpe.<target>.outputs.lock), dependencies form a Merkle tree: you can check both that a recipe hasn’t changed (like Nix) and that an output hasn’t changed (like Ubuntu). This exact guarantee is covered by tests/cli/cyclonedx_lock.bats.


Bootstrap your own Linux

The headline: go from a bare, pristine Ubuntu to a self-hosting Elpe — no Nix, just qemu + curl + content hashes — then cross-build and sign a native macOS Elpe from that same Linux. At the end you have the whole toolchain, on both platforms, reproducibly.

1. Stage 0 — the first elpe, without Nix

The only thing Nix uniquely produced was the first elpe binary needed to run build.ml (a chicken-and-egg). scripts/seed/ pins a stage0 toolchain by content hash so that first binary builds from Ubuntu alone. Trust roots: the Ubuntu archive PGP key (src/ubuntu-archive-keyring.asc, verified in-process by src/pgp.rs), the toolchain .debs (scripts/seed/toolchain.lock, SHA256), and the crates (Cargo.lock, SHA256).

Push-button, in a pristine VM (recommended):

# Any KVM-capable Linux host with qemu + cloud-image-utils + pijul:
scripts/seed/test-stage0-vm.sh

It fetches and verifies a pinned Ubuntu cloud image (vm.lock), boots it headless under QEMU with -snapshot (the base is never mutated), mounts a read-only clone of the repo over 9p, and runs stage0 inside the guest:

  1. curl + verify the pinned toolchain (rustc/cargo/gcc/clang/protoc) from the signed Ubuntu suite;
  2. cargo build the Rust backend elped (crates pinned by SHA256) — the only time cargo is used as the compiler;
  3. if the OCaml deps are present, build the elpe frontend, then run Phase B — elpe build build.ml twice — and assert the rebuilt daemon is bit-for-bit reproducible.

Manually, on a clean Ubuntu box:

scripts/seed/extract-hashes.sh   # derive crates.lock from Cargo.lock (no build)
scripts/seed/bootstrap-seed.sh   # fetch+verify toolchain, build stage0, close the loop

After stage0, Elpe rebuilds itself from its own recipe — cargo is never the compiler again (rust.ml calls rustc directly):

elpe build project.elpe          # the elpe binary, built by elpe

2. Cross-compile for macOS — from that same Linux

Elpe never emulates macOS and never vendors Apple software. A real Apple Silicon Mac is used only as (a) the pinned SDK source — a one-time SSH fetch of the non-redistributable MacOSX.sdk, streamed into the store and hash-verified — and (b) an isolation VM host. Mach-O relocation itself runs on Linux (machoedit). See doc/macos/darwin.md.

# arm64 macOS target, cross-linked from Linux against the pinned SDK
ELPE_TARGET_OS=darwin elpe build examples/darwin-elpe-bundle/build.ml

3. Sign it

Builds are ad-hoc signed by default (reproducible); a Developer-ID signature is a later, distribution-only step. Either the Mac signs (codesign -f -s -) or Linux does, cross, via rcodesign — both proven paths. examples/darwin-elpe-pkg wraps the result as a native .pkg whose postinstall wires /usr/local/bin/elpe:

ELPE_TARGET_OS=darwin elpe build examples/darwin-elpe-pkg/build.ml

4. …and then you have everything

A Linux Elpe, bootstrapped from Ubuntu with nothing but qemu + curl + hashes, that reproducibly builds and signs a native macOS Elpe. The store prefix (/opt/elpe), the output hashes, and the recipes are identical across both — the same Merkle-pinned closure, end to end.


Testing

One orchestrator runs every layer the machine supports and skips (never fails) the ones whose tooling is absent:

scripts/run-tests.sh                 # dune + cargo + bats + doctest, offline
ELPE_ONLINE=1 scripts/run-tests.sh   # also real-build / fetch-integrity tests
LayerCommandNeeds
OCaml recipe librarydune runtest (in elpe/)dune — offline
Rust unit + integrationcargo testcargo — offline (macOS/net tests gated)
CLI smoke batterybats tests/clibats + an elpe binary
Doc examplesdoc/doctest.pypython3 + an elpe binary

The CLI battery (tests/cli/) locates a binary via $ELPE (or common build outputs) and skips cleanly when none is built — bootstrapping is a prerequisite, not a test failure. The offline lock_offline.bats is the cheapest real coverage (recipe compile → sandboxed OCaml VM → emit lock, no daemon); cyclonedx_lock.bats and online.bats are ELPE_ONLINE-gated.


How it fits together

  • Frontend (elpe, eval-vm/cli): a Rust host embedding a sandboxed OCaml bytecode VM. It compiles your build.ml at runtime and evaluates it with no ambient I/O — only curated host callbacks.
  • Backend (elped, src/): a Rust gRPC server that runs each build in a Linux namespace/cgroup container against the /opt/elpe store. macOS builds run via elpe-darwin (native) or a Virtualization.framework guest.
  • Store: flat, content-addressed /opt/elpe/<blake3> — portable, shared across farm peers, identical prefix on every platform.

Project layout

PathWhat
eval-vm/the OCaml-VM frontend CLI (elpe)
src/the Rust backend daemon + build/container/store/macOS logic
elpe/lib/the OCaml recipe library (the DSL: ubuntu, cargo, bazel, Image, Lock, …)
elpe/test/OCaml recipe-library tests (dune runtest)
elfedit/, machoedit/ELF / Mach-O relocation (binary patching)
pkgedit/pure-Rust .pkg writer
elpe-sched/build scheduler + CPU placement
examples/~45 runnable recipes (see examples/)
scripts/seed/the Nix-free stage0 bootstrap (qemu + curl)
tests/cli/the BATS CLI smoke battery
doc/user + internals documentation (doctest.py-checked)

Design notes

  • We don’t bootstrap the whole system. Low-level packages come from existing distributions — mix-and-match versions and even distros. Bootstrapping the world is elegant but expensive to secure and maintain; outsourcing the base to Debian/Ubuntu lets us lean on external auditing while keeping your code in strict, reproducible isolation.
  • OCaml for recipes. Of the languages we prototyped (Haskell, OCaml, Rust, Scala, TypeScript, Luau), OCaml alone gave us fast compilation, a sound and unexciting type system (no brain-teasers), bytecode we can embed and sandbox, and nominal types for strict configuration options.
  • Separation of concerns. The primitive that pulls a distro package is deliberately hard to use for modifying the distro — so a security audit can cleanly separate your code from the distribution’s.

Security

Reproducibility is part of security, not the whole story. Elpe packages form a Merkle tree of dependencies, so both recipes (like Nix) and outputs (like Ubuntu) are verifiable. Builds are code running in an explicit, isolated container. And by outsourcing base-repository security to conventional distributions, audits can rely on external providers without giving up reproducibility.

Contributing

We intend the Elpe community to be inclusive and open, and enforce the Citizen Code of Conduct. Areas that want help: fetchers (tarball/Git imports), language-package-manager interfaces, documentation tooling, cross-language error handling, and a reproducible bootable ISO from Ubuntu packages.

The name

Elpe is the back-transliteration of the Franglais pronunciation of «Help» (Franglais quotes intended) — a tribute to our universal duty of always elping each other out.