Skip to content

Adding a new package recipe

The practical walkthrough: you want punix install foo to work for some foo that isn't yet in packages/official/. Two paths.

Path A: translate from a Homebrew formula

The fast path when the package already has a brew formula. The translator handles ~80 % of common shapes (cargo, go, npm, autotools, cmake, meson, swift, pip, git-tag sources) — you read its output and ship.

1. Translate

uv run python tools/migrate-brew.py \
  --brew-core sandbox/homebrew-core \
  --out /tmp/foo-out \
  --names-from <(echo foo)

Or for a one-off, the script is importable:

from pathlib import Path
from punix.migrate.brew import translate_formula
formula = Path("sandbox/homebrew-core/Formula/f/foo.rb").read_text()
result = translate_formula(formula, name_override="foo")
print(result.pcl) if not result.refused else print(result.refusal_reason)

Common refusal reasons and what they mean: see Corpus blockers → Translator pattern not handled yet.

2. Inspect the translation

module foo {
  version = "1.2.3"
  recipe = "std.cargo"
  source = {
    type = "url"
    url = "https://example.com/foo-1.2.3.tar.gz"
    hash = "..."
    hashType = "sha256"
  }
  recipeArgs = {
    manifest_path = "crates/foo-cli"
  }
  deps = [rustc-bootstrap.pname, openssl.pname]
  meta = { ... }
}

Walk through:

  • recipe picked by the translator from the install-block shape (std.cargo for system "cargo", "install", std.go for go build, …). Full list at src/punix/realise/recipes_lib/std/.
  • sourcetype = "url" for tarballs, type = "git" with hash = "vX.Y.Z" for git-tag sources.
  • recipeArgs — recipe-class-specific knobs the translator extracted from the formula. The shape is documented at the top of each std.<name>.py.
  • deps — every brew depends_on that maps to a Punix recipe in _DEPS_BREW_TO_PUNIX (a table at the top of src/punix/migrate/brew.py).
  • # TODO[brew-import] comments in meta — anything the translator couldn't auto-translate. Each is a concrete instruction for what to do.

3. Try to build

cp /tmp/foo-out/foo.pcl packages/official/foo.pcl
d=$(mktemp -d -t punix-build-XXXXXX)
cp packages/official/*.pcl packages/seeds/*.pcl "$d/"
uv run punix build "$d" --only foo

A green build means: source fetched, hash verified, deps wired, build succeeded, output landed in ~/.punix/store/<hash>-foo-1.2.3/.

A red build typically falls into one of three buckets:

  • Missing dep: ${DEP:X} unbound or Package requirements were not met. Add the missing recipe to deps. If the brew formula relied on the macOS system version, you'll need to declare it explicitly.
  • Wrong CWD: manifest path not found / go.mod not found. The build extracted to a subdir the recipe class doesn't auto-detect; pass src_dir = "." if the repo is flat, or src_dir = "<subdir>" otherwise.
  • Per-recipe shape issue: configure flag mismatch, custom Makefile target, etc. Read the build log at ~/.punix/punix-build-logs/foo.log and hand-tune the recipe.

4. Smoke-test

STORE=$(ls -d ~/.punix/store/*-foo-* | head -1)
"$STORE/bin/foo" --version

If the binary runs, the recipe is ready to promote.

5. Promote

If you've copied to packages/official/foo.pcl and the build is green, you're already done. Two follow-ups for a clean PR:

  • Add "foo": "foo" to _DEPS_BREW_TO_PUNIX in src/punix/migrate/brew.py so the coverage tool and the translator both know about it.
  • Run uv run python tools/status-report.py to refresh the corpus docs.

Path B: hand-write

The fallback when the brew formula's install shape doesn't translate (shell-only installs, vendored multi-source bundles, xcodebuild, etc.) or when the package isn't in brew at all.

1. Pick a recipe class

Build shape Recipe class
./configure && make && make install std.autotools
cmake -B build && cmake --build build && cmake --install build std.cmake
meson setup && meson compile && meson install std.meson
make && make install (no autoconf) std.make
cargo install std.cargo
go build std.go
npm install std.npm
gem build && gem install std.ruby_gem
pip install (multi-package via pip's resolver) std.python_venv
Pure-python single-package install std.python_wheel
swift build -c release std.swift
Copy files only std.copy_tree
Custom — write the bash yourself std.shell

Each class's accepted recipeArgs is documented at the top of its .py file under src/punix/realise/recipes_lib/std/.

2. Write the .pcl

module foo {
  version = "1.2.3"
  recipe = "std.autotools"
  source = {
    type = "url"
    url = "https://example.com/foo-1.2.3.tar.gz"
    hash = "<sha256 of the tarball>"
    hashType = "sha256"
  }
  recipeArgs = {
    configure_flags = ["--with-bar=${DEP:bar}"]
  }
  deps = [autoconf.pname, automake.pname, bar.pname]
  meta = {
    build_system = "autotools"
    description = "One-sentence project description"
    homepage = "https://example.com/foo"
    license = "MIT"
    origin = "hand-written"
  }
}

Notes:

  • Get the hash by attempting the build. Use a placeholder (64 0s), build, copy the "expected … got …" diff into hash, rebuild.
  • ${DEP:X} references expand to the dep's store path at build time. They only work for deps you've declared in deps.
  • origin = "hand-written" distinguishes from "brew-import" for the upgrade reporter and the status tooling.

3. Build, smoke-test, promote

Same as Path A steps 3–5.

Hashes: how to compute one safely

For a brand-new source URL whose hash you don't know:

curl -sLO https://example.com/foo-1.2.3.tar.gz
shasum -a 256 foo-1.2.3.tar.gz

Or let the fetcher tell you:

source = {
  type = "url"
  url  = "https://example.com/foo-1.2.3.tar.gz"
  hash = "0000000000000000000000000000000000000000000000000000000000000000"
  hashType = "sha256"
}

The build will fail with Source hash mismatch: expected 000…, got <actual>. Copy the got value into the recipe and rebuild. The store is content-addressed, so the wrong-hash attempt left nothing behind.

When the build fails: where to look

Symptom First place to look
Source hash mismatch The actual hash is in the error message — paste it in.
DEP: unbound variable A ${DEP:X} reference whose X isn't in deps. Either add it or remove the reference.
Package requirements were not met (autotools) Missing dep at configure time. Add it; if pkg-config is the resolution path, make sure pkgconf is in deps.
manifest path … not found (cargo) Source extracted flat or to an unexpected dir. Add src_dir = "." or src_dir = "<subdir>".
go.mod not found Same as cargo's manifest issue; src_dir = "." for git-source checkouts.
dyld: Library not loaded: @rpath/… at runtime Sequoia DYLD-stripping. std.cmake already bakes external-dep rpaths via install_name_tool in _postinstall.sh; for std.shell / std.autotools, pass -Wl,-rpath,${DEP:X}/lib in LDFLAGS.
cp: <path>: No such file or directory in post_install The build never produced that path. Check the actual build output: _PUNIX_BUILD_LOG=1 uv run punix build … keeps the build tree.

Build logs from each --only attempt land at ~/.punix/punix-build-logs/<name>.log. Sandbox temp dirs are cleaned up on success; on failure they stick around at /var/folders/.../punix-build-<random>/ (macOS) or /tmp/punix-build-<random>/ (Linux).