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.
← Commit history
Merge PR #369: kicad-lane: 1.0.97, tested on KiCad 10.0.8; releases record the KiCad they were tested on
5 files changed
+67−8
rust/Cargo.lock+3−3@@ -319,7 +319,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.96"+version = "1.0.97" dependencies = [ "adom-eda-checks", "kicad-core",@@ -331,7 +331,7 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.96"+version = "1.0.97" dependencies = [ "kicad-ipc-rs", "kicad-platform",@@ -358,7 +358,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.96"+version = "1.0.97" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.96"+version = "1.0.97" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/verbs.rs+30@@ -159,6 +159,16 @@ fn status(state: &mut State) -> Value { }) } +/// 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!({@@ -170,7 +180,13 @@ fn readiness(state: &mut State) -> Value { "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();@@ -654,3 +670,17 @@ fn actual_mechanism(o: &serde_json::Map<String, Value>, catalog: &str) -> String _ => 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");+ }+}
skills/kicad-bridge-test/run_verb_tests.py+7−1@@ -651,6 +651,11 @@ def main(): 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",@@ -1989,7 +1994,7 @@ def main(): 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,@@ -1999,6 +2004,7 @@ def main(): } (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]
tools/build_native_release.py+26−3@@ -93,6 +93,25 @@ def main(): 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}")@@ -112,7 +131,7 @@ def main(): 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"@@ -202,10 +221,10 @@ def main(): 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.@@ -227,6 +246,10 @@ def publish_zip(ver, zpath, sha, notes): 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")}