#!/usr/bin/env python3 """Configure KiCad user paths and library table entries for this library.""" from __future__ import annotations import argparse import json import os import re import shutil import sys from datetime import datetime from pathlib import Path ENV_VARS = ("KICAD_LIB_DIR_USER", "KICAD_PDF_DIR_USER", "KICAD_3D_PARTY_USER") def repo_root() -> Path: return Path(__file__).resolve().parents[1] def detect_kicad_config(version: str | None) -> Path: base = Path.home() / ".config" / "kicad" if version: path = base / version / "kicad_common.json" if not path.exists(): raise SystemExit(f"KiCad config not found: {path}") return path candidates = sorted(base.glob("*/kicad_common.json")) if not candidates: raise SystemExit(f"KiCad config not found under {base}") def version_key(path: Path) -> tuple[int, ...]: parts = [] for item in path.parent.name.split("."): try: parts.append(int(item)) except ValueError: parts.append(0) return tuple(parts) return max(candidates, key=version_key) def read_existing_vars(config_path: Path) -> dict[str, str]: data = json.loads(config_path.read_text(encoding="utf-8")) vars_obj = data.get("environment", {}).get("vars") return vars_obj if isinstance(vars_obj, dict) else {} def ask_path(name: str, prompt: str, default: str | None, required: bool = True) -> str: env_value = os.environ.get(name) if env_value: return str(Path(env_value).expanduser()) suffix = f" [{default}]" if default else "" while True: value = input(f"{prompt}{suffix}: ").strip() if not value and default: value = default if value or not required: return str(Path(value).expanduser()) if value else "" print("Path is required.") def backup(path: Path) -> Path: stamp = datetime.now().strftime("%Y%m%d-%H%M%S") target = path.with_name(f"{path.name}.bak-{stamp}") shutil.copy2(path, target) return target def update_common_config(config_path: Path, values: dict[str, str]) -> None: data = json.loads(config_path.read_text(encoding="utf-8")) environment = data.setdefault("environment", {}) vars_obj = environment.get("vars") if not isinstance(vars_obj, dict): vars_obj = {} environment["vars"] = vars_obj vars_obj.update(values) config_path.write_text( json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) def ensure_table(path: Path, header: str) -> None: if not path.exists(): path.write_text(f"({header}\n\t(version 7)\n)\n", encoding="utf-8") def upsert_library_entry(table_path: Path, header: str, entry: str) -> None: ensure_table(table_path, header) text = table_path.read_text(encoding="utf-8") name_match = re.search(r'\(name "([^"]+)"\)', entry) if not name_match: raise ValueError(f"Library entry has no name: {entry}") name = re.escape(name_match.group(1)) text = re.sub( rf'\n[^\n]*\(lib\s+\(name "{name}"\)[^\n]*', "", text, ) insert_at = text.rfind("\n)") if insert_at == -1: raise SystemExit(f"Unexpected KiCad table format: {table_path}") text = text[:insert_at] + "\n\t" + entry + text[insert_at:] table_path.write_text(text, encoding="utf-8") def configure_library_tables(config_dir: Path) -> None: sym_table = config_dir / "sym-lib-table" fp_table = config_dir / "fp-lib-table" upsert_library_entry( sym_table, "sym_lib_table", '(lib (name "eda-core") (type "Database") (uri "${KICAD_LIB_DIR_USER}/eda-core.kicad_dbl") (options "") (descr ""))', ) upsert_library_entry( sym_table, "sym_lib_table", '(lib (name "sym-lib-table") (type "Table") (uri "${KICAD_LIB_DIR_USER}/sym-lib-table") (options "") (descr ""))', ) upsert_library_entry( fp_table, "fp_lib_table", '(lib (name "fp-lib-table") (type "Table") (uri "${KICAD_LIB_DIR_USER}/fp-lib-table") (options "") (descr ""))', ) def main() -> int: parser = argparse.ArgumentParser( description="Configure KiCad paths and library tables for KiCad User Library." ) parser.add_argument("--kicad-version", help="KiCad config version, for example 10.0") parser.add_argument("--lib-dir", default=str(repo_root()), help="Path to this library repo") parser.add_argument("--pdf-dir", help="Path to PDF datasheet directory") parser.add_argument("--3d-dir", dest="models_dir", help="Path to user 3D models directory") parser.add_argument("--non-interactive", action="store_true", help="Do not ask questions") args = parser.parse_args() config_path = detect_kicad_config(args.kicad_version) existing = read_existing_vars(config_path) lib_dir = str(Path(args.lib_dir).expanduser().resolve()) pdf_dir = args.pdf_dir or existing.get("KICAD_PDF_DIR_USER") models_dir = args.models_dir or existing.get("KICAD_3D_PARTY_USER") if not args.non_interactive: lib_dir = ask_path("KICAD_LIB_DIR_USER", "KiCad library path", lib_dir) pdf_dir = ask_path("KICAD_PDF_DIR_USER", "PDF directory path", pdf_dir) if args.models_dir or existing.get("KICAD_3D_PARTY_USER"): models_dir = ask_path("KICAD_3D_PARTY_USER", "3D models directory path", models_dir) values = { "KICAD_LIB_DIR_USER": lib_dir, "KICAD_PDF_DIR_USER": pdf_dir or "", } if models_dir: values["KICAD_3D_PARTY_USER"] = models_dir if not values["KICAD_PDF_DIR_USER"]: raise SystemExit("Missing required value: KICAD_PDF_DIR_USER") for key, value in values.items(): if key != "KICAD_LIB_DIR_USER" and not Path(value).expanduser().exists(): print(f"warning: {key} path does not exist yet: {value}", file=sys.stderr) backups = [backup(config_path)] config_dir = config_path.parent for table_name in ("sym-lib-table", "fp-lib-table"): table_path = config_dir / table_name if table_path.exists(): backups.append(backup(table_path)) update_common_config(config_path, values) configure_library_tables(config_dir) print(f"Configured KiCad {config_dir.name}:") for key in ENV_VARS: if key in values: print(f" {key} = {values[key]}") print("Library table entries updated:") print(" sym-lib-table: eda-core, sym-lib-table") print(" fp-lib-table: fp-lib-table") print("Backups:") for item in backups: print(f" {item}") print("Restart KiCad if it was running.") return 0 if __name__ == "__main__": raise SystemExit(main())