- Zig 99.2%
- C 0.8%
|
|
||
|---|---|---|
| docs | ||
| examples | ||
| include | ||
| src | ||
| tests | ||
| .editorconfig | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| README.md | ||
| SECURITY.md | ||
Ember
Ember builds Linux root filesystems from explicit inputs.
It is a dependency-light rootfs engine written in Zig 0.16.0 and shipped as a statically linked CLI, a static PIC library, and a stable C ABI. Its primary working pipeline starts with caller-supplied ALT APT-RPM metadata, raw-bootstraps only the target package tools, then installs the final closure through target rpmi with scriptlets and triggers enabled.
Project status: early development (
0.2.0). The ALT bootstrap and normal target RPM transaction work end to end, but Ember is not yet a general-purpose distribution image builder. See Current limitations.
Why Ember?
Traditional image builders often rely on a host package manager, shell pipelines, container runtimes, or a prebuilt rootfs. Ember instead makes the artifact and trust boundaries explicit:
caller-supplied metadata and package requests
-> appended OpenPGP Release signature + identity verification
-> pkglist size and BLAKE2b verification
-> dependency resolution
-> selected individual RPM downloads
-> size and BLAKE2b verification
-> embedded RSA/OpenPGP header and payload signature verification
-> native RPM + LZMA + CPIO extraction
-> empty staging rootfs
-> target RPM database bootstrap
-> normal target rpmi transaction with scriptlets and triggers
-> ownership/mode normalization and rpm -qa/-Va validation
-> atomic publication
Ember does not download repository metadata, signing keys, profiles, scripts, OCI layers, or ready-made root filesystems. The only network objects fetched by the ALT bootstrap are individual RPMs already selected from metadata supplied by the caller.
Working features
- ALT
pkglist.classic.xzparsing, including concatenated XZ streams; - ALT appended detached Release signature verification against a caller-pinned fingerprint;
- signed Release identity plus pkglist size/BLAKE2b verification before solving;
- package, capability, absolute-path, and ordinary RPM EVR dependency resolution;
- exact-size and BLAKE2b verification of downloaded RPMs;
- embedded
RPMSIGTAG_RSA/RPMSIGTAG_GPGverification with exact signer provenance before extraction; - native RPM header parsing and bounded LZMA payload decompression;
- safe CPIO
newcextraction with traversal and symlink-parent protection; - usr-merge aliases, hardlinks, modes, numeric ownership, and mtimes;
- atomic ALT initial-root construction from an empty staging directory;
- target
rpm --initdband dependency-ordered--justdbregistration; - normal target
rpmiinstallation of the non-bootstrap closure with scriptlets and post-transaction file triggers enabled; - ordered transaction diagnostics, target package-set lock comparison, mode/ownership normalization, and strict
rpm -Vaallowlisting; - canonical
multi-alt.repository-lockv1 output with repository snapshot, policy, NEVRA, origin, size, SHA-256 and ALT BLAKE2b-512 for every selected RPM; - fail-closed
build --lockedverification with field-specific mutable-snapshot and package drift diagnostics; - normalized rootfs comparison over sorted paths, types, modes, hardlink counts, sizes, file bytes, and symlink targets, with an explicit derived-cache/RPM-index policy;
- a shared verified-RPM provider API with direct network, network-backed CAS, and strictly offline CAS implementations;
- a BLAKE2b-512 content-addressed RPM cache with atomic object publication, per-object advisory locking, verified reads, and explicit digest-scoped eviction;
- low-level JSON tree assembly;
- typed package-origin
alt:p11/base,alt:p11/crucible-build, andalt:p11/live-systemrootfs profiles; the live profile composes a TTY installer, network/storage tools, an installer target/unit tree, and ordered systemd configuration steps without an embedded target offline pool; - reusable native namespace runner with explicit mount graphs, bounded output, progress/cancellation callbacks, process-group cancellation, and namespace-scoped cleanup;
- static CLI and PIC static library with C ABI version 3, including native and embedding-owned external runner boundaries plus a locked built-in profile request.
A real ALT P11 x86_64 run currently raw-bootstraps a 39-package closure for setup, filesystem, rpm, and fakeroot, then installs diffutils and tzdata normally for a 41-package verified result.
Requirements
- Zig 0.16.0 to build Ember;
- Linux x86_64 for the currently tested ALT bootstrap;
- root privileges for
ember bootstrap alt-rpm, because the bootstrap preserves numeric ownership and invokes target RPM throughchroot; - caller-provided ALT Release/pkglist snapshot, pinned Release fingerprint, and separately identified trusted package-key keyring;
gpgvfor detached OpenPGP verification (or an embedding-providedrelease.Verifiercallback);- HTTPS access to the configured RPM repository for the direct/cached network providers; the offline provider performs no HTTP and requires every selected RPM in its caller-opened CAS pool.
No external Zig packages are used. Runtime extraction does not call host rpm, rpm2cpio, cpio, xz, tar, Docker, Podman, or a host package manager. The default trust adapter invokes gpgv with only the caller-supplied keyring; the typed Zig API permits a different verifier implementation.
Build
zig build test
zig build -Doptimize=ReleaseSmall
Artifacts:
zig-out/bin/ember static CLI
zig-out/lib/libember.a static PIC library
zig-out/include/ember.h public C header
Verify the CLI:
./zig-out/bin/ember --version
./zig-out/bin/ember --help
Quick start: assemble a tiny rootfs tree
This unprivileged example demonstrates deterministic tree assembly and source paths relative to the manifest:
zig build -Doptimize=ReleaseSmall
rm -rf /tmp/ember-example
./zig-out/bin/ember assemble examples/assemble/rootfs.json /tmp/ember-example
find /tmp/ember-example -maxdepth 3 -printf '%y %p -> %l\n'
/tmp/ember-example/bin/hello
Expected final command output:
hello from an Ember-assembled rootfs
The result is a filesystem tree, not a bootable distribution. See examples/assemble/README.md.
Build an ALT P11 transaction rootfs
- Put caller-supplied x86_64/noarch
releaseandpkglist.classic.xzfiles plus the P11 public-key keyring inexamples/alt-p11/metadata/. - Review the repository URLs and requested packages in
examples/alt-p11/plan.json. - Run:
sudo ./zig-out/bin/ember bootstrap alt-rpm \
examples/alt-p11/plan.json \
/var/tmp/ember-alt-p11-rootfs
sudo chroot /var/tmp/ember-alt-p11-rootfs /bin/rpm -qa | sort
sudo chroot /var/tmp/ember-alt-p11-rootfs /bin/bash -c \
'printf "rpm=%s bash=%s\n" "$(rpm --version)" "$BASH_VERSION"'
The destination must not exist. Ember builds under <destination>.work, removes temporary RPMs, and publishes the rootfs by rename only after bootstrap succeeds. Failure removes staging and leaves an existing destination untouched.
The example builds a bootstrap plus a small normal target RPM transaction, not yet a complete bootable ALT container. Full instructions and expected checks are in examples/alt-p11/README.md.
CLI reference
ember verify alt-release <release> <index> <keyring> <fingerprint> <architecture> <component>
ember verify rpm <file.rpm> <keyring> <fingerprint> <trust-store-identity>
ember inspect apt-rpm-pkglist <pkglist.xz> [package]
ember inspect rpm <file.rpm>
ember compare rootfs <first> <second>
ember solve apt-rpm <pkglist.xz>... -- <package>...
ember fetch rpm <base-url> <pkglist.xz> <package> --output <file.rpm>
ember extract rpm <file.rpm> <destination> <keyring> <fingerprint> <trust-store-identity>
ember bootstrap alt-rpm <plan.json> <destination>
ember bootstrap alt-rpm <plan.json> <destination> --lock <repository-lock.json>
ember build --locked <plan.json> <repository-lock.json> <destination>
ember profile <alt:p11/base|alt:p11/crucible-build|alt:p11/live-system> <plan.json> <destination> --lock <repository-lock.json>
ember profile <alt:p11/base|alt:p11/crucible-build|alt:p11/live-system> --locked <plan.json> <repository-lock.json> <destination>
ember assemble <manifest.json> <destination>
Run ember --help for the authoritative command summary.
compare rootfs omits mtimes, derived APT/ldconfig cache files, and raw Berkeley DB RPM index files. The latter are transaction-layout state rather than a canonical package inventory; acceptance must additionally compare sorted rpm -qa, verify each RPM database, and compare the repository/profile locks. Numeric ownership is verified separately when accepting a system rootfs because Zig's portable file-stat surface intentionally does not expose uid/gid.
C API
ABI version 3 exposes the low-level manifest assembler, ember_run_step, and ember_build_locked_profile. The locked-profile request contains only profile, caller-supplied plan path, canonical repository-lock path, and destination; unknown profiles, null fields, drift, and existing destinations fail closed. ember_run_step accepts a command, working directory, network-isolation policy, declarative mounts, cancellation/progress callbacks, and either the native backend or an embedding-owned external runner callback.
#include <stdio.h>
#include <ember.h>
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "usage: %s <manifest.json> <destination>\n", argv[0]);
return 2;
}
ember_status status = ember_build(argv[1], argv[2]);
if (status != EMBER_OK) {
fprintf(stderr, "ember_build: %s\n", ember_status_string(status));
return 1;
}
return 0;
}
Compile with a normal C compiler:
cc -I zig-out/include examples/c-api/assemble.c \
zig-out/lib/libember.a -pthread -o /tmp/ember-c-example
cc -I zig-out/include examples/c-api/run-step.c \
zig-out/lib/libember.a -pthread -o /tmp/ember-c-run-step
rm -rf /tmp/ember-c-rootfs
/tmp/ember-c-example examples/assemble/rootfs.json /tmp/ember-c-rootfs
/tmp/ember-c-run-step
/tmp/ember-c-run-step --mount
/tmp/ember-c-run-step --cancel
/tmp/ember-c-run-step --external
The ALT build API will later expose opaque plans and package providers. CLI and C interfaces already use the same Zig runner core; the C external callback lets an embedding process own a cross-architecture or stronger sandbox.
Safety model
- Existing destinations are never overwritten.
- Every build uses a private staging directory and atomic publication.
- Archive paths and manifest paths reject traversal and unsafe components.
- CPIO extraction rejects writes through symlink parents created by an archive.
- Downloads use identity encoding and are accepted only after metadata-derived size and BLAKE2b checks.
- Cache objects use
objects/blake2b-512/<first-two-hex>/<full-hex>; writes and materialization are atomic, reads are reverified, and per-object locks serialize insertion against eviction. - The offline provider rejects remote sources before creating a staging output and resolves cache sources only by their trusted size and BLAKE2b-512 identity.
- ALT catalogs enter the solver only after their exact Release prefix is authenticated and their size/BLAKE2b record matches.
- Locked builds compare authenticated Release/pkglist identities before RPM download and compare the complete canonical lock before publication.
- Target package tools are executed from the constructed rootfs, not from the host.
- Runtime mounts are modeled separately and must never leak into published artifacts.
Current limitations
- ALT
set:symbol-set requirements use decodedrpmsetcmpsubset semantics; malformed encodings fail closed with deterministic diagnostics. - The bootstrap plan accepts signed ALT Release-chain repositories plus a caller-approved Forgejo manifest whose exact RPMs may be realized from local paths for unpublished builds. Local transport is digest-verified, explicitly records unsigned-own provenance, and never implies that the RPM was published; remote Forgejo acquisition is not wired yet.
- Bootstrap packages are registered with
--justdb; every non-bootstrap package is installed by a normal targetrpmitransaction. %ghostpaths can make RPM normalization print missing-file warnings; unexplained nonzero ownership normalization is fatal, and publication still requires an empty or explicitly allowlistedrpm -Varesult.- Symlink ownership remains root-owned due to a Zig 0.16
Dir.setFileOwnererror-set defect. - Automatic cache-size policy and broader build provenance are not complete. The native runner currently targets privileged Linux/native-architecture execution; user namespaces, seccomp, cgroups, and cross-architecture emulation remain caller-owned external-runner concerns. Cache eviction is intentionally an explicit digest-scoped API boundary; no implicit LRU/TTL policy exists yet.
alt:p11/live-systemrequires the package-originmulti-alt-installerRPM. For local-only validation it can consume the digest-bound RPM through an approved Forgejo manifest; production builds still require a published, trusted Forgejo source. Ember does not substitute an unowned binary or prebuilt rootfs.- LXC, OCI, Crucible, and LiveCD emitters over the package-origin rootfs core are not implemented. Ember has no prebuilt-rootfs import path; those formats are output-only boundaries.
Do not use the current output as a production or security-hardened base image without independent validation.
Roadmap
The intended order is:
- Trust and solver correctness — RPM signatures, conflicts/obsoletes, deterministic providers, and lock/provenance output.
- Built-in ALT profiles — complete
alt:p11/base, then Crucible build-root and systemd-networkd LXC profiles. - Artifact emitters — deterministic LXC and OCI output from the same validated rootfs core.
- Higher-level systems — complete live-system roots and LiveCD stages.
- Public APIs — extend the Zig and C APIs with opaque build plans, input providers, runner callbacks, diagnostics, and progress/cancellation.
Near-term development is focused on stages 1–2. The project will not label package-origin LXC/OCI output complete until a disposable artifact has been built and boot-tested.
Documentation
docs/architecture.md— artifact boundaries, inputs, runners, mount graph, and emitters;docs/alt-p11-from-rpms.md— ALT bootstrap research and implementation path;CHANGELOG.md— shipped capabilities and planned work;CONTRIBUTING.md— development and verification requirements;SECURITY.md— security scope and reporting.
License
MIT © 2026 WetGrape.