A transparent and reproducible package manager
Find a file
riomoo 4856fdcab4
version 0.1.10 (#13)
- added fix for gnu distros that use musl from false musl label for the entire distro

Reviewed-on: #13
2026-11-02 16:49:56 +00:00
assets updated README with video (#10) 2026-11-02 13:00:38 +00:00
docs version 0.1.9 (#11) 2026-11-02 15:17:45 +00:00
examples version 0.1.6 (#5) 2026-11-02 08:15:23 +00:00
recipes version 0.1.6 (#5) 2026-11-02 08:15:23 +00:00
src version 0.1.10 (#13) 2026-11-02 16:49:56 +00:00
.gitignore version 0.1.4 (#1) 2026-11-02 05:31:34 +00:00
build.zig version 0.1.4 (#1) 2026-11-02 05:31:34 +00:00
LICENSE init commit 2026-07-28 13:45:00 -04:00
README.md version 0.1.9 minor README fix (#12) 2026-11-02 15:30:06 +00:00

Axiom

A user package repository that can't run your recipe's shell, because there isn't one.

The AUR's malware problem comes from one design decision: a PKGBUILD is a bash program. Anyone can upload one, and anything bash can do, a PKGBUILD can do: curl | sh, base64 payloads, Unicode tricks that hide code from reviewers, silently swapping the source URL. Reviewing every update by eye doesn't scale.

License

Custom badge

What's this is for

Axiom replaces the program with data and puts everything that must still execute into a box:

Problem in the AUR What Axiom does
Recipes are bash scripts Recipes are a strict declarative format. Commands are argv arrays passed to execve; there is no shell anywhere in Axiom. Shells, interpreters, and network/privilege tools are banned as build steps.
Hidden code (bidi overrides, zero-width chars, homoglyphs, CRLF, escape sequences) Recipes must be printable ASCII. Every rejected byte is reported with line, column and name, e.g. U+202E RIGHT-TO-LEFT OVERRIDE (bidi, Trojan Source).
source= can point at anything, branches move https:// only, no IPs, no punycode, no credentials. Source is pinned to a full commit hash; an optional tag must still resolve to that commit or the build stops.
-bin packages download unverified blobs Prebuilt binaries are used only with a SHA-256 in the recipe, only if they match your arch + libc, and a mismatch is a hard failure, never a fallback.
Builds run as you, with your $HOME, SSH agent and network Builds run in fresh user/mount/PID/IPC/UTS/network namespaces: no network, no $HOME, host read-only. Only the build dir and the output dir are writable.
"Did this binary come from that source?" Builds are reproducible (fixed paths, SOURCE_DATE_EPOCH, normalized output). axiom verify rebuilds from scratch and compares bit-for-bit.
Dependencies pile up in /usr Dependencies live in Axiom's store, linked via rpath. Build-only deps are deleted after the build; removing a package removes what it pulled in.

Axiom is one static binary (~5 MB, no libc) written in Zig. It runs on Arch, Debian/Ubuntu, Alpine, Fedora and anything else with a Linux kernel, git, and curl or wget. It never touches the host package manager.

Example:

How it functions

Quick start

zig build --release            # Zig 0.17 or 0.16 (ReleaseSafe)
./zig-out/bin/axiom doctor     # proves the sandbox is actually isolated on this machine

export AXIOM_RECIPES=$PWD/recipes
axiom install libpng           # builds ninja (build dep) + zlib (runtime dep) + libpng
axiom env                      # prints the PATH line to add to your shell
axiom verify libpng            # rebuild from scratch, compare bit-for-bit
axiom remove libpng            # zlib goes too; nothing else depended on it

Commands

lint <file.axiom>...      check recipes (charset, schema, pins, argv policy)
info <name|file>          show a recipe and what Axiom would do on this machine
build <name|file>...      build into the store without installing
install <name|file>...    build/fetch, then add to your profile (name, repo/name or path)
remove <name>...          remove from profile and delete now-unused deps
list [--all]              installed packages (--all: every store entry)
verify <name>             check store integrity and rebuild to prove reproducibility
sync [--dry-run]          make the system match packages.axiom (declarative mode)
update                    pull recipe repos (fast-forward only, optional signature check)
gc [--dry-run]            delete store entries unreachable from installed packages
hash <file|dir>           sha256 of a file, or Axiom tree hash of a directory
new <git-url> [--tag T]   write a starter recipe: pins the commit, detects build system and license
bump <name|file> [--tag T]  move a recipe to a new upstream tag (lists tags without --tag)
env                       print paths and the PATH line for your shell
doctor                    check tools and prove the build sandbox is isolated

Global options: --binary, --root <dir>, --sandbox auto|native|bwrap, --unsafe-no-sandbox, --keep-work, --rebuild, -j <n>.

A recipe

name = "libpng"
version = "1.6.44"
summary = "Official PNG reference library (installs pngfix)"
license = "libpng-2.0"
homepage = "https://github.com/pnggroup/libpng"

[source]
git = "https://github.com/pnggroup/libpng.git"
commit = "f5e92d76973a7a53f517579bc95d61483bf108c0"
tag = "v1.6.44"

[build]
system = "cmake"
deps = ["ninja"]                 # build-only: collected after the build
args = ["-G", "Ninja", "-DPNG_TESTS=OFF", "-DPNG_STATIC=OFF"]

[runtime]
deps = ["zlib"]                  # linked via rpath into the store

Build systems: cargo meson cmake autotools make go zig none, or explicit [[step]] argv lists. The full format, every rule and the reasoning behind them are in docs/RECIPE.md.

Packaging Zig programs

axiom new https://codeberg.org/someone/tool.git --tag v1.2.0   # writes tool/tool.axiom
$EDITOR tool/tool.axiom                                        # fill in summary, read it
axiom verify ./tool/tool.axiom                                 # build + rebuild, compare

axiom new resolves the tag to its commit (annotated tags are peeled), checks the source out once to detect the build system and license, and writes a recipe with deps = ["zig"]. The zig recipe is the official toolchain tarball, pinned by sha256; it's a build-only dependency, so it's removed again after the build. The zig template runs:

zig build --fetch=all                                  # network, lockfile phase only
zig build install --release=safe -Dcpu=baseline -j{jobs} --prefix {out}   # offline

--release=safe works whether or not build.zig sets a preferred optimize mode. -Dcpu=baseline keeps the binary portable across CPUs and reproducible across build machines. If build.zig doesn't call standardTargetOptions, -Dcpu would be rejected, so axiom new writes an equivalent [[step]] without it. For a new upstream release, run axiom bump tool --tag v1.3.0.

Source vs. binary

  1. If a recipe has a git source, Axiom always builds from source at the pinned commit.
  2. A [binary.<arch>-linux-<gnu|musl|static>] entry is used only when you ask (--binary, or prefer_binary = true) or there is no source, and the triple fits this machine (Axiom detects glibc vs. musl), and the SHA-256 matches.
  3. Untrusted archives are unpacked inside the sandbox.

Declarative mode

If packages.axiom exists in the config dir (~/.config/axiom/ per user, /etc/axiom/ as root), it becomes the single source of truth:

packages = [
  "lua",
  "libpng",
]
prefer_binary = false

axiom sync builds everything first and only then commits the new set, so a failed build leaves your system exactly as it was. Anything not listed is removed along with its dependencies. While the file exists, axiom install / remove refuse and tell you to edit the file instead. axiom sync --dry-run shows the plan.

Where things live

~/.local/share/axiom/            (root: /var/lib/axiom, or $AXIOM_ROOT)
  store/<hash>-<name>-<ver>/     immutable package trees (0444/0555, fixed mtimes)
  db/pkgs/<store-name>           output hash, commit, deps, build deps
  db/roots                       what you explicitly installed
  profile -> profiles/<n>/       bin/ and share/ symlinks; this goes on $PATH
~/.config/axiom/                 (root: /etc/axiom)
  axiom.conf                     options and recipe repositories
  packages.axiom                 optional: switches on declarative mode

Store paths are input-addressed: the hash covers the exact recipe bytes, patches, build mode and the store paths of every dependency. Change anything and you get a new path. Nothing is ever modified in place.

axiom.conf:

prefer_binary = false
jobs = 0                       # 0 = all CPUs
sandbox = "auto"               # auto | native | bwrap  (config can't disable it)
recipes = ["/srv/my-recipes"]

[repo.main]
git = "https://example.org/axiom-recipes.git"
branch = "main"
allowed_signers = "main.allowed_signers"  # SSH keys trusted to sign the repo, and/or
gpg_keys = "main.asc"                      # GPG public keys trusted to sign it

Package names and repositories

A package name means "the recipe with that name", so with several sources it matters which one you get:

  • axiom install matrix/gopherbook takes it from the matrix repository only.
  • axiom install gopherbook works when exactly one source has it. If several do, Axiom never guesses:
    • if the package is already installed from one of them, it keeps using that one;
    • otherwise, at a terminal, it lists each source with its version and summary and asks which one you want (Enter cancels);
    • with no terminal to ask (scripts, CI, pipes), it stops and tells you to name one, e.g. matrix/gopherbook.
  • An installed package remembers where it came from (axiom list shows it). If a plain name later resolves somewhere else, for example because the original repository dropped it and another one has a recipe with the same name, install and sync refuse before fetching anything. Naming the new source (other/gopherbook) is how you opt in.
  • A recipe's dependencies come from its own repository first; deps may also name one exactly, as repo/name.
  • Only repositories listed in axiom.conf count. Removing one from the config stops it supplying recipes, even though its checkout stays on disk.

axiom update only fast-forwards: a recipe repo whose history was rewritten is treated as compromised and refused.

Setting allowed_signers (a standard git allowed-signers file: email ssh-ed25519 AAAA...) or gpg_keys (an exported OpenPGP public key file) makes the newest commit's signature mandatory, and only those keys count. Key files are resolved relative to the config dir. GPG keys are checked in a private keyring Axiom creates for each update, never yours, so keys you happen to have imported (or a keyserver auto-fetch) can't vouch for a repo. require_signed = true on its own still works but trusts every key in your GnuPG keyring, and Axiom warns about it. Unknown or mistyped keys in a [repo.*] table are errors, so a typo can't silently switch checking off.

The sandbox

/usr /etc /bin /lib* host toolchain, read-only
/dev null zero full random urandom tty only
/proc private PID namespace
/build the work dir (read-write)
store read-only, except this package's output dir
$HOME, /var, /run, /root not present
network none (except the lockfile fetch phase; see below)

Backends: native namespaces (no setuid helper), falling back to bubblewrap where unprivileged user namespaces are restricted (e.g. Ubuntu's AppArmor policy). If neither works, Axiom refuses to build unless you pass --unsafe-no-sandbox.

Cargo, Go and Zig fetch dependencies from their lockfiles in a separate phase that has network but cannot see the output directory; the build itself then runs offline (--offline, GOPROXY=off).

What Axiom does not solve

Being honest about the boundary matters more than marketing:

  • A malicious upstream project is still malicious. Axiom guarantees you build exactly the commit the recipe names, that the recipe can't add code of its own, and that the build can't phone home or read your files. It can't make the program you then run trustworthy. Pinning means a hijacked upstream can't silently change what you get, and reviewers have a fixed commit to look at.
  • Upstream build systems still run code (configure scripts, build.rs). That code is sandboxed, not trusted.
  • The host toolchain is visible (read-only /usr). Reproducibility across machines therefore depends on matching compiler versions; toolchains can be packaged as Axiom recipes to pin them too. See recipes/go, a build-only Go toolchain from the official release + SHA-256 that is deleted after use.

Tested

On Ubuntu 24.04 x86_64, Zig 0.16.0 and 0.17.0: zlib, ninja, libpng and lua built from pinned commits as root and as an unprivileged user; zlib, lua and libpng verified bit-for-bit reproducible. Lua produced the identical output hash for two different users with different store paths. The go build system was tested on goawk (also bit-for-bit reproducible), and binary installs plus [install] links on a SHA-256-pinned release archive. Wrong hashes, moved tags, tampered store files and the four hostile recipes in examples/malicious/ are all rejected.

Not yet exercised: the bubblewrap fallback (bwrap wasn't installable in the test environment); the go and gopherbook recipes themselves (go.dev and git.jester-designs.com weren't reachable from the test environment); and non-x86_64 hosts (the code handles aarch64/riscv64 syscall differences, but hasn't run there).

Layout

src/sys.zig       raw Linux syscalls (version-independent; no std.fs / std.Io)
src/proc.zig      fork/execve, never a shell
src/sandbox.zig   namespaces, pivot_root, read-only binds; bwrap fallback
src/recipe.zig    byte lint, strict TOML-subset parser, schema + policy
src/builder.zig   planning, git pinning, downloads, build phases, verify
src/store.zig     store, records, GC, profile generations, tree hashing
src/config.zig    axiom.conf, packages.axiom, recipe lookup
src/main.zig      CLI
recipes/          gopherbook, go (toolchain), zlib, ninja, libpng, lua
examples/malicious/  recipes Axiom must reject
docs/RECIPE.md    format specification