← All Pull Requests

Report a library Fusion refuses instead of calling it opened #35

Merged Released in 1.13.70 on 4 Nov 2026 Approved opened by Colby Knox 2026-11-04

Fusion can refuse a library while the bridge reports that it opened. This makes that refusal visible.

What happened

A 7-part Cloud Library went through fusion_build_library_3d. The run reported stage: done, 7/7 bound and opened: true, while Fusion showed "<file>.lbr has errors and cannot be opened" over an empty design. fusion_preflight_lbr had passed the same file.

The file had 3D bindings naming packages it did not contain. The wiki sent each part's name as package, and inject_package3d_bindings wrote it into <packageinstance> as given. That only resolves when a package happens to share its part's name.

The wiki side is fixed and on prod: each part now gets its own package, and the recipe sends the real package name. This PR makes the bridge catch the same failure from any caller.

Changes

  • rust/lbr/src/lib.rs: new preflight check, package3d-refs-resolve. It fails when a <packageinstance> names a package the library does not have, and it names the offenders. It only runs on libraries with bindings, so existing golden results do not change.
  • rust/bridge/src/orchestrations.rs: open_lbr treats that check as a hard failure, so the file never reaches Fusion and no modal appears. It also runs apply_failure_dialogs before returning, so any other refusal comes back as success: false and the Error dialog is closed.
  • rust/bridge/src/mint.rs: build_library_3d_core keeps the open's error, and reports success: false when the library will not open, even if every part minted.

Testing

  • cargo test -p fusion-lbr: 23 passed, including a new test with a dangling binding and a good one.
  • cargo test -p adom-fusion-bridge: 113 passed.
  • Not run against real Fusion. The check was not exercised on the Windows box, and neither was the apply_failure_dialogs path in open_lbr.

For review

  • apply_failure_dialogs closes the Error dialog after recording it. The bare fusion_open_lbr route may already apply dialog handling, so please check that running it twice does no harm there.
  • build_library_3d_core now always includes an error key, null on success.

Based on the wiki's HEAD at bridge version 1.13.69. No version bump, so you can release it with your next batch.

Generated with Claude Code

Diff Skip to comments

--- a/rust/lbr/src/lib.rs+++ b/rust/lbr/src/lib.rs@@ -1,1238 +1,1264 @@⋯ 888 unchanged lines ⋯         }         c("pads-unique", dups.is_empty(), if dups.is_empty() { "all pad names unique per package".into() } else {             format!("duplicate pad names - Fusion's importer refuses the ENTIRE library over these: {}. Rename to e.g. 17, 17@2, 17@3...", dups.iter().take(6).cloned().collect::<Vec<_>>().join(", ")) });+        // A 3D package naming a package the library does not have makes Fusion refuse the ENTIRE+        // library: an Error modal and an empty design. Only libraries with bindings get this check.+        if root.descendants().any(|n| el(n, "packageinstance")) {+            let names: Vec<&str> = root.descendants().filter(|n| el(*n, "package")).filter_map(|p| p.attribute("name")).collect();+            let dangling: Vec<&str> = root.descendants().filter(|n| el(*n, "packageinstance")).filter_map(|p| p.attribute("name")).filter(|n| !names.iter().any(|k| k.eq_ignore_ascii_case(n))).collect();+            c("package3d-refs-resolve", dangling.is_empty(), if dangling.is_empty() { "every 3D binding names a package in this library".into() } else {+                format!("3D binding(s) name packages this library does not have, so Fusion refuses the ENTIRE library: {}. Bind to the <package> name, not the deviceset's.", dangling.iter().take(6).copied().collect::<Vec<_>>().join(", ")) });+        }     }     finish(checks, xml.as_deref()) }⋯ 19 unchanged lines ⋯ #[cfg(test)] mod heatsink_tests {     #[test]+    fn preflight_refuses_a_3d_binding_to_a_missing_package() {+        let lib = |instance: &str| format!(r#"<?xml version="1.0"?><eagle><drawing><library><packages><package name="SOIC-8"><smd name="1" x="0" y="0" dx="1" dy="1" layer="1"/></package></packages><packages3d><package3d name="{instance}" wip_urn="urn:x"><packageinstances><packageinstance name="{instance}"/></packageinstances></package3d></packages3d><symbols><symbol name="S"/></symbols><devicesets><deviceset name="NY8A054ES8"><devices><device name="" package="SOIC-8"><connects/></device></devices></deviceset></devicesets></library></drawing></eagle>"#);+        let dir = std::env::temp_dir().join(format!("fusion-lbr-refs-{}", std::process::id()));+        std::fs::create_dir_all(&dir).unwrap();+        let check = |instance: &str| {+            let path = dir.join(format!("{instance}.lbr"));+            std::fs::write(&path, lib(instance)).unwrap();+            crate::preflight_lbr(&path).checks.into_iter().find(|c| c.check == "package3d-refs-resolve").expect("the check runs on a library with bindings")+        };+        let bad = check("NY8A054ES8");+        assert!(!bad.ok, "a binding to the deviceset's name is refused");+        assert!(bad.detail.contains("NY8A054ES8"), "and names it: {}", bad.detail);+        assert!(check("SOIC-8").ok, "a binding to the real package passes");+        let _ = std::fs::remove_dir_all(&dir);+    }++    #[test]     fn heatsink_vias_are_not_shorts() {         let lbr = r#"<eagle version="9.6.2"><drawing><library><packages><package name="QFN-EP"> <smd name="B4" x="0" y="0" dx="3" dy="3" layer="1"/>⋯ 316 unchanged lines ⋯     let text = root.to_xml_document();     Ok((text, json!({"changed": changed, "connectsDropped": dropped, "renamed": renames.iter().map(|(a, b)| json!({"from": a, "to": b})).collect::<Vec<_>>()}))) }+--- a/rust/bridge/src/orchestrations.rs+++ b/rust/bridge/src/orchestrations.rs@@ -1,1803 +1,1806 @@⋯ 98 unchanged lines ⋯             "acceptedArgs": ["filePath", "lbrPath", "libraryPath", "path"], "receivedArgs": received, "_hint": "Re-call with a Windows-local path, e.g. fusion_open_lbr {\"lbrPath\": \"C:/tmp/rayparts/tja1050.lbr\"}."});     }     let pf = fusion_lbr::preflight_lbr(std::path::Path::new(&file_path));-    let hard_ok = ["file-readable", "size-sane", "xml-parses", "root-is-eagle"].iter().all(|k| pf.checks.iter().find(|c| c.check == *k).map(|c| c.ok).unwrap_or(true));+    let hard_ok = ["file-readable", "size-sane", "xml-parses", "root-is-eagle", "package3d-refs-resolve"].iter().all(|k| pf.checks.iter().find(|c| c.check == *k).map(|c| c.ok).unwrap_or(true));     let file_path = file_path.replace('\\', "/");     if !hard_ok {         let sz = std::fs::metadata(&file_path).ok().map(|m| m.len());⋯ 58 unchanged lines ⋯     if !verification.is_null() { response["data"]["verification"] = verification; }     let (shots, _) = crate::lifecycle::post_open_screenshot(ctx, 2.0);     response["data"]["postOpenScreenshot"] = shots;-    response+    // Fusion reports a refused library only through its Error modal: the command itself succeeds+    // and leaves an empty design. Without this, every caller reads the refusal as a good open.+    crate::small_verbs::apply_failure_dialogs(response, &ctx.caller, &ctx.pids) }  /// Python's `repr(bytes)`.⋯ 1630 unchanged lines ⋯         assert!(seen > 3, "no boards found");     } }+--- a/rust/bridge/src/mint.rs+++ b/rust/bridge/src/mint.rs@@ -1,649 +1,657 @@⋯ 366 unchanged lines ⋯         }     }     let mut opened = Value::Null;-    if let Some(b) = &bound { if args["openWhenDone"].as_bool().unwrap_or(true) { opened = json!(crate::orchestrations::open_lbr(ctx, &json!({"filePath": b}), pass)["success"].as_bool() == Some(true)); } }+    let mut open_error = Value::Null;+    if let Some(b) = &bound { if args["openWhenDone"].as_bool().unwrap_or(true) {+        let op = crate::orchestrations::open_lbr(ctx, &json!({"filePath": b}), pass);+        let ok = op["success"].as_bool() == Some(true);+        opened = json!(ok);+        if !ok { open_error = op["error"].clone(); }+    } }     let n_ok = results.iter().filter(|r| r["success"].as_bool() == Some(true)).count();-    json!({"success": n_ok > 0, "cancelled": cancelled, "boundLbr": bound, "partsBound": n_ok, "partsTotal": total, "parts": results, "screenshots": shots, "opened": opened,+    // Parts minted but a library Fusion will not open is a failed build, whatever each part did.+    json!({"success": n_ok > 0 && open_error.is_null(), "error": open_error, "cancelled": cancelled, "boundLbr": bound, "partsBound": n_ok, "partsTotal": total, "parts": results, "screenshots": shots, "opened": opened,         "_hint": format!("{n_ok}/{total} parts got a RENDERING footprint+chip 3D package; all bindings injected into {} and the library opened once. NEXT: fusion_check_dialogs should be 0 (no broken-ref). For clean device previews + screenshots, save to the cloud and REOPEN from the cloud (fusion_aps_open) - the in-session view often gets an empty 'Untitled' shoved in front. Each device's lower-right 3D preview should show footprint + chip (NOT 'Thumbnail download failed'). The BEFORE (footprint) / AFTER (chip placed) PNGs are in screenshots[] (on the box in %TEMP%/adom-fusion-shots; ab pulls them to you as localFullPath) - pull them with desktop_pull_file (or re-capture via desktop_screenshot_window) for the demo video. AFTER (recommended): call fusion_capture_library_views per showcase part to grab the symbol / footprint / component (pin<->pad mapped) views - those are what make EEs trust the library, and the 3D-only shots miss them. PITFALLS: needs Fusion running. ⚠️ RELAY TIMEOUT: a many-part run takes minutes but the Adom Bridge relay times out the REQUEST at ~60s - you may get 'Request timed out' even though the build KEEPS RUNNING server-side and finishes. Do NOT assume it failed: wait, then verify by reading the boundLbr (count <package3d name=) + the before/after PNGs (%TEMP%/adom-fusion-shots on the box, pulled to you as localFullPath). If a part is missing, just re-run build_library_3d for the missing parts pointing outLbrPath at the SAME file - binding injection is now MERGE-AWARE and idempotent, so it accumulates onto the existing bindings (it no longer wipes the parts that already succeeded). Each part also self-retries once on a transient first-part failure. Full recipe: the fusion-multipart-libraries skill.", bound.as_deref().unwrap_or("None"))}) } ⋯ 271 unchanged lines ⋯         assert!(q.contains(r#"_PN="Bob's \"project\"""#));     } }+

Reviews

Colby Knox approved · 7d ago

Reviewed. Tests pass (fusion-lbr 23, bridge 113). Running apply_failure_dialogs twice is harmless: the second pass finds no dialog and returns the first result. The new check refuses none of 18 real libraries with 3D bindings, except a known-bad one that Fusion does refuse. Not run on Fusion yet: that goes with the next release's live test.

Comments

No comments yet.

Log in to comment.