#!/usr/bin/env bash # kicad-build — orchestrator for circuit_toolkit + kicad-cli. # # Usage: # ./kicad-build [--erc] [--drc] [--gerber] [--bom] [--schematic] [--sim] [--all] # # Steps (all are optional, --all runs everything): # 1. circuit_toolkit build (generates .kicad_sch + .kicad_pcb) # 2. kicad-cli sch erc (electrical rules check) # 3. kicad-cli pcb drc (design rules check) # 4. kicad-cli pcb export gerber/drill/position # 5. kicad-cli sch export bom/pdf/netlist # 6. SPICE simulation (if sim/ present) # # Requires: # - KiCad.app installed (for pcbnew) # - kicad-cli on PATH (homebrew: brew install kicad) # - ngspice on PATH (for --sim, brew install ngspice) # - netlistsvg on PATH (for --schematic SVG, npm install -g netlistsvg) # - circuit_toolkit installed into KiCad's Python (run install-kicad-toolkit.sh) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" KICAD_PYTHON="$SCRIPT_DIR/kicad-python" KICAD_CLI=$(command -v kicad-cli 2>/dev/null || echo "") # circuit_toolkit paths TOOLKIT_SRC="$PROJECT_ROOT/kicad-claude-toolkit/python/circuit_toolkit" TOOLKIT_PY="$TOOLKIT_SRC/circuit_toolkit" # Output directory (relative to board dir) OUTPUT_DIR="" usage() { echo "Usage: $0 [--erc] [--drc] [--gerber] [--bom] [--schematic] [--sim] [--all]" echo "" echo " --erc Run ERC on generated schematic" echo " --drc Run DRC on generated PCB" echo " --gerber Export Gerber + drill + position files" echo " --bom Generate BOM (flat + JLCPCB)" echo " --schematic Generate schematic SVG via netlistsvg" echo " --sim Run SPICE simulations (if sim/ present)" echo " --all Run all steps (default)" echo " --dry-run Print planned commands without executing" exit 1 } # Parse args BOARD_DIR="" DO_ERC=false DO_DRC=false DO_GERBER=false DO_BOM=false DO_SCHEMATIC=false DO_SIM=false DO_ALL=true DRY_RUN=false for arg in "$@"; do case "$arg" in --erc) DO_ERC=true; DO_ALL=false;; --drc) DO_DRC=true; DO_ALL=false;; --gerber) DO_GERBER=true; DO_ALL=false;; --bom) DO_BOM=true; DO_ALL=false;; --schematic) DO_SCHEMATIC=true; DO_ALL=false;; --sim) DO_SIM=true; DO_ALL=false;; --dry-run) DRY_RUN=true;; -h|--help) usage;; -*) echo "Unknown option: $arg" >&2 usage;; *) if [ -z "$BOARD_DIR" ]; then BOARD_DIR="$arg" else echo "Unexpected argument: $arg" >&2 usage fi ;; esac done if [ -z "$BOARD_DIR" ]; then echo "ERROR: board directory required" >&2 usage fi BOARD_DIR="$(cd "$BOARD_DIR" && pwd)" OUTPUT_DIR="$BOARD_DIR/output" if $DO_ALL; then DO_ERC=true; DO_DRC=true; DO_GERBER=true; DO_BOM=true; DO_SCHEMATIC=true fi # ── Pre-flight checks ────────────────────────────────────────────────── check_kicad_python() { if ! $DRY_RUN && ! bash "$KICAD_PYTHON" -c "import pcbnew" 2>/dev/null; then echo "ERROR: pcbnew not available. Run KiCad.app at least once, then retry." >&2 exit 1 fi } check_kicad_cli() { if [ -z "$KICAD_CLI" ]; then echo "ERROR: kicad-cli not on PATH. Install via: brew install kicad" >&2 exit 1 fi } # ── Step 1: circuit_toolkit build ────────────────────────────────────── step_build() { local build_script="$BOARD_DIR/build.py" if [ ! -f "$build_script" ]; then echo "SKIP build: no build.py in $BOARD_DIR" return 0 fi echo "=== Step 1: circuit_toolkit build ===" if $DRY_RUN; then echo " [dry-run] KiCad Python $KICAD_PYTHON" echo " [dry-run] cd $BOARD_DIR && $KICAD_PYTHON build.py" return 0 fi mkdir -p "$OUTPUT_DIR" pushd "$BOARD_DIR" >/dev/null PYTHONPATH="$TOOLKIT_SRC:$PYTHONPATH" \ bash "$KICAD_PYTHON" build.py popd >/dev/null echo " -> $OUTPUT_DIR/" } # ── Step 2: ERC ──────────────────────────────────────────────────────── step_erc() { local sch="$BOARD_DIR/*.kicad_sch" sch=$(ls $sch 2>/dev/null | head -1 || true) if [ -z "$sch" ]; then echo "SKIP ERC: no .kicad_sch found" return 0 fi echo "=== Step 2: ERC ===" if $DRY_RUN; then echo " [dry-run] $KICAD_CLI sch erc $sch" return 0 fi mkdir -p "$OUTPUT_DIR/erc" $KICAD_CLI sch erc "$sch" --output "$OUTPUT_DIR/erc/erc_report.txt" 2>&1 || true echo " -> $OUTPUT_DIR/erc/" } # ── Step 3: DRC ──────────────────────────────────────────────────────── step_drc() { local pcb="$BOARD_DIR/*.kicad_pcb" pcb=$(ls $pcb 2>/dev/null | head -1 || true) if [ -z "$pcb" ]; then echo "SKIP DRC: no .kicad_pcb found" return 0 fi echo "=== Step 3: DRC ===" if $DRY_RUN; then echo " [dry-run] $KICAD_CLI pcb drc $pcb" return 0 fi mkdir -p "$OUTPUT_DIR/drc" $KICAD_CLI pcb drc "$pcb" --output "$OUTPUT_DIR/drc/drc_report.html" 2>&1 || true echo " -> $OUTPUT_DIR/drc/" } # ── Step 4: Gerber/Drill/Position export ────────────────────────────── step_gerber() { local pcb="$BOARD_DIR/*.kicad_pcb" pcb=$(ls $pcb 2>/dev/null | head -1 || true) if [ -z "$pcb" ]; then echo "SKIP gerber: no .kicad_pcb found" return 0 fi echo "=== Step 4: Gerber + Drill + Position export ===" if $DRY_RUN; then echo " [dry-run] $KICAD_CLI pcb export gerbers $pcb --output $OUTPUT_DIR/fab/gerber" return 0 fi mkdir -p "$OUTPUT_DIR/fab/gerber" $KICAD_CLI pcb export gerbers "$pcb" --output "$OUTPUT_DIR/fab/gerber" 2>&1 $KICAD_CLI pcb export drill "$pcb" --output "$OUTPUT_DIR/fab/gerber" 2>&1 $KICAD_CLI pcb export pos "$pcb" --output "$OUTPUT_DIR/fab/positions.csv" 2>&1 echo " -> $OUTPUT_DIR/fab/" } # ── Step 5: BOM + schematic PDF + netlist ───────────────────────────── step_bom() { local sch="$BOARD_DIR/*.kicad_sch" sch=$(ls $sch 2>/dev/null | head -1 || true) if [ -z "$sch" ]; then echo "SKIP bom: no .kicad_sch found" return 0 fi echo "=== Step 5: BOM + schematic PDF + netlist ===" if $DRY_RUN; then echo " [dry-run] $KICAD_CLI sch export bom $sch --output $OUTPUT_DIR/fab" return 0 fi mkdir -p "$OUTPUT_DIR/fab" $KICAD_CLI sch export bom "$sch" --output "$OUTPUT_DIR/fab" 2>&1 $KICAD_CLI sch export pdf "$sch" --output "$OUTPUT_DIR/schematic.pdf" 2>&1 $KICAD_CLI sch export "$sch" --format netlist --output "$OUTPUT_DIR/fab/netlist.xml" 2>&1 echo " -> $OUTPUT_DIR/fab/" } # ── Step 6: SPICE simulation ────────────────────────────────────────── step_sim() { if [ ! -d "$BOARD_DIR/sim" ]; then echo "SKIP sim: no sim/ directory" return 0 fi echo "=== Step 6: SPICE simulation ===" if $DRY_RUN; then echo " [dry-run] ngspice -b $BOARD_DIR/sim/*.cir" return 0 fi if ! command -v ngspice &>/dev/null; then echo "SKIP sim: ngspice not on PATH (brew install ngspice)" return 0 fi mkdir -p "$OUTPUT_DIR/sim" for cir in "$BOARD_DIR"/sim/*.cir; do echo " sim: $(basename "$cir")" ngspice -b -r "$OUTPUT_DIR/sim/$(basename "$cir" .cir).raw" "$cir" 2>&1 || true done echo " -> $OUTPUT_DIR/sim/" } # ── Run ──────────────────────────────────────────────────────────────── echo "kicad-build: $BOARD_DIR" echo " KiCad Python: $KICAD_PYTHON" echo " kicad-cli: $KICAD_CLI" echo " output: $OUTPUT_DIR" echo "" check_kicad_python if [ -n "$KICAD_CLI" ]; then : # ok else echo "WARNING: kicad-cli not found (ERC/DRC/gerber/bom will be skipped)" fi step_build $DO_ERC && step_erc $DO_DRC && step_drc $DO_GERBER && step_gerber $DO_BOM && step_bom $DO_SIM && step_sim echo "" echo "=== Done ===" echo "Output: $OUTPUT_DIR/" ls -la "$OUTPUT_DIR/" 2>/dev/null || true