Skip to content

punix install

Build one or more modules (cache-hit if already built) and put their binaries on your $PATH via the per-user profile at ~/.punix/current/bin/.

punix install MODULES... [--file FILE] [options]

What it does

  1. Loads --file FILE (a .pcl file or a directory of them) — or discovers it from $PUNIX_PACKAGES / ./packages/ when omitted.
  2. Type-checks the program (located [E#] → exit 1).
  3. Resolves the union of the build closures of every MODULE (each module + its transitive deps; shared deps appear once).
  4. Builds the closure via the same content-addressed store as punix build — cache-hit when nothing changed.
  5. Atomically symlinks every <store>/bin/<exe> into <profile>/bin/<exe>, per successfully-built module.

Exit codes: 0 = all installed; 2 = an unknown module was requested (none installed — atomic at the request level); 3 = at least one build failed (the modules that built successfully are still linked).

One-time setup

Add this line to your shell's rc file (~/.zshrc, ~/.bashrc, …):

export PATH="$HOME/.punix/current/bin:$PATH"

Re-source the rc or open a new shell, then:

$ cd ~/projects/my-pcl-tree    # has a packages/ subdir

$ punix install just
 installing 1 module: just (+1 dep)
   just (1 binary)
installed 1 module in ~/.punix/current/bin

$ just --version
just 1.51.0

Multi-module install — shared deps build once, one line per module:

$ punix install git uv ghq ruff wget
 installing 5 modules: git, uv, ghq, ruff, wget (+14 deps)
   git (1 binary)
   uv (1 binary)
   ghq (1 binary)
   ruff (1 binary)
   wget (1 binary)
installed 5 modules in ~/.punix/current/bin

When a module fails mid-batch, the successful ones are still linked and the exit code is 3:

$ punix install git some-broken-recipe ruff
 installing 3 modules: git, some-broken-recipe, ruff (+8 deps)
FAIL  some-broken-recipe  (log: ~/.punix/punix-build-logs/some-broken-recipe.log)
   git (1 binary)
   ruff (1 binary)
installed 2 modules in ~/.punix/current/bin
error: 1 of 3 failed: some-broken-recipe; 2 succeeded and are linked
$ echo $?
3

Verbosity

punix install just              # default: one tick per module, one summary
punix install just -v           # per-build progress + every linked binary + store paths
punix install just -q           # silent on success — failures still print

-v and -q are mutually exclusive (combining them exits 2).

Options

  • --file FILE — explicit PCL file or directory; overrides the default discovery.
  • --store-root PATH — store location (default ~/.punix/store).
  • --profile-root PATH — profile root (default ~/.punix/current). Put <profile-root>/bin on $PATH.
  • --scenario NAME — evaluate under scenario NAME (see Modules).
  • --bootstrap MODE — bootstrap trust policy: fast / seeded / source / bootstrappable. Default: $PUNIX_BOOTSTRAP_MODE or seeded. See the Bootstrap modes section below.
  • -v / --verbose — show per-build progress + every linked binary + store paths.
  • -q / --quiet — silent on success; failures still print + exit code still reflects them.

Bootstrap modes

Before any build runs, Punix checks every recipe in the closure against the active bootstrap mode (punix help bootstrap has the full table):

Mode What it allows Default?
fast Homebrew bridges + any prebuilt binary opt-in
seeded system clang + signed prebuilt toolchains (rust, go, llvm, node) yes
source system clang only; everything built from source opt-in
bootstrappable minimal hex seed → full toolchain from source opt-in (future)

Set per-call with --bootstrap=MODE; per-session with $PUNIX_BOOTSTRAP_MODE. A policy violation aborts before any build:

$ punix install some-recipe-that-bridges-brew
error: bootstrap-policy violations in 'seeded' mode (1 recipe(s) cannot be used):
  [brew-bridge] some-recipe: recipe references Homebrew package 'openssl@3' via
                              `$(brew --prefix openssl@3)`. In 'seeded' mode,
                              Punix does not bridge to Homebrew — rewrite the
                              recipe to use a Punix-built dep, or switch to
                              `--bootstrap=fast`.

Every violation in the closure is reported in one pass (fix-all, not fix-retry-fix).

Atomicity

Each symlink is replaced atomically: a side-named symlink is created first, then Path.replaced onto the target name. The visible state is always either the old link or the new one, never partial.

Generations and rollback

Every punix install writes a new generation under <profile-root>'s parent>/profiles/gen-NNN/ and atomically flips <profile-root> to point at it. The prior generations stay on disk for instant rollback:

$ punix install just                # writes gen-001
$ punix install atuin               # writes gen-002 (carries forward just)
$ punix profile list
   gen-001  2026-05-27T…  just
 * gen-002  2026-05-27T…  atuin, just       active
$ punix profile switch 1              # atomic flip back; just only
$ punix profile switch 2              # forward again

The atomic flip is a single os.replace on the active-profile symlink — the same D9 primitive used by punix service rollback. Crash before it returns: the prior generation stays active. After: the new one is active. No partial state.

See punix uninstall and punix profile for the management commands.

What this doesn't do

  • No upgrade. punix install Foo builds whatever version is in your PCL — bump the version in the recipe and re-install. (Phase 5 will add punix upgrade.)
  • No first-install latency mitigation. First-time builds are from-source; expect 30s for a small Rust app, minutes for V8/heavy autotools. Phase 6 (Homebrew bottles as substituter) will turn these into seconds.

Diagnosing failures

Symptom Likely cause
error: 'X': not a package module in FILE The module name is wrong, or the module has no recipe field. List candidates with grep '^module' FILE.
FAIL\t<module>\t<reason> The build failed. The full log lives at ~/.punix/punix-build-logs/<module>.log. Exit code is 3; any other module that built successfully in the same invocation IS linked.
warning: 'X' produced no binaries in <path>/bin The recipe builds successfully but doesn't create a bin/ directory (e.g. it's a library). The store path exists; nothing was symlinked.

See also