Contributing¶
Practical setup for working on Punix itself — environment, layout, daily commands, debugging.
Prerequisites¶
- Python 3.13 or 3.14. CI matrix tests both; earlier versions are unsupported.
- uv ≥ 0.5.
curl -LsSf https://astral.sh/uv/install.sh | shorbrew install uv. git,make,clang/gcc. Standard on macOS, install on Linux via the distro package manager.- For e2e SSH tests:
ssh+rsync(any modern OpenSSH). - For corpus work: clone of homebrew-core under
sandbox/(only needed if you'll be running the brew translator).
Get the code¶
uv sync resolves the dependency graph (including the pinned MIXINv2 git source — Atry/MIXINv2, pinned by rev in pyproject.toml; there is no in-repo vendor/ copy) and creates .venv/. After that, every uv run ... command works.
Optional: activate the venv so you can drop the uv run prefix.
First-pass verification¶
make test # whole test suite — should be 583+ passed in ~10 s
make check # lint + format + type-check
If make test is green, you have a working dev env.
Repo layout¶
.
├── src/punix/ # the implementation
│ ├── frontend/ # parser, type checker, lowerer (above the seam)
│ ├── ir/ # IR + canonical-hash + provenance (the seam)
│ ├── realise/ # sandboxed builds, recipe classes, fetcher (below)
│ │ └── recipes_lib/
│ │ └── std/ # std.cargo, std.go, std.cmake, std.autotools, ...
│ ├── deploy/ # transport, manifest, atomic switch, generators
│ ├── migrate/ # RCL→PCL and Homebrew→PCL translators
│ └── cli.py # cyclopts entry points
├── packages/
│ ├── official/ # 365 working recipes
│ ├── seeds/ # bootstrap recipes (rustc, go, swift)
│ └── experimental/ # translator scratch + needs-rework
├── tests/
│ ├── a_unit/ # fast pure-logic
│ ├── b_integration/ # real I/O, no subprocess
│ └── c_e2e/ # full CLI workflows + conformance
├── tools/ # status-report, migrate-brew, coverage scripts
├── notes/ # design docs (read these before changing core code)
├── docs/ # the doc site you're reading
└── zensical.toml # docs site config
The notes/ directory holds the normative spec (01-theory.md → 06-review-and-v2-alignment.md). Before changing anything in src/punix/frontend/ or src/punix/ir/, read at least 01-theory.md (the inheritance calculus + the feature-decision procedure) and 04-design.md §12 (target layout). Most architectural mistakes start with "I'll just add this small thing" in the wrong layer.
The eval/realise seam¶
Punix's whole design is organised around one boundary:
- Above the seam: pure config. Typed, decidable, terminating, no effects.
- At the seam: IR + canonical-derivation hash. Provenance flows through.
- Below the seam: every host-bound effect (build, transport, service lifecycle).
A PR that puts removal, IFD, build-cycle dependencies, secrets-as-values, or generations into the pure layer is rejected by theorem (see feature-procedure). When in doubt, classify the feature before writing the code.
Daily commands¶
# Run a subset of tests
uv run pytest -m unit # by marker
uv run pytest tests/a_unit/frontend/ # one package (a_unit mirrors src/punix)
uv run pytest tests/a_unit/frontend/test_parser.py::test_one -v # one test, verbose
# Type-check + lint
make check # ruff + ty + pyrefly + mypy
make format # auto-fix what's fixable
# Build the docs locally
make docs # static build
make docs-serve # live-reload on http://localhost:8000
# Build the package corpus (or one recipe)
make dogfood # everything in packages/official/ + seeds/
uv run punix build /tmp/sandbox --only foo,bar # specific recipes
The CLI from inside the repo¶
uv run punix --help
uv run punix check stack.pcl
uv run punix build /path/to/packages-tree
uv run punix service deploy MyStack --file stack.pcl
Useful flags during development:
--verboseon subcommands surfaces internal step traces.PUNIX_PACKAGES=/path/to/treeenv var is the canonical override for the packages-tree discovery; less typing than--filefor repeated calls.PUNIX_BOOTSTRAP_MODE=sourcerejects any prebuilt-binary shortcut (theseeds/std.binaryrecipes). Use this when you want to validate that everything builds from source.
Debugging a failing build¶
# 1. Find the build log
ls ~/.punix/punix-build-logs/<package>.log
# 2. Inspect the build script the recipe class generated
# (each build writes to a temp dir under /var/folders/ or /tmp/punix-build-XXX/;
# normally cleaned up on success, sticks around on failure)
ls /var/folders/*/punix-build-* 2>/dev/null # macOS
ls /tmp/punix-build-* 2>/dev/null # Linux
# 3. Re-run the script manually inside the build dir
cd /var/folders/.../punix-build-XXX/source
bash -x /tmp/tmpXXXXXX.sh # the script path is in the log
The build sandbox is just a temp dir + an output dir + the recipe-class-generated shell script. Once you cd into the build dir, you can iterate on the script, the configure flags, the env — same shell, same tools.
Code conventions¶
strat dataclass/JSON boundaries;Pathinternally. Frontend / config-facing surfaces take strings (PCL writes strings); internal logic usespathlib.Path.- Errors:
- Above the seam (frontend /
check): fail-fast, located, never silent. Every error has aFILE:LINE: error: [E#] messageshape. - Below the seam (transport / realise / backend): wrap-and-reraise at boundaries.
TransportError,SourceFetchError, etc. - Type hints on every public function. Dataclasses over dicts. Functional core, imperative shell.
from __future__ import annotationsis a required first import in every module.- Project Python playbooks live in
local-notes/playbooks/python/(coding-guidelines.md,testing.md,CHECKLISTS.md).
Commit conventions¶
Conventional Commits-ish:
feat(realise): std.python_venv recipe class + pip bootstrap
fix(migrate): autotools --with-X flags use ${DEP:X} not brew-bridge
docs(status): per-topic corpus status report + script
chore(coverage): mark 4 more Linux-only deps as darwin-noop
The first line is a one-liner that survives in git log --oneline. The body explains why, not what (the diff shows what).
When to write a stage / what counts as done¶
Each stage ends with a one-paragraph entry in notes/08-status.md. The Definition of Done is:
- Code lands.
- Conformance tests for the property the stage adds are green (
tests/c_e2e/test_conformance_stage<N>.py). - Docs are updated — at minimum, the relevant
docs/concepts/,docs/cli/, anddocs/status/what-works.mdentries. - STATUS paragraph in
notes/08-status.mdsummarising what shipped + what's deferred to the next stage.
A regression in any shipped conformance property blocks the release. There's no "we'll fix this in a follow-up."
Related¶
- Architecture — the eval/realise seam in detail.
- Feature procedure — the four-way classification every feature must pass.
- Invariants — what NOT to break.
- Testing — the test pyramid + dogfood loop.
- Adding a package — the practical recipe-author workflow.
- Extending Punix — adding backends / recipe classes / source kinds.