Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/kicad_mcp/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class CliCapabilities:
supports_allegro_import: bool = False
supports_pads_import: bool = False
supports_geda_import: bool = False
supports_cli_variant: bool = False


def _candidate_cli_paths() -> list[Path]:
Expand Down Expand Up @@ -195,6 +196,7 @@ def get_cli_capabilities(cli_path: Path) -> CliCapabilities:
supports_allegro_import="allegro" in blob,
supports_pads_import="pads" in blob,
supports_geda_import="geda" in blob,
supports_cli_variant="--variant" in blob,
)
_CLI_CAPABILITIES_CACHE[cache_key] = capabilities
return capabilities
Expand Down
26 changes: 23 additions & 3 deletions src/kicad_mcp/tools/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,31 @@ def _with_low_level_export_notice(message: str) -> str:

def _active_variant_args(variant_name: str | None = None) -> list[str]:
try:
return variant_apply_to_kicad_cli_args(variant_name)
args = variant_apply_to_kicad_cli_args(variant_name)
except ValueError:
if variant_name:
raise
return []
if not args:
return args
# ``--variant`` was added to ``kicad-cli`` in KiCad 10. Earlier CLIs (9.x
# and below) reject it as ``Unknown argument`` and abort the export. The
# ``default`` variant is a synthetic no-op baseline that adds no overrides,
# so suppress it unconditionally; for explicit non-default variants, gate
# on the local CLI's advertised capability.
if args == ["--variant", "default"]:
return []
try:
caps = get_cli_capabilities(get_config().kicad_cli)
except Exception:
return args
if not caps.supports_cli_variant:
raise ValueError(
f"The detected kicad-cli does not support --variant. "
f"Cannot apply variant '{args[1]}'. Upgrade to KiCad 10+ "
f"or run variant_set_active('default') to clear the override."
)
return args


async def _report_progress(
Expand Down Expand Up @@ -389,7 +409,7 @@ def _export_sch_pdf() -> str:
out_dir = _ensure_output_dir()
out_file = out_dir / "schematic.pdf"
variant_args = _active_variant_args()
code, _, stderr = _run_cli_variants(
code, stdout, stderr = _run_cli_variants(
[
["sch", "export", "pdf", *variant_args, "--output", str(out_file), str(sch_file)],
[
Expand All @@ -405,7 +425,7 @@ def _export_sch_pdf() -> str:
]
)
if code != 0:
return f"Schematic PDF export failed: {stderr or 'unknown error'}"
return f"Schematic PDF export failed: {stderr or stdout or 'unknown error'}"
return f"Schematic PDF exported to {out_file}"

@headless_compatible
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_export_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ class Result:
supports_render=True,
supports_3d_pdf=True,
supports_spice_netlist=True,
supports_cli_variant=True,
),
)

Expand Down
1 change: 1 addition & 0 deletions tests/unit/test_release_hardening.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ def fake_run_cli_variants(variants: list[list[str]]) -> tuple[int, str, str]:
drill_command="drill",
position_command="pos",
supports_ipc2581=True,
supports_cli_variant=True,
),
)

Expand Down
69 changes: 69 additions & 0 deletions tests/unit/test_variant_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,75 @@ def test_variant_apply_to_kicad_cli_args_returns_empty_when_project_has_no_activ
assert variant_apply_to_kicad_cli_args() == []


class _FakeConfig:
kicad_cli = None


def _patch_variant_args_dependencies(
monkeypatch: pytest.MonkeyPatch,
variant_args: list[str],
*,
supports_cli_variant: bool,
) -> None:
from kicad_mcp.discovery import CliCapabilities

monkeypatch.setattr(
"kicad_mcp.tools.export.variant_apply_to_kicad_cli_args",
lambda variant_name=None: list(variant_args),
)
monkeypatch.setattr("kicad_mcp.tools.export.get_config", lambda: _FakeConfig())
monkeypatch.setattr(
"kicad_mcp.tools.export.get_cli_capabilities",
lambda _cli: CliCapabilities(
version="KiCad 9.0.7", supports_cli_variant=supports_cli_variant
),
)


def test_active_variant_args_raises_when_cli_lacks_variant_support(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Explicit non-default variants must fail loudly on pre-KiCad-10 CLIs.

Silently dropping the flag would cause a requested variant (e.g. ``lite``)
to manufacture as the default board, which is a correctness footgun.
"""
from kicad_mcp.tools.export import _active_variant_args

_patch_variant_args_dependencies(
monkeypatch, ["--variant", "lite"], supports_cli_variant=False
)

with pytest.raises(ValueError, match="does not support --variant"):
_active_variant_args()


def test_active_variant_args_suppresses_synthetic_default_on_old_cli(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The synthetic ``default`` variant adds no overrides, so dropping it is safe."""
from kicad_mcp.tools.export import _active_variant_args

_patch_variant_args_dependencies(
monkeypatch, ["--variant", "default"], supports_cli_variant=False
)

assert _active_variant_args() == []


def test_active_variant_args_forwards_variant_on_new_cli(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""KiCad-10 CLIs accept ``--variant``, so the flag must pass through."""
from kicad_mcp.tools.export import _active_variant_args

_patch_variant_args_dependencies(
monkeypatch, ["--variant", "lite"], supports_cli_variant=True
)

assert _active_variant_args() == ["--variant", "lite"]


def test_render_variant_components_merges_unknown_override_reference(
sample_project,
monkeypatch: pytest.MonkeyPatch,
Expand Down