initial commit

This commit is contained in:
brentperteet
2026-02-22 08:16:48 -06:00
commit 3f0aff923d
15 changed files with 4364 additions and 0 deletions

335
add_libraries.py Normal file
View File

@@ -0,0 +1,335 @@
#!/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())