app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
← All Pull Requests
kicad-lane: 1.0.97, tested on KiCad 10.0.8; releases record the KiCad they were tested on #59
Merged opened by Colby Knox 2026-10-31
1.0.97: tested on KiCad 10.0.8, and every build now records the KiCad its battery ran on.
- KiCad 10.0.8: the test laptop was updated from 10.0.7 through the bridge's own kicad_tool_update apply (the official installer, per user, no UAC). This build's full battery ran on 10.0.8.
- Release records the tested KiCad: build_native_release.py --battery refuses a failed, partial or foreground-grabbing run, and refuses a run that tested another bridge version. Otherwise it writes the result:
- into the manifest as testedWith {kicad, battery, target, at}, which carries to public on promotion;
- into the release notes.
- kicad_readiness: testedKicad is the KiCad this build's battery passed on (null in a dev build). newerThanTested is true when the installed KiCad is newer.
- The battery report records the KiCad and bridge it tested (report.json "tested", and the report's heading).
Battery 355 pass / 0 fail / 1 skip on KiCad 10.0.8 (bridge 1.0.97), no coverage gap, no foreground grabs.
🤖 Generated with Claude Code
Diff Skip to comments
@@ -1,1307 +1,1308 @@⋯ 318 unchanged lines ⋯ [[package]] name = "kicad-bridge"-version = "1.0.96"+version = "1.0.97" dependencies = [ "adom-eda-checks", "kicad-core",⋯ 5 unchanged lines ⋯ [[package]] name = "kicad-core"-version = "1.0.96"+version = "1.0.97" dependencies = [ "kicad-ipc-rs", "kicad-platform",⋯ 20 unchanged lines ⋯ [[package]] name = "kicad-platform"-version = "1.0.96"+version = "1.0.97" dependencies = [ "serde", "serde_json",⋯ 940 unchanged lines ⋯ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"+@@ -1,27 +1,28 @@⋯ 2 unchanged lines ⋯ members = ["crates/*"] [workspace.package]-version = "1.0.96"+version = "1.0.97" edition = "2021" license = "MIT" publish = false⋯ 15 unchanged lines ⋯ codegen-units = 1 strip = true panic = "abort"+@@ -1,656 +1,687 @@⋯ 158 unchanged lines ⋯ }) } +/// The KiCad version this build's battery passed on, set by tools/build_native_release.py --battery (None in a dev+/// build).+pub const TESTED_KICAD: Option<&str> = option_env!("ADOM_TESTED_KICAD");++/// True when version `a` (10.0.9) is newer than `b` (10.0.8), number by number.+fn newer(a: &str, b: &str) -> bool {+ let n = |s: &str| s.split(|c: char| !c.is_ascii_digit()).filter(|x| !x.is_empty()).map(|x| x.parse::<u64>().unwrap_or(0)).collect::<Vec<_>>();+ n(a) > n(b)+}+ fn readiness(state: &mut State) -> Value { let info = state.refresh(); let mut v = json!({⋯ 5 unchanged lines ⋯ "configDir": info.primary().and_then(|p| detect::config_dir(&p.version)).map(|p| p.to_string_lossy().replace('\\', "/")), "runtime": {"kind": "native", "provisioned": false}, "platform": native().capabilities(),+ "testedKicad": TESTED_KICAD, });+ // a KiCad newer than the one this build's battery ran on works as far as KiCad kept its formats and API; say so+ let installed = info.primary().map(|i| if i.cli_version.is_empty() { i.version.clone() } else { i.cli_version.clone() });+ if let (Some(t), Some(i)) = (TESTED_KICAD, installed.as_deref()) {+ v["newerThanTested"] = json!(newer(i, t));+ } if !info.installed { v["errorCode"] = json!("not_installed"); v["alternatives"] = alternatives();⋯ 477 unchanged lines ⋯ _ => catalog.into(), } }++#[cfg(test)]+mod tested_tests {+ use super::newer;++ #[test]+ fn versions_compare_number_by_number() {+ assert!(newer("10.0.9", "10.0.8"));+ assert!(newer("10.1.0", "10.0.10"));+ assert!(!newer("10.0.8", "10.0.8"));+ assert!(!newer("9.0.7", "10.0.8"));+ assert!(newer("10.0.10", "10.0.9"), "10 is not less than 9 as text would have it");+ }+}+@@ -1,2021 +1,2028 @@⋯ 650 unchanged lines ⋯ ab(a.target, "kicad_close", {"force": True, "reason": "battery --close-open-kicad: the open editors are known to be disposable"}, timeout=120) time.sleep(4) + # the KiCad and the bridge this run tests: the release records them (testedWith)+ _lv = ab(a.target, "kicad_list_versions", {}, timeout=120)+ _inst = (_lv.get("installs") or _lv.get("versions") or [{}])[0]+ tested = {"kicad": _inst.get("cliVersion") or _inst.get("version"), "kicadSource": _inst.get("source"), "bridge": _lv.get("bridgeVersion")}+ print(f"[tested] KiCad {tested['kicad']} ({tested['kicadSource']}), bridge {tested['bridge']}") # phase 0: push fixtures print(f"[0] pushing rp2040 fixture to {base} ...") files = [str(PACK / f) for f in ("rp2040-breakout.kicad_pcb", "rp2040-breakout.kicad_sch",⋯ 1332 unchanged lines ⋯ passes = [v for v, r in results.items() if r["result"] == "PASS"] skips = [v for v, r in results.items() if r["result"] == "SKIP"] report = {- "target": a.target, "caller": AI_THREAD, "started": datetime.now(timezone.utc).isoformat(),+ "target": a.target, "caller": AI_THREAD, "started": datetime.now(timezone.utc).isoformat(), "tested": tested, "elapsed_s": round(time.time() - t0, 1), "totals": {"pass": len(passes), "fail": len(fails), "skip": len(skips)}, "coverage_gap": missing, "failures": fails, "results": results,⋯ 3 unchanged lines ⋯ } (outdir / "report.json").write_text(json.dumps(report, indent=2)) md = [f"# KiCad bridge verb test — {a.target} — {report['started'][:16]}",+ f"Bridge {tested.get('bridge')} on KiCad {tested.get('kicad')}.", f"**{len(passes)} pass / {len(fails)} fail / {len(skips)} skip** in {report['elapsed_s']}s", ""] for v in sorted(results): r = results[v]⋯ 14 unchanged lines ⋯ if __name__ == "__main__": sys.exit(main())+@@ -1,312 +1,336 @@⋯ 92 unchanged lines ⋯ if publish and not notes.strip(): sys.exit("REFUSING: --publish needs --notes \"what changed\" (the page reuses it as the changelog)") ver = version()+ # --battery <dir or report.json>: the run that tested this build. Its KiCad version goes into the binary+ # (kicad_readiness), the manifest (testedWith) and the notes; a failed or partial run refuses the release.+ tested = None+ if "--battery" in sys.argv:+ bp = Path(sys.argv[sys.argv.index("--battery") + 1])+ rep = json.loads(((bp / "report.json") if bp.is_dir() else bp).read_text())+ t, tot = rep.get("tested") or {}, rep.get("totals") or {}+ if tot.get("fail") or rep.get("coverage_gap") or rep.get("foreground_grabs"):+ sys.exit(f"REFUSING: the battery in {bp} failed ({tot.get('fail')} fail, coverage gap {rep.get('coverage_gap')}, grabs {len(rep.get('foreground_grabs') or [])})")+ if not t.get("kicad"):+ sys.exit(f"REFUSING: the battery in {bp} does not record the KiCad it ran on (run_verb_tests.py from 1.0.97 does)")+ if t.get("bridge") and t["bridge"] != ver:+ sys.exit(f"REFUSING: the battery tested bridge {t['bridge']}, not {ver}")+ tested = {"kicad": t["kicad"], "battery": f"{tot.get('pass')}/{tot.get('fail')}/{tot.get('skip')}", "target": rep.get("target"), "at": rep.get("started")}+ os.environ["ADOM_TESTED_KICAD"] = t["kicad"]+ notes = (notes.rstrip() + f"\n\nTested on KiCad {t['kicad']} (battery {tested['battery']} pass/fail/skip on {tested['target']}).") if notes else notes+ print(f"tested on KiCad {t['kicad']}, battery {tested['battery']}")+ elif publish:+ print("WARNING: no --battery given: the release will not record which KiCad it was tested on") if publish and not re.fullmatch(r"\d+\.\d+\.\d+", ver): sys.exit(f"REFUSING: {ver} is not a plain x.y.z; set [workspace.package].version in rust/Cargo.toml before publishing (ab and the tiers want one plain semver per build)") print(f"native kicad bridge {ver}")⋯ 13 unchanged lines ⋯ sys.exit("REFUSING: the exe inside the reused zip is unsigned") sha = hashlib.sha256(zpath.read_bytes()).hexdigest() print(f"reusing {zpath.name} ({len(verbs)} verbs, sha256 {sha})")- return publish_zip(ver, zpath, sha, notes) if publish else None+ return publish_zip(ver, zpath, sha, notes, tested) if publish else None sh(["cargo", "build", "--release"], cwd=RUST) sh(["cargo", "build", "--release", "--target", TARGET], cwd=RUST) exe = RUST / "target" / TARGET / "release" / "kicad-bridge.exe"⋯ 83 unchanged lines ⋯ if not publish: print("not published (pass --publish --notes \"...\" to ship to insiders)") return- publish_zip(ver, zpath, sha, notes)+ publish_zip(ver, zpath, sha, notes, tested) -def publish_zip(ver, zpath, sha, notes):+def publish_zip(ver, zpath, sha, notes, tested=None): from datetime import datetime, timezone REF = "adom/kicad-bridge" # 1. The repo must tell the same story as the zip: push every rust source file.⋯ 15 unchanged lines ⋯ url=f"https://wiki.adom.inc/download/{REF}/{ver}/{zpath.name}", released_at=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), runtime="native")+ if tested:+ m["testedWith"] = tested+ else:+ m.pop("testedWith", None) # ab reads every platforms entry as a download (url, sha256, size): one zip serves both OSes. dl = {"url": m["url"], "sha256": sha, "size": zpath.stat().st_size} m["platforms"] = {k: dict(v, **dl) for k, v in PLATFORMS.items() if v.get("supported")}⋯ 77 unchanged lines ⋯ if __name__ == "__main__": main()+
Comments
Log in to comment.
No comments yet.