Developing yapnr¶
How the repository is built, tested and laid out, plus the environment notes worth knowing. For what yapnr is, see README.md; for the migration from Splanc, see docs/migration-plan.md.
Prerequisites¶
Everyday commands¶
bazel run //:yapnr -- --version # the CLI
bazel run //:doctor # environment report (Python, numpy, torch, KiCad CLI)
bazel run //:viewer -- --root <live> # the live viewer on http://127.0.0.1:8766
bazel test //... # unit tests and repo checks (no KiCad needed)
bazel test --config=quick //... # skip tests tagged slow
bazel run //docs:build # docs -> docs/site/html/
bazel run //docs:serve # preview on http://127.0.0.1:8000/
prek run --all-files # presubmit lints
tools/release/version.py --field pep440 # the version of this checkout (from git)
tools/image/build_local.sh # build and smoke-test the container images (Docker)
yapnr atopile setup # the pinned atopile environment (needs uv 0.12.21)
tools/atopile/update_locks.sh --check # the atopile locks are current (network)
tools/image/smoke_part_cache.sh # build and smoke-test the part cache image (Docker)
The atopile toolchain and its tests are described in
docs/frontends/atopile.md; the part cache in
docs/part-cache.md. Tests that run a real atopile build are tagged
atopile and manual.
Test tiers and tags are described in docs/architecture.md.
New test files go under tests/<tier>/<area>/test_*.py in a package whose BUILD.bazel calls
yapnr_py_tests(); the wiring check fails on test files no target runs.
Bazel¶
Output base: internal, case-sensitive disk¶
Keep Bazel’s output base on an internal, case-sensitive disk, not on an external or
case-insensitive volume: rules_python’s extracted interpreter tree has files that differ only by
case, and a case-insensitive output base corrupts. The content-addressed caches that .bazelrc
puts in the checkout (.bazel-disk-cache/, .bazel-repo-cache/) are safe anywhere, because their
file names are hashes.
Set it once in a gitignored user.bazelrc at the repository root (it is try-imported by
.bazelrc). Use an absolute path; ~ is not expanded there:
startup --output_base=/absolute/path/to/home/.cache/yapnr-bazel
If a Bazel server wedges (for example a defunct java process after a crash), point Bazel at a
fresh output base rather than killing processes you did not start.
Python dependencies¶
requirements.in lists the direct dependencies; requirements.lock is the resolved, hashed set
that Bazel consumes as @yapnr_pypi. After editing requirements.in:
bazel run //:requirements.update # regenerate requirements.lock (hermetic Python 3.11)
bazel test //:requirements.test # freshness check (network; tagged manual)
Resolve on darwin-arm64 or linux-aarch64 only; see
docs/decisions.md for why the lock is not valid on
linux-x86_64 yet. Commit MODULE.bazel.lock whenever it changes; CI runs with
--lockfile_mode=error.
Presubmit (prek)¶
The lint gate is prek, a drop-in reimplementation of pre-commit,
with the hooks and pins in .pre-commit-config.yaml (Splanc’s set plus a privacy scan). Set up a
checkout once:
./setup-precommit.sh
The script uses a prek on PATH only if it is 0.4.12 (it warns otherwise) and else installs
prek 0.4.12 into a private virtualenv under .venv/prek (with uv when available); it never
installs into the system Python. It then installs the git hook and runs every lint once. CI runs
the same version with the same config.
The privacy scan (tools/privacy_scan.py) rejects absolute home, volume and temporary-directory
paths (also in the dash-encoded form agent tooling uses for project directories), tailnet and
.local host names, tailnet and private network addresses, personal e-mail addresses and
credentials. Use repository-relative or ~ paths, documentation values (example.com,
192.0.2.0/24) and GitHub noreply addresses instead. The owner’s public commit address is no
exception in files; only tools/privacy/allowed_identities.txt names it. An @ match counts as
an e-mail address only if its local part has a letter or digit, it is not a call (followed by
() and its top-level domain is in the IANA root zone (the static copy in
tools/privacy/iana_tlds.txt), so decorators, matrix products and endpoints such as
net@board.usbc:A6 in code are not findings. --list-rules prints the rules.
KiCad¶
yapnr drives KiCad 10 headlessly: kicad-cli for DRC and exports, and KiCad’s bundled Python
(pcbnew) for worker processes. yapnr doctor reports what is configured; set the CLI with
YAPNR_KICAD_CLI (PNR_KICAD_CLI is accepted as an alias). Toolchain discovery
(yapnr.kicad.toolchain) arrives in PR6a.
macOS: use a headless copy (no Dock icons)¶
On macOS every kicad-cli call from the stock KiCad.app registers with LaunchServices as a
foreground application, so the KiCad icon blips in the Dock for each call (the engine makes
several per second). Never point yapnr, tests or scripts at the stock bundle. Make a
background-only copy instead; an APFS clone costs no extra disk:
mkdir -p ~/Applications
cp -c -R /Applications/KiCad/KiCad.app ~/Applications/KiCad-headless.app
plutil -replace CFBundleIdentifier -string org.kicad.kicad.headless \
~/Applications/KiCad-headless.app/Contents/Info.plist
plutil -replace LSBackgroundOnly -bool true \
~/Applications/KiCad-headless.app/Contents/Info.plist
codesign --force --deep --sign - ~/Applications/KiCad-headless.app
export YAPNR_KICAD_CLI=~/Applications/KiCad-headless.app/Contents/MacOS/kicad-cli
The copy’s kicad-cli registers as background-only and never shows a Dock icon; its DRC results
match the stock bundle. KiCad’s Python workers do not register with the Dock. A future
yapnr kicad make-headless command automates this (PR3d). Redo the copy after upgrading KiCad.
The live viewer¶
yapnr/viewer is the web front end for experiments (docs/viewer.md; code
layout and tests in yapnr/viewer/README.md). Notes for working on it:
Run a development copy on a spare loopback port, never on the ports of viewers other people use:
bazel run //:viewer -- --root <live> --port 8795. It reads the same live directory safely (writes only pins, drafts, snapshots and control requests you make).The served files are
//yapnr/viewer:dist:static/plus elkjs and three.js, fetched pinned by sha256 at build time (never vendored; THIRD_PARTY.md). To upgrade one, change its URL and sha256 inMODULE.bazeland the file hashes intests/unit/viewer/test_dist.py, and check the schematic and 3D views in a browser.KiCad for the viewer comes from
--kicad-cli/--kicad-python,YAPNR_KICAD_CLI(PNR_KICAD_CLI) andYAPNR_KICAD_PYTHON, or~/.config/yapnr/config.toml; see KiCad for the headless copy. The Ask agent and AI net labels make paid Claude calls and are off unless--agent on/--net-summaries on; the unit tests use fake CLIs, and the live checks intests/e2e/viewerare manual.The front end is plain JavaScript without a build step or a node toolchain; there is no JavaScript test runner yet, so check changes to
static/in a browser (headless Chrome works).
Container images and releases¶
The container images (ghcr.io/studio-fug/yapnr and its KiCad base) are described in
docs/containers.md, including how to build them locally
(tools/image/build_local.sh, which needs Docker and a few GB of disk). Versions come from git tags
only; docs/releases.md has the versioning rules, the pull request labels that
group the release notes, and the owner’s release checklist. Agents never create tags or releases.
After changing a runtime pin (torch, numpy, pyyaml or their dependencies) in requirements.lock,
regenerate the image’s runtime locks with tools/image/update_runtime_locks.sh (needs uv);
tests/unit/repo/test_images.py fails until they agree.
Repository layout¶
Path |
What |
|---|---|
|
the Python package (today: version, CLI and the live viewer) |
|
the atopile toolchain: setup, runner, hook, offline picker, locks |
|
the part cache: store, server, clients, importer |
|
the atopile toolchain discovery and |
|
hermetic unit tests and repo checks |
|
manual live checks (paid agent calls, real KiCad exports) |
|
synthetic test inputs (the viewer’s small atopile project) |
|
privacy scan, test-wiring check, internal Bazel macros |
|
version derivation, release notes, image build and smoke test |
|
the yapnr wheel ( |
|
the container images: |
|
documentation (Sphinx with MyST); the site is built by |
|
logo, mark, favicons and palette |
|
CI workflows, CODEOWNERS, pull request template |
The planned full layout is in docs/migration-plan.md.
CI¶
.github/workflows/ci.yaml runs on pull requests, pushes to main and on demand:
lint: prek on all files; every new commit must use an address listed intools/privacy/allowed_identities.txt(the owner’s public commit address) or a GitHub noreply address (privacy_scan.py --identities), and the new commits’ messages and patches are privacy-scanned (git log -p --diff-merges=separate, so a merge commit’s diff against each parent is included).test:bazel test //... --config=cionubuntu-24.04-arm, and the lock freshness check.docs: builds the site and uploads it as an artifact.deploy-preview,cleanup-preview,deploy-pages: publish to GitHub Pages (previews underpr-preview/pr-N/, only afterlint,testanddocspass). Skipped unless the repository variableYAPNR_PAGES_ENABLEDistrue. The first deploy is a manual run onmain, which creates thegh-pagesbranch; the steps are in the comment above the Pages jobs inci.yaml.
lint, test and docs are the required checks. .github/workflows/macos.yaml runs the tests on
macos-latest for information only.
.github/workflows/ladder.yaml runs the native regression ladder (hardware/pnr/regression) inside
the published arm64 image ghcr.io/studio-fug/yapnr:edge, resolved to a digest: cases 01 to 06 with
seed 0 on pull requests that change engine inputs, and nightly all 8 cases with seeds 0 and 1 plus
a traced run with the initial placement pool, whose animations (//hardware/pnr:ladder_animations)
are uploaded as an artifact. A runtime guard installs the checkout’s runtime lock into an overlay
venv when it differs from the image’s. tools/ci/ladder_summary.py writes the step summary. The
lane is informational: its ladder check is not required yet.
.github/workflows/image.yaml builds and smoke-tests the container images for linux/amd64 and
linux/arm64 on pull requests that change what goes into them, and publishes edge from such
merges to main (informational until v0.1.0).
.github/workflows/release.yaml runs on release tags (and as a dry run on demand): CI at the tag,
the images with the release tags, and the GitHub release. See
docs/releases.md.