Lib/scripts/setup_kicad_user_library.py
2026-06-21 20:50:05 +03:00

204 lines
6.7 KiB
Python
Executable file

#!/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())