fix(board-view): KiCad 10 kicad-cli svg export compatibility (#209)
KiCad 10 changed `kicad-cli pcb export svg` in three ways that broke get_board_2d_view (reported with a verified fix in #209): - `--output` is now a FILE path, not a directory (a dir fails with "Failed to create file '<dir>'"). - `--mode-single` is required to merge layers into one SVG (the default multi-file behavior is deprecated). - exit code 2 is returned for the deprecation warning even on success. Pass a file path to `--output` and add `--mode-single` (both also valid on KiCad 8/9). Stop gating on the exit code — judge success by whether an SVG was actually produced — so the exit-2 deprecation warning is no longer a false failure. Add regression tests for the exit-2-is-success case and the command shape (file output + --mode-single). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -182,7 +182,23 @@ class BoardViewCommands:
|
|||||||
}
|
}
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
cmd = [kicad_cli, "pcb", "export", "svg", "--output", tmpdir, "--black-and-white"]
|
# KiCad 10 changed `pcb export svg`:
|
||||||
|
# - `--output` is now a FILE path, not a directory (a directory
|
||||||
|
# fails with "Failed to create file '<dir>'").
|
||||||
|
# - `--mode-single` is required to merge layers into one SVG
|
||||||
|
# (the default multi-file behavior is deprecated).
|
||||||
|
# These flags also work on KiCad 8/9, so we always pass them.
|
||||||
|
output_svg = os.path.join(tmpdir, "board.svg")
|
||||||
|
cmd = [
|
||||||
|
kicad_cli,
|
||||||
|
"pcb",
|
||||||
|
"export",
|
||||||
|
"svg",
|
||||||
|
"--output",
|
||||||
|
output_svg,
|
||||||
|
"--mode-single",
|
||||||
|
"--black-and-white",
|
||||||
|
]
|
||||||
if layers:
|
if layers:
|
||||||
cmd += ["--layers", ",".join(layers)]
|
cmd += ["--layers", ",".join(layers)]
|
||||||
cmd.append(pcb_path)
|
cmd.append(pcb_path)
|
||||||
@@ -196,21 +212,22 @@ class BoardViewCommands:
|
|||||||
"errorDetails": " ".join(cmd),
|
"errorDetails": " ".join(cmd),
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.returncode != 0:
|
# Do NOT gate on the exit code: KiCad 9+/10 returns exit code 2
|
||||||
|
# for a deprecation warning even when the SVG is written
|
||||||
|
# correctly. Use "was an SVG actually produced?" as the success
|
||||||
|
# signal instead — robust across versions and modes.
|
||||||
|
svg_files = (
|
||||||
|
[output_svg]
|
||||||
|
if os.path.exists(output_svg)
|
||||||
|
else glob.glob(os.path.join(tmpdir, "*.svg"))
|
||||||
|
)
|
||||||
|
if not svg_files:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "kicad-cli SVG export failed",
|
"message": "kicad-cli SVG export failed",
|
||||||
"errorDetails": result.stderr.strip() or result.stdout.strip(),
|
"errorDetails": result.stderr.strip() or result.stdout.strip(),
|
||||||
}
|
}
|
||||||
|
|
||||||
svg_files = glob.glob(os.path.join(tmpdir, "*.svg"))
|
|
||||||
if not svg_files:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": "kicad-cli produced no SVG output",
|
|
||||||
"errorDetails": result.stdout.strip(),
|
|
||||||
}
|
|
||||||
|
|
||||||
svg_path = svg_files[0]
|
svg_path = svg_files[0]
|
||||||
|
|
||||||
# --- Render to bytes (shared for both response modes) ---
|
# --- Render to bytes (shared for both response modes) ---
|
||||||
|
|||||||
@@ -154,3 +154,63 @@ def test_file_mode_png_writes_file_and_returns_path(tmp_path):
|
|||||||
assert expected.exists()
|
assert expected.exists()
|
||||||
assert expected.read_bytes() == _FAKE_PNG
|
assert expected.read_bytes() == _FAKE_PNG
|
||||||
assert "imageData" not in result
|
assert "imageData" not in result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# KiCad 10 kicad-cli compatibility (issue #209)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_kicad10_exit_code_2_with_svg_produced_is_success(tmp_path):
|
||||||
|
"""KiCad 9+/10 returns exit code 2 on a deprecation warning even when the SVG
|
||||||
|
is written. Success must be judged by the produced SVG, not the exit code."""
|
||||||
|
cmd, root, board_path = _make_view_cmd(tmp_path)
|
||||||
|
svg_path = root / "MyBoard.svg"
|
||||||
|
svg_path.write_bytes(_FAKE_SVG)
|
||||||
|
|
||||||
|
fake_result = MagicMock()
|
||||||
|
fake_result.returncode = 2 # deprecation warning exit code
|
||||||
|
fake_result.stderr = "This command has deprecated behavior as of KiCad 9.0..."
|
||||||
|
fake_result.stdout = "Done."
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("shutil.which", return_value="/usr/bin/kicad-cli"),
|
||||||
|
patch("subprocess.run", return_value=fake_result),
|
||||||
|
patch("glob.glob", return_value=[str(svg_path)]),
|
||||||
|
):
|
||||||
|
result = cmd.get_board_2d_view(
|
||||||
|
{"pcbPath": str(board_path), "format": "svg", "responseMode": "inline"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert base64.b64decode(result["imageData"]) == _FAKE_SVG
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_kicad10_export_cmd_uses_file_output_and_mode_single(tmp_path):
|
||||||
|
"""The export command must pass a FILE path to --output (not a directory) and
|
||||||
|
include --mode-single, as required by KiCad 10."""
|
||||||
|
cmd, root, board_path = _make_view_cmd(tmp_path)
|
||||||
|
svg_path = root / "MyBoard.svg"
|
||||||
|
svg_path.write_bytes(_FAKE_SVG)
|
||||||
|
|
||||||
|
fake_result = MagicMock()
|
||||||
|
fake_result.returncode = 0
|
||||||
|
fake_result.stderr = ""
|
||||||
|
fake_result.stdout = ""
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("shutil.which", return_value="/usr/bin/kicad-cli"),
|
||||||
|
patch("subprocess.run", return_value=fake_result) as run_mock,
|
||||||
|
patch("glob.glob", return_value=[str(svg_path)]),
|
||||||
|
):
|
||||||
|
cmd.get_board_2d_view(
|
||||||
|
{"pcbPath": str(board_path), "format": "svg", "responseMode": "inline"}
|
||||||
|
)
|
||||||
|
|
||||||
|
argv = run_mock.call_args[0][0]
|
||||||
|
assert "--mode-single" in argv
|
||||||
|
out_idx = argv.index("--output")
|
||||||
|
out_arg = argv[out_idx + 1]
|
||||||
|
assert out_arg.endswith(".svg") # a file path, not a directory
|
||||||
|
|||||||
Reference in New Issue
Block a user