From 066e7f63d47de828cb4c1235684f2b06bd0aa178 Mon Sep 17 00:00:00 2001 From: mixelpixx Date: Sat, 30 May 2026 09:42:20 -0400 Subject: [PATCH] fix(board-view): KiCad 10 kicad-cli svg export compatibility (#209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ''"). - `--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) --- python/commands/board/view.py | 37 ++++++++---- tests/test_get_board_2d_view_save_to_file.py | 60 ++++++++++++++++++++ 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/python/commands/board/view.py b/python/commands/board/view.py index ef8459e..8156ecf 100644 --- a/python/commands/board/view.py +++ b/python/commands/board/view.py @@ -182,7 +182,23 @@ class BoardViewCommands: } 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 ''"). + # - `--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: cmd += ["--layers", ",".join(layers)] cmd.append(pcb_path) @@ -196,21 +212,22 @@ class BoardViewCommands: "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 { "success": False, "message": "kicad-cli SVG export failed", "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] # --- Render to bytes (shared for both response modes) --- diff --git a/tests/test_get_board_2d_view_save_to_file.py b/tests/test_get_board_2d_view_save_to_file.py index f6d8b3e..5e3f7a5 100644 --- a/tests/test_get_board_2d_view_save_to_file.py +++ b/tests/test_get_board_2d_view_save_to_file.py @@ -154,3 +154,63 @@ def test_file_mode_png_writes_file_and_returns_path(tmp_path): assert expected.exists() assert expected.read_bytes() == _FAKE_PNG 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