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.
master
John Lauer
1.0.1: 1.0.1: Freerouting on the user's terms. kicad_freerouting {status|install|uninstall} installs Freerouting's own…
471d9cf
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
//! kicad_uninstall: remove everything this bridge ever created (Bridge SDK uninstall
//! contract, kicad-bridge issue #18). Idempotent, non-interactive, best effort per item,
//! scoped to our own artifacts. Port of handlers/uninstall.py; `testResidue` is the
//! subset the verb runner leaves behind.
use std::path::{Path, PathBuf};
use serde_json::{json, Value};
use crate::libraries::LibTable;
use kicad_platform::native;
const MESA_DLLS: [&str; 3] = ["opengl32.dll", "libgallium_wgl.dll", "dxil.dll"];
struct Report {
dry: bool,
removed: Vec<String>,
failed: Vec<Value>,
absent: usize,
}
impl Report {
fn rm_file(&mut self, p: &Path, label: &str) {
if !p.exists() {
self.absent += 1;
return;
}
if !self.dry {
if let Err(e) = std::fs::remove_file(p) {
self.failed.push(json!({"item": format!("{label}: {}", p.display()), "error": e.to_string()}));
return;
}
}
self.removed.push(format!("{label}: {}", p.display()));
}
fn rm_tree(&mut self, p: &Path, label: &str) {
if !p.exists() {
self.absent += 1;
return;
}
if !self.dry {
if let Err(e) = remove_tree_force(p) {
self.failed.push(json!({"item": format!("{label}: {}", p.display()), "error": e}));
return;
}
}
self.removed.push(format!("{label}: {}", p.display()));
}
fn remove_row(&mut self, table_path: &Path, nickname: &str) {
if !table_path.exists() {
self.absent += 1;
return;
}
match LibTable::parse_file(table_path) {
Ok(mut t) => {
if t.has_library(nickname) {
if !self.dry {
t.remove_library(nickname);
if let Err(e) = t.write_file(table_path) {
self.failed.push(json!({"item": format!("'{nickname}' row in {}", table_path.display()), "error": e}));
return;
}
}
self.removed.push(format!("'{nickname}' row in {}", table_path.display()));
} else {
self.absent += 1;
}
}
Err(e) => self.failed.push(json!({"item": format!("'{nickname}' row in {}", table_path.display()), "error": e})),
}
}
}
/// rmtree that clears read-only bits on the way (the Python's onerror chmod).
fn remove_tree_force(p: &Path) -> Result<(), String> {
if let Err(first) = std::fs::remove_dir_all(p) {
if let Ok(rd) = std::fs::read_dir(p) {
for e in rd.flatten() {
if let Ok(md) = e.metadata() {
let mut perm = md.permissions();
if perm.readonly() {
#[allow(clippy::permissions_set_readonly_false)]
perm.set_readonly(false);
let _ = std::fs::set_permissions(e.path(), perm);
}
}
}
}
std::fs::remove_dir_all(p).map_err(|e| format!("{first}; retry: {e}"))?;
}
Ok(())
}
fn subdirs(root: Option<PathBuf>) -> Vec<PathBuf> {
let Some(root) = root else { return Vec::new() };
let Ok(rd) = std::fs::read_dir(&root) else { return Vec::new() };
rd.flatten().filter(|e| e.path().is_dir()).map(|e| e.path()).collect()
}
fn glob_prefix(dir: &Path, prefix: &str, suffix: &str) -> Vec<PathBuf> {
let Ok(rd) = std::fs::read_dir(dir) else { return Vec::new() };
rd.flatten()
.filter(|e| {
let n = e.file_name().to_string_lossy().to_string();
n.starts_with(prefix) && n.ends_with(suffix)
})
.map(|e| e.path())
.collect()
}
/// Every KiCad bin dir this bridge may have dropped the Mesa fallback into.
fn bin_dirs(installs: &[crate::detect::KicadInstall]) -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = Vec::new();
for i in installs {
let b = PathBuf::from(&i.bin_dir);
if b.is_dir() && !out.iter().any(|x| x.to_string_lossy().eq_ignore_ascii_case(&b.to_string_lossy())) {
out.push(b);
}
}
out
}
pub fn handle(installs: &[crate::detect::KicadInstall], args: &Value) -> Value {
let dry = args.get("dryRun").and_then(Value::as_bool).unwrap_or(false);
let test_residue = args.get("testResidue").and_then(Value::as_bool).unwrap_or(false);
let mut r = Report { dry, removed: Vec::new(), failed: Vec::new(), absent: 0 };
let plat = native();
if test_residue {
return remove_test_residue(&mut r);
}
for ver_dir in subdirs(plat.user_root()) {
let site = ver_dir.join("3rdparty").join("Python311").join("site-packages");
r.rm_file(&site.join("adom_bridge.py"), "plugin payload");
r.rm_file(&site.join("usercustomize.py"), "plugin loader");
let pyc = site.join("__pycache__");
if pyc.is_dir() {
for f in glob_prefix(&pyc, "adom_bridge", ".pyc").into_iter().chain(glob_prefix(&pyc, "usercustomize", ".pyc")) {
r.rm_file(&f, "plugin pycache");
}
}
r.rm_file(&ver_dir.join("symbols").join("Adom.kicad_sym"), "Adom symbol library");
r.rm_tree(&ver_dir.join("footprints").join("Adom.pretty"), "Adom footprint library");
r.rm_tree(&ver_dir.join("3dmodels").join("Adom"), "Adom 3D model library");
}
for cfg in subdirs(plat.config_root()) {
for table in ["sym-lib-table", "fp-lib-table"] {
r.remove_row(&cfg.join(table), "Adom");
}
}
for b in bin_dirs(installs) {
for dll in MESA_DLLS {
r.rm_file(&b.join(dll), "mesa fallback");
r.rm_file(&b.join(format!("{dll}.local")), "mesa fallback marker");
}
for loc in glob_prefix(&b, "", ".exe.local") {
r.rm_file(&loc, "mesa dotlocal");
}
}
for f in glob_prefix(&std::env::temp_dir(), "adom-kicad-bridge-", ".json") {
r.rm_file(&f, "plugin discovery file");
}
r.rm_tree(Path::new("C:/tmp/adom-desktop-screenshots"), "screenshot cache");
r.rm_tree(&plat.freerouting_dir(), "Freerouting engine (kicad_freerouting install)");
if let Some(docs) = plat.documents_dir() {
r.rm_tree(&docs.join("adom-kicad-demo"), "demo project");
r.rm_tree(&docs.join("adom-part-exports"), "one-off part exports");
r.rm_tree(&docs.join("adom-part-staging"), "part install staging");
}
let ok = r.failed.is_empty();
json!({
"success": ok,
"cascade": args.get("cascade").and_then(Value::as_bool).unwrap_or(false),
"dryRun": dry,
"removed": r.removed,
"failed": r.failed,
"alreadyClean": r.removed.is_empty() && ok,
"summary": format!("{} {} item(s), {} failed, {} already absent", if dry { "would remove" } else { "removed" }, r.removed.len(), r.failed.len(), r.absent),
"_hint": "Bridge-created artifacts only: KiCad itself, user projects, and other bridges are untouched. Idempotent: safe to re-run. A running KiCad may hold the plugin's .pyc open; re-run after KiCad exits if anything failed. Bridge removes its own bridges-cache during the cascade.",
})
}
fn remove_test_residue(r: &mut Report) -> Value {
let plat = native();
if let Some(docs) = plat.documents_dir() {
r.rm_tree(&docs.join("adom-kicad-verbtest"), "verbtest fixture dir");
r.rm_tree(&docs.join("adom-kicad-demo"), "demo project");
}
for ver_dir in subdirs(plat.user_root()) {
r.rm_file(&ver_dir.join("footprints").join("Adom.pretty").join("ADOM_VERBTEST.kicad_mod"), "test footprint");
let sym = ver_dir.join("symbols").join("Adom.kicad_sym");
if sym.exists() {
match std::fs::read_to_string(&sym) {
Ok(text) => match crate::libraries::extract_top_level_symbols(&text) {
Ok(blocks) => {
let hits: Vec<&String> = blocks.iter().filter(|(n, _)| n == "ADOM_VERBTEST").map(|(_, b)| b).collect();
if !hits.is_empty() {
if !r.dry {
let mut out = text.clone();
for b in &hits {
out = out.replacen(b.as_str(), "", 1);
}
if let Err(e) = std::fs::write(&sym, out) {
r.failed.push(json!({"item": format!("ADOM_VERBTEST in {}", sym.display()), "error": e.to_string()}));
continue;
}
}
r.removed.push(format!("ADOM_VERBTEST symbol in {}", sym.display()));
}
}
Err(e) => r.failed.push(json!({"item": format!("ADOM_VERBTEST in {}", sym.display()), "error": e})),
},
Err(e) => r.failed.push(json!({"item": format!("ADOM_VERBTEST in {}", sym.display()), "error": e.to_string()})),
}
}
}
for cfg in subdirs(plat.config_root()) {
for table in ["sym-lib-table", "fp-lib-table"] {
let tp = cfg.join(table);
if tp.exists() {
r.remove_row(&tp, "AdomVerbTest");
}
}
}
for f in glob_prefix(&std::env::temp_dir(), "adom-kicad-bridge-", ".json") {
let stem = f.file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
match stem.rsplit('-').next().and_then(|p| p.parse::<u32>().ok()) {
None => r.rm_file(&f, "legacy discovery file"),
Some(pid) => {
if plat.process_alive(pid) == Some(false) {
r.rm_file(&f, "stale discovery file");
}
}
}
}
let ok = r.failed.is_empty();
json!({
"success": ok, "testResidue": true, "dryRun": r.dry,
"removed": r.removed, "failed": r.failed,
"alreadyClean": r.removed.is_empty() && ok,
"summary": format!("{} {} test-residue item(s), {} failed", if r.dry { "would remove" } else { "removed" }, r.removed.len(), r.failed.len()),
"_hint": "Suite residue only: plugin, Adom libraries, KiCad and user files untouched. Run after every verb-suite pass; idempotent.",
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dry_run_reports_without_touching() {
let tmp = std::env::temp_dir().join(format!("kb-uninstall-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let demo = tmp.join("adom-kicad-demo");
std::fs::create_dir_all(&demo).unwrap();
std::fs::write(demo.join("x.txt"), "x").unwrap();
let mut r = Report { dry: true, removed: vec![], failed: vec![], absent: 0 };
r.rm_tree(&demo, "demo project");
r.rm_file(&tmp.join("nope"), "absent");
assert_eq!(r.removed.len(), 1);
assert_eq!(r.absent, 1);
assert!(demo.is_dir(), "dry run must not delete");
let mut r = Report { dry: false, removed: vec![], failed: vec![], absent: 0 };
r.rm_tree(&demo, "demo project");
assert!(!demo.exists());
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn stale_discovery_pid_parsing() {
let f = PathBuf::from("adom-kicad-bridge-12345.json");
let stem = f.file_stem().unwrap().to_string_lossy().to_string();
assert_eq!(stem.rsplit('-').next().unwrap().parse::<u32>().ok(), Some(12345));
let f = PathBuf::from("adom-kicad-bridge-legacy.json");
let stem = f.file_stem().unwrap().to_string_lossy().to_string();
assert_eq!(stem.rsplit('-').next().unwrap().parse::<u32>().ok(), None);
}
}