app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.
master
Colby Knox
2.17.0: assemble verb (multi-deviceset .lbr assembler for Cloud Libraries)
6926bb9
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
//! Multi-deviceset .lbr assembler (Cloud Libraries phase 2).
//!
//! `adom-lbr assemble -o lib.lbr <part.lbr...>` merges the <packages>,
//! <symbols>, and <devicesets> of many single-part libraries into one
//! document: packages and symbols dedupe by CONTENT (identical bytes modulo
//! the name attribute collapse to one entry, references retargeted), name
//! collisions across different content rename with a numeric suffix, and any
//! input that fails lint is refused up front. The assembled document must
//! itself pass lint or the command fails.
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process;
use crate::{lint_lbr, LintResult};
/// One named entry (a <package>, <symbol>, or <deviceset> block).
struct Entry {
name: String,
xml: String,
}
/// Inner text of `<tag>...</tag>` (first opening, last closing).
fn section_inner<'a>(content: &'a str, tag: &str) -> Option<&'a str> {
let open = format!("<{}>", tag);
let close = format!("</{}>", tag);
let start = content.find(&open)? + open.len();
let end = content.rfind(&close)?;
if end < start {
return None;
}
Some(&content[start..end])
}
/// Split a section into its top-level named entries. The EAGLE library
/// sections never nest an entry inside another entry of the same tag, so
/// scanning open/close pairs is sound.
fn split_entries(section: &str, tag: &str) -> Vec<Entry> {
let mut out = Vec::new();
let open_a = format!("<{} ", tag);
let open_b = format!("<{}>", tag);
let close = format!("</{}>", tag);
let mut pos = 0;
while pos < section.len() {
let a = section[pos..].find(&open_a).map(|i| i + pos);
let b = section[pos..].find(&open_b).map(|i| i + pos);
let start = match (a, b) {
(Some(x), Some(y)) => x.min(y),
(Some(x), None) => x,
(None, Some(y)) => y,
(None, None) => break,
};
let end = match section[start..].find(&close) {
Some(i) => start + i + close.len(),
None => break,
};
let xml = section[start..end].to_string();
let name = attr_value(&xml, "name").unwrap_or_default();
out.push(Entry { name, xml });
pos = end;
}
out
}
/// First `attr="value"` in the first tag of a block.
fn attr_value(xml: &str, attr: &str) -> Option<String> {
let tag_end = xml.find('>')?;
let head = &xml[..tag_end];
let needle = format!("{}=\"", attr);
let i = head.find(&needle)? + needle.len();
let j = head[i..].find('"')? + i;
Some(head[i..j].to_string())
}
/// Content key for dedup: the block with its name attribute and all
/// whitespace runs normalized, so formatting differences do not defeat it.
fn content_key(xml: &str, name: &str) -> String {
let named = format!("name=\"{}\"", name);
let stripped = xml.replacen(&named, "name=\"\"", 1);
stripped.split_whitespace().collect::<Vec<_>>().join(" ")
}
/// Replace `attr="old"` with `attr="new"` everywhere in a block.
fn rewrite_attr(xml: &str, attr: &str, old: &str, new: &str) -> String {
xml.replace(
&format!("{}=\"{}\"", attr, old),
&format!("{}=\"{}\"", attr, new),
)
}
/// Self-closing <layer .../> lines from a document, keyed by layer number.
fn collect_layers(content: &str, seen: &mut HashMap<String, ()>, out: &mut Vec<String>) {
let mut pos = 0;
while let Some(i) = content[pos..].find("<layer ") {
let start = pos + i;
let end = match content[start..].find("/>") {
Some(j) => start + j + 2,
None => break,
};
let xml = &content[start..end];
if let Some(num) = attr_value(xml, "number") {
if seen.insert(num, ()).is_none() {
out.push(xml.to_string());
}
}
pos = end;
}
}
struct Pool {
entries: Vec<Entry>,
by_key: HashMap<String, String>, // content key -> canonical name
names: HashMap<String, ()>, // taken names
}
impl Pool {
fn new() -> Self {
Pool { entries: Vec::new(), by_key: HashMap::new(), names: HashMap::new() }
}
/// Add an entry, deduping by content and resolving name collisions.
/// Returns the name the entry ended up under, and whether it was deduped.
fn add(&mut self, e: Entry, dedup_by_content: bool) -> (String, bool) {
let key = content_key(&e.xml, &e.name);
if dedup_by_content {
if let Some(existing) = self.by_key.get(&key) {
return (existing.clone(), true);
}
}
let mut name = e.name.clone();
let mut n = 2;
while self.names.contains_key(&name) {
name = format!("{}_{}", e.name, n);
n += 1;
}
let xml = if name != e.name {
e.xml.replacen(
&format!("name=\"{}\"", e.name),
&format!("name=\"{}\"", name),
1,
)
} else {
e.xml
};
self.names.insert(name.clone(), ());
self.by_key.insert(key, name.clone());
self.entries.push(Entry { name: name.clone(), xml });
(name, false)
}
}
pub fn run(out: &Path, name: Option<&str>, inputs: &[PathBuf]) {
if inputs.is_empty() {
eprintln!("ERROR: No input .lbr files given");
eprintln!("Hint: adom-lbr assemble -o lib.lbr part1.lbr part2.lbr ...");
process::exit(1);
}
let mut packages = Pool::new();
let mut symbols = Pool::new();
let mut devicesets = Pool::new();
let mut layer_seen: HashMap<String, ()> = HashMap::new();
let mut layers: Vec<String> = Vec::new();
let mut renamed: Vec<serde_json::Value> = Vec::new();
let mut packages_deduped = 0usize;
let mut symbols_deduped = 0usize;
for input in inputs {
let content = fs::read_to_string(input).unwrap_or_else(|e| {
eprintln!("ERROR: Cannot read {}: {}", input.display(), e);
process::exit(1);
});
// Refuse lint-failing inputs: the assembled library only ships clean.
let lint = lint_lbr(&content);
if !lint.passed {
eprintln!(
"ERROR: {} fails lint ({}); refusing to assemble it",
input.display(),
lint.errors
.first()
.map(|e| e.code.clone())
.unwrap_or_else(|| "error".into())
);
for err in &lint.errors {
eprintln!(" [{}] {}", err.code, err.message);
}
process::exit(2);
}
collect_layers(&content, &mut layer_seen, &mut layers);
let pkg_entries = section_inner(&content, "packages")
.map(|s| split_entries(s, "package"))
.unwrap_or_default();
let sym_entries = section_inner(&content, "symbols")
.map(|s| split_entries(s, "symbol"))
.unwrap_or_default();
let ds_entries = section_inner(&content, "devicesets")
.map(|s| split_entries(s, "deviceset"))
.unwrap_or_default();
// Per-input rename maps, applied to this input's devicesets only
// (each part file is self-contained).
let mut pkg_map: HashMap<String, String> = HashMap::new();
let mut sym_map: HashMap<String, String> = HashMap::new();
for e in pkg_entries {
let old = e.name.clone();
let (new_name, deduped) = packages.add(e, true);
if deduped {
packages_deduped += 1;
}
if new_name != old {
if !deduped {
renamed.push(serde_json::json!({"kind": "package", "from": old, "to": new_name, "file": input.display().to_string()}));
}
pkg_map.insert(old, new_name);
}
}
for e in sym_entries {
let old = e.name.clone();
let (new_name, deduped) = symbols.add(e, true);
if deduped {
symbols_deduped += 1;
}
if new_name != old {
if !deduped {
renamed.push(serde_json::json!({"kind": "symbol", "from": old, "to": new_name, "file": input.display().to_string()}));
}
sym_map.insert(old, new_name);
}
}
for e in ds_entries {
let mut xml = e.xml;
for (old, new) in &sym_map {
xml = rewrite_attr(&xml, "symbol", old, new);
}
for (old, new) in &pkg_map {
xml = rewrite_attr(&xml, "package", old, new);
}
let old = e.name.clone();
let (new_name, _) = devicesets.add(Entry { name: e.name, xml }, false);
if new_name != old {
renamed.push(serde_json::json!({"kind": "deviceset", "from": old, "to": new_name, "file": input.display().to_string()}));
}
}
}
let description = name.unwrap_or("Adom Cloud Library");
let mut xml = String::with_capacity(64 * 1024);
xml.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
xml.push_str("<!DOCTYPE eagle SYSTEM \"eagle.dtd\">\n");
xml.push_str("<eagle version=\"9.6.2\">\n<drawing>\n");
xml.push_str("<settings><setting alwaysvectorfont=\"no\"/></settings>\n");
xml.push_str("<grid distance=\"0.1\" unitdist=\"inch\" unit=\"inch\" style=\"lines\" multiple=\"1\" display=\"no\" altdistance=\"0.01\" altunitdist=\"inch\" altunit=\"inch\"/>\n");
xml.push_str("<layers>\n");
for l in &layers {
xml.push_str(l);
xml.push('\n');
}
xml.push_str("</layers>\n");
xml.push_str("<library>\n");
xml.push_str(&format!("<description>{}</description>\n", crate::esc_xml(description)));
xml.push_str("<packages>\n");
for e in &packages.entries {
xml.push_str(&e.xml);
xml.push('\n');
}
xml.push_str("</packages>\n<symbols>\n");
for e in &symbols.entries {
xml.push_str(&e.xml);
xml.push('\n');
}
xml.push_str("</symbols>\n<devicesets>\n");
for e in &devicesets.entries {
xml.push_str(&e.xml);
xml.push('\n');
}
xml.push_str("</devicesets>\n</library>\n</drawing>\n</eagle>\n");
// The assembled document is held to the same gate as its inputs.
let lint: LintResult = lint_lbr(&xml);
if !lint.passed {
eprintln!("ERROR: assembled library fails lint");
for err in &lint.errors {
eprintln!(" [{}] {}", err.code, err.message);
}
process::exit(2);
}
if let Err(e) = fs::write(out, &xml) {
eprintln!("ERROR: Cannot write {}: {}", out.display(), e);
process::exit(1);
}
let summary = serde_json::json!({
"passed": true,
"out": out.display().to_string(),
"parts": inputs.len(),
"packages": packages.entries.len(),
"symbols": symbols.entries.len(),
"devicesets": devicesets.entries.len(),
"packages_deduped": packages_deduped,
"symbols_deduped": symbols_deduped,
"renamed": renamed,
"lint_warnings": lint.warnings.len(),
});
println!("{}", serde_json::to_string_pretty(&summary).unwrap());
}