fix: include KiCad 10 Windows install paths in library auto-discovery
Some checks failed
CI/CD Pipeline / TypeScript Build & Test (20.x, macos-latest) (push) Has been cancelled
CI/CD Pipeline / TypeScript Build & Test (20.x, ubuntu-22.04) (push) Has been cancelled
CI/CD Pipeline / TypeScript Build & Test (20.x, ubuntu-24.04) (push) Has been cancelled
CI/CD Pipeline / TypeScript Build & Test (20.x, windows-latest) (push) Has been cancelled
CI/CD Pipeline / TypeScript Build & Test (22.x, macos-latest) (push) Has been cancelled
CI/CD Pipeline / TypeScript Build & Test (22.x, ubuntu-22.04) (push) Has been cancelled
CI/CD Pipeline / TypeScript Build & Test (22.x, ubuntu-24.04) (push) Has been cancelled
CI/CD Pipeline / TypeScript Build & Test (22.x, windows-latest) (push) Has been cancelled
CI/CD Pipeline / Python Tests (ubuntu-22.04, 3.10) (push) Has been cancelled
CI/CD Pipeline / Python Tests (ubuntu-22.04, 3.11) (push) Has been cancelled
CI/CD Pipeline / Python Tests (ubuntu-22.04, 3.12) (push) Has been cancelled
CI/CD Pipeline / Python Tests (ubuntu-22.04, 3.9) (push) Has been cancelled
CI/CD Pipeline / Python Tests (ubuntu-24.04, 3.10) (push) Has been cancelled
CI/CD Pipeline / Python Tests (ubuntu-24.04, 3.11) (push) Has been cancelled
CI/CD Pipeline / Python Tests (ubuntu-24.04, 3.12) (push) Has been cancelled
CI/CD Pipeline / Python Tests (ubuntu-24.04, 3.9) (push) Has been cancelled
CI/CD Pipeline / Integration Tests (Linux + KiCAD) (10.0) (push) Has been cancelled
CI/CD Pipeline / Integration Tests (Linux + KiCAD) (8.0) (push) Has been cancelled
CI/CD Pipeline / Integration Tests (Linux + KiCAD) (9.0) (push) Has been cancelled
CI/CD Pipeline / Docker Build Test (push) Has been cancelled
CI/CD Pipeline / Code Quality (push) Has been cancelled
CI/CD Pipeline / Documentation Check (push) Has been cancelled

Fixes #245 — _find_kicad_footprint_dir() only listed the 9.0/8.0 Windows
install paths, so a Windows host with only KiCad 10 installed got None and
footprint auto-discovery found nothing. The same gap existed for symbols in
library_symbol.py and dynamic_symbol_loader.py.

- Add C:/Program Files/KiCad/10.0/share/kicad/{footprints,symbols} to the
  candidate lists (10.0 first, matching the existing newest-first order)
- Honor KICAD10_FOOTPRINT_DIR and KICAD10_SYMBOL_DIR env overrides alongside
  the existing 9/8 ones
- Regression tests in tests/test_kicad10_paths.py
This commit is contained in:
mixelpixx
2026-06-12 16:06:11 -04:00
parent 48f2d09fdd
commit 99b1d8a168
4 changed files with 74 additions and 20 deletions

View File

@@ -38,6 +38,7 @@ class DynamicSymbolLoader:
possible_paths = [ possible_paths = [
Path("/usr/share/kicad/symbols"), Path("/usr/share/kicad/symbols"),
Path("/usr/local/share/kicad/symbols"), Path("/usr/local/share/kicad/symbols"),
Path("C:/Program Files/KiCad/10.0/share/kicad/symbols"),
Path("C:/Program Files/KiCad/9.0/share/kicad/symbols"), Path("C:/Program Files/KiCad/9.0/share/kicad/symbols"),
Path("C:/Program Files/KiCad/8.0/share/kicad/symbols"), Path("C:/Program Files/KiCad/8.0/share/kicad/symbols"),
Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"), Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"),
@@ -477,8 +478,10 @@ class DynamicSymbolLoader:
# Iterate over every top-level (property ...) block in the symbol # Iterate over every top-level (property ...) block in the symbol
search_pos = 0 search_pos = 0
while True: while True:
m = re.search(r'\(property\s+"([^"]+)"\s+"[^"]*"\s+\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)', m = re.search(
sym_block[search_pos:]) r'\(property\s+"([^"]+)"\s+"[^"]*"\s+\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)',
sym_block[search_pos:],
)
if not m: if not m:
break break
abs_start = search_pos + m.start() abs_start = search_pos + m.start()
@@ -495,7 +498,7 @@ class DynamicSymbolLoader:
effects_str = self._extract_paren_block(prop_block, eff_pos) effects_str = self._extract_paren_block(prop_block, eff_pos)
# Strip (hide ...) sub-expressions — visibility will be set # Strip (hide ...) sub-expressions — visibility will be set
# separately by the caller # separately by the caller
effects_str = re.sub(r'\s*\(hide\s+[^)]+\)', '', effects_str) effects_str = re.sub(r"\s*\(hide\s+[^)]+\)", "", effects_str)
effects_str = effects_str.strip() effects_str = effects_str.strip()
else: else:
effects_str = "(effects (font (size 1.27 1.27)))" effects_str = "(effects (font (size 1.27 1.27)))"
@@ -556,14 +559,13 @@ class DynamicSymbolLoader:
new_uuid = str(uuid.uuid4()) new_uuid = str(uuid.uuid4())
# --- read property offsets from the already-injected lib_symbols block ----- # --- read property offsets from the already-injected lib_symbols block -----
lib_props = self._extract_lib_property_positions( lib_props = self._extract_lib_property_positions(schematic_path, library_name, symbol_name)
schematic_path, library_name, symbol_name
)
_DEFAULT_EFFECTS = "(effects (font (size 1.27 1.27)))" _DEFAULT_EFFECTS = "(effects (font (size 1.27 1.27)))"
def _prop_at(name: str, fallback_dx: float, fallback_dy: float, def _prop_at(
fallback_angle: float = 0) -> tuple: name: str, fallback_dx: float, fallback_dy: float, fallback_angle: float = 0
) -> tuple:
"""Return (abs_x, abs_y, text_angle, effects_str) for a property.""" """Return (abs_x, abs_y, text_angle, effects_str) for a property."""
if name in lib_props: if name in lib_props:
dx, dy, text_ang, eff = lib_props[name] dx, dy, text_ang, eff = lib_props[name]

View File

@@ -182,12 +182,15 @@ class LibraryManager:
possible_paths = [ possible_paths = [
"/usr/share/kicad/footprints", "/usr/share/kicad/footprints",
"/usr/local/share/kicad/footprints", "/usr/local/share/kicad/footprints",
"C:/Program Files/KiCad/10.0/share/kicad/footprints",
"C:/Program Files/KiCad/9.0/share/kicad/footprints", "C:/Program Files/KiCad/9.0/share/kicad/footprints",
"C:/Program Files/KiCad/8.0/share/kicad/footprints", "C:/Program Files/KiCad/8.0/share/kicad/footprints",
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints", "/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints",
] ]
# Also check environment variable # Also check environment variable
if "KICAD10_FOOTPRINT_DIR" in os.environ:
possible_paths.insert(0, os.environ["KICAD10_FOOTPRINT_DIR"])
if "KICAD9_FOOTPRINT_DIR" in os.environ: if "KICAD9_FOOTPRINT_DIR" in os.environ:
possible_paths.insert(0, os.environ["KICAD9_FOOTPRINT_DIR"]) possible_paths.insert(0, os.environ["KICAD9_FOOTPRINT_DIR"])
if "KICAD8_FOOTPRINT_DIR" in os.environ: if "KICAD8_FOOTPRINT_DIR" in os.environ:

View File

@@ -83,9 +83,7 @@ class SymbolLibraryManager:
logger.debug("Skipping unparseable library: %s", nickname) logger.debug("Skipping unparseable library: %s", nickname)
logger.info("Symbol cache warm-up complete (%d libraries)", len(self.libraries)) logger.info("Symbol cache warm-up complete (%d libraries)", len(self.libraries))
threading.Thread( threading.Thread(target=_warm, name="symbol-cache-warmup", daemon=True).start()
target=_warm, name="symbol-cache-warmup", daemon=True
).start()
def _load_libraries(self) -> None: def _load_libraries(self) -> None:
"""Load libraries from sym-lib-table files""" """Load libraries from sym-lib-table files"""
@@ -232,12 +230,15 @@ class SymbolLibraryManager:
possible_paths = [ possible_paths = [
"/usr/share/kicad/symbols", "/usr/share/kicad/symbols",
"/usr/local/share/kicad/symbols", "/usr/local/share/kicad/symbols",
"C:/Program Files/KiCad/10.0/share/kicad/symbols",
"C:/Program Files/KiCad/9.0/share/kicad/symbols", "C:/Program Files/KiCad/9.0/share/kicad/symbols",
"C:/Program Files/KiCad/8.0/share/kicad/symbols", "C:/Program Files/KiCad/8.0/share/kicad/symbols",
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols", "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols",
] ]
# Check environment variable # Check environment variable
if "KICAD10_SYMBOL_DIR" in os.environ:
possible_paths.insert(0, os.environ["KICAD10_SYMBOL_DIR"])
if "KICAD9_SYMBOL_DIR" in os.environ: if "KICAD9_SYMBOL_DIR" in os.environ:
possible_paths.insert(0, os.environ["KICAD9_SYMBOL_DIR"]) possible_paths.insert(0, os.environ["KICAD9_SYMBOL_DIR"])
if "KICAD8_SYMBOL_DIR" in os.environ: if "KICAD8_SYMBOL_DIR" in os.environ:
@@ -610,8 +611,10 @@ class SymbolLibraryCommands:
# Patch (SER2RJ45): keep cache when project_path matches AND libraries were # Patch (SER2RJ45): keep cache when project_path matches AND libraries were
# actually loaded. Original early-return skipped rebuild even when the cache # actually loaded. Original early-return skipped rebuild even when the cache
# was empty (e.g. first call after create_project, before sym-lib-table existed). # was empty (e.g. first call after create_project, before sym-lib-table existed).
if (self.library_manager.project_path == project_path if (
and len(self.library_manager.libraries) > 0): self.library_manager.project_path == project_path
and len(self.library_manager.libraries) > 0
):
return return
logger.info(f"Rebuilding SymbolLibraryManager for project: {project_path}") logger.info(f"Rebuilding SymbolLibraryManager for project: {project_path}")
self.library_manager = SymbolLibraryManager(project_path=project_path) self.library_manager = SymbolLibraryManager(project_path=project_path)

View File

@@ -0,0 +1,46 @@
"""Regression tests for issue #245: KiCad 10 Windows install paths must be in
the auto-discovery candidate lists (footprints and symbols), with env-var
overrides for KiCad 10.
"""
import sys
from pathlib import Path
from unittest.mock import patch
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
from commands.library import LibraryManager # noqa: E402
from commands.library_symbol import SymbolLibraryManager # noqa: E402
K10_FOOTPRINTS = "C:/Program Files/KiCad/10.0/share/kicad/footprints"
K10_SYMBOLS = "C:/Program Files/KiCad/10.0/share/kicad/symbols"
@pytest.mark.unit
class TestKicad10FootprintDir:
def test_k10_windows_path_is_discovered(self):
lm = LibraryManager.__new__(LibraryManager)
with patch("commands.library.os.path.isdir", side_effect=lambda p: p == K10_FOOTPRINTS):
assert lm._find_kicad_footprint_dir() == K10_FOOTPRINTS
def test_k10_env_override_wins(self, monkeypatch):
lm = LibraryManager.__new__(LibraryManager)
monkeypatch.setenv("KICAD10_FOOTPRINT_DIR", "/custom/k10/footprints")
with patch("commands.library.os.path.isdir", return_value=True):
assert lm._find_kicad_footprint_dir() == "/custom/k10/footprints"
@pytest.mark.unit
class TestKicad10SymbolDir:
def test_k10_windows_path_is_discovered(self):
sm = SymbolLibraryManager.__new__(SymbolLibraryManager)
with patch("commands.library_symbol.os.path.isdir", side_effect=lambda p: p == K10_SYMBOLS):
assert sm._find_kicad_symbol_dir() == K10_SYMBOLS
def test_k10_env_override_wins(self, monkeypatch):
sm = SymbolLibraryManager.__new__(SymbolLibraryManager)
monkeypatch.setenv("KICAD10_SYMBOL_DIR", "/custom/k10/symbols")
with patch("commands.library_symbol.os.path.isdir", return_value=True):
assert sm._find_kicad_symbol_dir() == "/custom/k10/symbols"