335 lines
9.3 KiB
Python
335 lines
9.3 KiB
Python
#!/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()) |