#!/usr/bin/env python3 """ Adds all *.kicad_sym libraries under UM_KICAD/lib/symbols to the KiCad v9 global sym-lib-table. Also adds all *.pretty footprint libraries under UM_KICAD/lib/footprints to the KiCad v9 global fp-lib-table. Safe behavior: - Does NOT remove existing entries - Skips libraries already present (by URI) - Creates sym-lib-table.bak and fp-lib-table.bak backups """ from __future__ import annotations import os import sys import re from pathlib import Path from typing import Dict, List # ---------------------------- # KiCad v9 config location # ---------------------------- def kicad9_config_dir() -> Path: home = Path.home() if sys.platform.startswith("win"): appdata = os.environ.get("APPDATA") if not appdata: raise RuntimeError("APPDATA not set") return Path(appdata) / "kicad" / "9.0" if sys.platform == "darwin": return home / "Library" / "Preferences" / "kicad" / "9.0" xdg = os.environ.get("XDG_CONFIG_HOME") if xdg: return Path(xdg) / "kicad" / "9.0" return home / ".config" / "kicad" / "9.0" def global_sym_lib_table_path() -> Path: return kicad9_config_dir() / "sym-lib-table" def global_fp_lib_table_path() -> Path: return kicad9_config_dir() / "fp-lib-table" # ---------------------------- # Basic sym-lib-table parsing # ---------------------------- LIB_BLOCK_RE = re.compile(r"\(lib\s+(?:.|\n)*?\)\s*\)", re.MULTILINE) NAME_RE = re.compile(r"\(name\s+([^)]+)\)") URI_RE = re.compile(r"\(uri\s+([^)]+)\)") def strip_atom(atom: str) -> str: atom = atom.strip() if atom.startswith('"') and atom.endswith('"'): return atom[1:-1] return atom def parse_existing_libs(text: str) -> Dict[str, str]: libs = {} for m in LIB_BLOCK_RE.finditer(text): block = m.group(0) name_m = NAME_RE.search(block) uri_m = URI_RE.search(block) if not name_m or not uri_m: continue name = strip_atom(name_m.group(1)) uri = strip_atom(uri_m.group(1)) libs[name] = uri return libs # ---------------------------- # Helpers # ---------------------------- def make_unique_name(name: str, used: set[str]) -> str: if name not in used: return name i = 2 while f"{name}_{i}" in used: i += 1 return f"{name}_{i}" def create_lib_block(name: str, uri: str, descr: str) -> str: return ( "(lib\n" f" (name \"{name}\")\n" " (type \"KiCad\")\n" f" (uri \"{uri}\")\n" " (options \"\")\n" f" (descr \"{descr}\")\n" ")" ) def create_fp_lib_block(name: str, uri: str, descr: str) -> str: return ( "(lib\n" f" (name \"{name}\")\n" " (type \"KiCad\")\n" f" (uri \"{uri}\")\n" " (options \"\")\n" f" (descr \"{descr}\")\n" ")" ) # ---------------------------- # Main logic # ---------------------------- def sync_symbol_libraries(um_root: Path) -> int: """Synchronize symbol libraries""" symbols_dir = um_root / "lib" / "symbols" if not symbols_dir.exists(): print(f"ERROR: {symbols_dir} does not exist") return 1 sym_table_path = global_sym_lib_table_path() sym_table_path.parent.mkdir(parents=True, exist_ok=True) existing_text = "" if sym_table_path.exists(): existing_text = sym_table_path.read_text(encoding="utf-8") existing_libs = parse_existing_libs(existing_text) # Backup if sym_table_path.exists(): backup_path = sym_table_path.with_suffix(".bak") backup_path.write_text(existing_text, encoding="utf-8") print(f"Backup written: {backup_path}") # Filter out all existing UM_KICAD entries existing_blocks = [] removed = 0 for m in LIB_BLOCK_RE.finditer(existing_text): block = m.group(0).strip() uri_m = URI_RE.search(block) if uri_m: uri = strip_atom(uri_m.group(1)) if "${UM_KICAD}" in uri: removed += 1 continue existing_blocks.append(block) # Scan UM_KICAD/lib/symbols sym_files = sorted(symbols_dir.glob("*.kicad_sym")) if not sym_files: print("No .kicad_sym files found.") # Still rebuild table without UM_KICAD entries output = ["(sym_lib_table"] for block in existing_blocks: output.append(" " + block.replace("\n", "\n ")) output.append(")\n") sym_table_path.write_text("\n".join(output), encoding="utf-8") print(f"Removed {removed} UM_KICAD entries") return 0 # Build new UM_KICAD blocks new_blocks: List[str] = [] used_names = set() # Collect names from non-UM_KICAD entries to avoid conflicts for name, uri in existing_libs.items(): if "${UM_KICAD}" not in uri: used_names.add(name) for sym_file in sym_files: rel_path = sym_file.relative_to(um_root).as_posix() uri = "${UM_KICAD}/" + rel_path base_name = sym_file.stem name = make_unique_name(base_name, used_names) block = create_lib_block( name=name, uri=uri, descr=f"Auto-added from {rel_path}" ) new_blocks.append(block) used_names.add(name) # Rebuild table all_blocks = existing_blocks + new_blocks output = ["(sym_lib_table"] for block in all_blocks: output.append(" " + block.replace("\n", "\n ")) output.append(")\n") sym_table_path.write_text("\n".join(output), encoding="utf-8") print(f"Updated {sym_table_path}") print(f"Found: {len(sym_files)} libraries") print(f"Removed: {removed} old UM_KICAD entries") print(f"Added: {len(new_blocks)} new UM_KICAD entries") return 0 def sync_footprint_libraries(um_root: Path) -> int: """Synchronize footprint libraries""" footprints_dir = um_root / "lib" / "footprints" if not footprints_dir.exists(): print(f"Warning: {footprints_dir} does not exist, skipping footprint sync") return 0 fp_table_path = global_fp_lib_table_path() fp_table_path.parent.mkdir(parents=True, exist_ok=True) existing_text = "" if fp_table_path.exists(): existing_text = fp_table_path.read_text(encoding="utf-8") existing_libs = parse_existing_libs(existing_text) # Backup if fp_table_path.exists(): backup_path = fp_table_path.with_suffix(".bak") backup_path.write_text(existing_text, encoding="utf-8") print(f"Backup written: {backup_path}") # Filter out all existing UM_KICAD entries existing_blocks = [] removed = 0 for m in LIB_BLOCK_RE.finditer(existing_text): block = m.group(0).strip() uri_m = URI_RE.search(block) if uri_m: uri = strip_atom(uri_m.group(1)) if "${UM_KICAD}" in uri: removed += 1 continue existing_blocks.append(block) # Scan UM_KICAD/lib/footprints for *.pretty directories pretty_dirs = sorted([d for d in footprints_dir.iterdir() if d.is_dir() and d.name.endswith('.pretty')]) if not pretty_dirs: print("No .pretty footprint libraries found.") # Still rebuild table without UM_KICAD entries output = ["(fp_lib_table"] for block in existing_blocks: output.append(" " + block.replace("\n", "\n ")) output.append(")\n") fp_table_path.write_text("\n".join(output), encoding="utf-8") print(f"Removed {removed} UM_KICAD footprint entries") return 0 # Build new UM_KICAD blocks new_blocks: List[str] = [] used_names = set() # Collect names from non-UM_KICAD entries to avoid conflicts for name, uri in existing_libs.items(): if "${UM_KICAD}" not in uri: used_names.add(name) for pretty_dir in pretty_dirs: rel_path = pretty_dir.relative_to(um_root).as_posix() uri = "${UM_KICAD}/" + rel_path base_name = pretty_dir.stem # Removes .pretty extension name = make_unique_name(base_name, used_names) block = create_fp_lib_block( name=name, uri=uri, descr=f"Auto-added from {rel_path}" ) new_blocks.append(block) used_names.add(name) # Rebuild table all_blocks = existing_blocks + new_blocks output = ["(fp_lib_table"] for block in all_blocks: output.append(" " + block.replace("\n", "\n ")) output.append(")\n") fp_table_path.write_text("\n".join(output), encoding="utf-8") print(f"Updated {fp_table_path}") print(f"Found: {len(pretty_dirs)} footprint libraries") print(f"Removed: {removed} old UM_KICAD entries") print(f"Added: {len(new_blocks)} new UM_KICAD entries") return 0 def main() -> int: um_root_env = os.environ.get("UM_KICAD") if not um_root_env: print("ERROR: UM_KICAD not set") return 1 um_root = Path(um_root_env).resolve() print("=" * 60) print("Syncing Symbol Libraries...") print("=" * 60) result = sync_symbol_libraries(um_root) if result != 0: return result print("\n" + "=" * 60) print("Syncing Footprint Libraries...") print("=" * 60) result = sync_footprint_libraries(um_root) if result != 0: return result print("\n" + "=" * 60) print("Restart KiCad to refresh libraries.") print("=" * 60) return 0 if __name__ == "__main__": raise SystemExit(main())