Concepts¶
pcbkit is a pipeline of small stages that hand files to each other. The board project interface is the reference for every file and key; this page explains how the pieces fit, and links there for the details.
A board is a project¶
A board project is a folder with a pcbkit.toml. Every command finds it from the current
folder upwards. These are the files you write:
| File | What it says | Reference |
|---|---|---|
pcbkit.toml |
The board's names and revision, the stackup, the router and stitching settings, the fab profile, and which check groups run. | pcbkit.toml |
design.py |
The circuit: every part, and the net on each of its pins. | design.py |
layout.py |
The board's size, corners and mounting holes, and where each part sits. | layout.py |
routing.py |
What routing needs from you: net classes, design rules, hand-made routes, keep-outs and the ground pours. | routing.py |
silk.py |
The silkscreen text: labels, polarity marks, the title block. | silk.py |
specs.py |
The numbers the checks read, such as datasheet limits. | Checks |
checks/test_*.py |
Your own checks, written as pytest functions. | Writing a check |
mutants.py |
Planted mistakes that the checks must catch. | mutants.py |
Three more are optional: bom.py (when the BOM should say something other than the
schematic), footprints.py and footprints/ (a symbol or footprint that KiCad does not
have), and circuits.py (operating-point and SPICE helpers for the circuit checks).
In design.py, two pins that name the same net are connected: pcbkit draws the schematic
with a label on each pin and no wires. The board is built from the netlist that KiCad
exports from that schematic, not from design.py directly, so the two cannot drift apart.
Coordinates in layout.py and in the routing hooks are millimetres from the board's
top-left corner, with y pointing down; pcbkit adds KiCad's page offset itself.
The pipeline¶
Eight stages take the project from circuit to fab files. The second column says which command runs each one, and the last what you steer it with.
| Stage | Run by | What happens | You steer it with |
|---|---|---|---|
| 1. Schematic and ERC | sch, build |
Draws the schematic from design.py, runs KiCad's ERC on it and exports the netlist. |
design.py |
| 2. Placement | build |
Builds the board from the netlist: each footprint at its position, the outline, the mounting holes and the stackup. | layout.py, [stackup] |
| 3. Pre-route | route |
Applies the net classes and design rules, runs your prerouted and keepouts hooks (hand-made copper is locked, so the router goes round it), adds a keep-out strip along each board edge and exports the router's input. |
routing.py |
| 4. Freerouting | route |
The autorouter completes what is left. It runs up to [route] tries times, and a run that never starts routing is killed after stall_timeout_s. |
[route] |
| 5. Post | route, finalize |
Imports the router's result, runs your gnd_links and zones hooks, fills the pours, stitches the ground layers together with vias, and takes out vias that carry copper on one layer only and tracks that end nowhere. |
routing.py, [stitch] |
| 6. Silkscreen | route, finalize |
Adds your labels, a reference designator for every part, the assembly drawing text and the title block. | silk.py |
| 7. DRC | route, promote, finalize |
KiCad's design rule check, with schematic parity: the board must match the schematic as well as obey the rules. | the rules in routing.py |
| 8. Fab files | finalize |
The Gerbers and drill files in a zip, the BOM, the pick-and-place file, PDFs and 3D renders. | bom.py, [fab] |
build design.py -> [1 schematic, ERC, netlist] -> [2 placement]
route [3 pre-route] -> [4 router] -> [5 post] -> [6 silk] -> [7 DRC]
promote the routed board, if its DRC is clean -> golden/
finalize golden/ -> [5 post] -> [6 silk] -> [7 DRC] -> [8 fab files]
route runs stage 3, then stages 4 and 5 with a DRC after each try, until a try comes out
with no copper problem; it finishes with stages 6 and 7. finalize starts from golden/
instead of the router: it runs stages 5 to 8, and stops without exporting anything if DRC
finds a single problem.
The commands that run these hooks
has the details of both, and the files each stage hands to the next.
Golden routes¶
Freerouting does not promise the same board for the same input. On one Mac a run once left a net unrouted and the next run completed it. So a fresh route is a new layout, and you should look at it before you order boards made from it.
pcbkit promote is how you say "this one". It runs DRC on the routed board, refuses unless
there is nothing to report, and copies three files into golden/: the placed board with
your hand-made routes (prerouted.kicad_pcb), the router's result (<stem>.ses) and its
input (<stem>.dsn). pcbkit finalize rebuilds the board from those, with no router, so
the fab files always come from a route you accepted. golden/ is part of your project:
commit it.
When you change the circuit or the layout, you do not have to lose that route. pcbkit
route --eco golden copies the golden route's copper onto the new board and locks it,
except where it clashes with what changed, and lets Freerouting complete the rest. If the
router stalls, route routes the whole board instead and says so. Use a plain route when
the placement really changed: it is a new layout, so review it again. Either way, promote
and finalize follow.
finalize runs DRC with schematic parity on what it rebuilt, so a golden route that has
fallen out of step with the schematic, after a part's value changed for instance, stops it
before anything is exported (see
Getting started). To see what a re-route moved,
keep a copy of the old kicad/<stem>.kicad_pcb and run pcbkit compare OLD NEW
(comparing boards).
Checks, specs and mutants¶
pcbkit check runs two kinds of check with pytest, in one run: the built-in checks of the
groups listed in [checks] groups, and your own, the test_*.py files in checks/.
| Group | What it judges | Reads |
|---|---|---|
kicad (on by default) |
ERC on the schematic, and DRC with schematic parity. | nothing |
outputs (on by default) |
The Gerbers, drill files, BOM and pick-and-place file, read back and compared with the board and the schematic; and that every fitted part has a part number a buyer can order. | the fab files, pcbkit.toml, layout.py; design.py and bom.py for the part numbers |
fab |
The fab house's limits, polarity marks on wire pads, decoupling capacitors close to their pins, the rise time of an I2C bus. | specs.py |
copper |
Regions reserved for some nets, ground stitching, coupling into sensitive traces, the current and voltage drop of the heavy copper. | specs.py |
circuit |
Operating points solved from the netlist: pins against the datasheet, LED currents, resistor power, capacitor voltage, supply pins. | specs.py, circuits.py |
esp32s3 |
Pin rules for a board that carries an ESP32-S3 DevKit: strapping pins, analogue inputs, the current each GPIO sources. | specs.py |
What each group reads, name by name, is in Built-in groups.
specs.py is where the numbers live: a datasheet limit, a design range, the fab house's
minimums. Write each one's source in a comment next to it, as blinky's specs.py does. A
group that you switch on but give no number to does not skip: its check fails and names
the missing name and the file, so a check cannot go quiet because nobody wrote down what
it should hold the board to.
Your own checks are ordinary pytest functions that ask for fixtures: nl (the netlist),
board (the routed board), specs, record. The blinky example has two, in
checks/test_indicator.py
(source).
Writing checks shows how to write your own.
A run leaves out/checks/results.json, and pcbkit report turns it into
out/checks/VALIDATION.md. A run with -k leaves a partial result, marked as such, which
the order skill in the Claude plugin refuses.
Mutants¶
A check that has never been seen to fail is not trusted. A check that is always green may be checking nothing: a net name with a typo, a limit that is far too loose, a table that came out empty. So every hazard gets a planted mistake, and you watch the check fail on it.
mutants.py lists mistakes as edits to design.py: what is wrong, the text to find and
its replacement, and the check that must then fail. pcbkit mutants first runs a control,
the same checks on an unedited scratch copy of the project, which must pass. Then it plants
each mistake in a fresh scratch copy, rebuilds the schematic there and runs the checks. A
mistake is caught when a check fails, and missed when every check still passes, or
when the text to find is not in design.py: a stale entry counts as a miss. The exit code
is 0 when every mistake is caught, 1 when one is missed and 2 when the control fails.
Mutants edit design.py only, so they prove the checks that read the netlist. A check on
the copper or the layout is proven another way: a test that builds a bad board and asserts
that the check fails on it.
A failing check is information. Never loosen a limit, skip a check or delete it to get a green run: fix the design, or change the limit and write down the source for the new number.
The two tiers¶
KiCad's pcbnew Python module is how pcbkit reads and edits a board: it moves tracks,
fills pours and runs the design rules. It only imports under the Python that ships inside
KiCad (3.9 in KiCad 10 on macOS), so it cannot be imported from whichever Python you
installed pcbkit with. That is why pcbkit runs on Python 3.9 and newer, and why its
commands come in two tiers:
| Tier 1 | Tier 2 | |
|---|---|---|
| Commands | new, doctor, setup, sch, report, quote, shots |
build, route, promote, finalize, check, mutants, compare |
Imports pcbnew |
no | yes |
| Runs in | any Python that has pcbkit: uvx, uv tool install, a virtual environment |
the board project's own .venv, as .venv/bin/pcbkit |
| Also needs | sch and shots call kicad-cli; shots also needs rsvg-convert |
kicad-cli, and rsvg-convert for finalize; route also needs Java and Freerouting |
pcbkit setup joins the two. It builds .venv on KiCad's own Python with
--system-site-packages, installs the project's dependencies (pcbkit and pytest) into it,
and checks that import pcbnew works there. From then on you run tier 2 commands as
.venv/bin/pcbkit; you never activate the environment. Run one anywhere else and it stops
at once, with a message instead of a traceback:
If you change the project's dependencies, run pcbkit setup again rather than a bare uv
sync, which can rebuild .venv on a Python that cannot import pcbnew.
Generated folders¶
pcbkit writes three folders and treats a fourth as generated too. Never edit them by hand: the next command that writes them overwrites your edit, and so does a save from KiCad's GUI. Change the Python instead.
| Folder | What is in it | Written by | In git |
|---|---|---|---|
kicad/ |
The schematic, netlist, placed and routed boards, the ERC and DRC reports, the router's files. | sch, build, route, promote, finalize |
No: .gitignore leaves it out. |
out/ |
out/fab/ (what you send to the fab house), out/docs/ (PDFs and renders), out/checks/, out/shots/. |
finalize, check, report, shots |
No. |
golden/ |
The route you accepted. | promote |
Yes: finalize needs it. |
fab/ |
Not written by any pcbkit command. If it exists, quote and the outputs checks read it when out/fab/ is not there. |
you | Your choice. |
pcbkit never deletes a file that it no longer makes. When kicad/ has gone stale, delete
the folder and run pcbkit build: golden/ keeps the route. Opening the files in KiCad to
look at them is fine. The guard hooks of the Claude plugin stop Claude
Code from editing these folders, though they cannot see a shell command that does.