From f01c44d80725b45c4b9f19e45ab53ca2c6f11040 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sun, 12 Jul 2026 13:44:41 -0600 Subject: [PATCH] Implement the freeroute CLI (DSN in, SES out, JAR-flag-compatible) The pyproject declared a freeroute script but freeroute.cli was missing, so the entry point was broken. Add it: reads a Specctra .dsn, routes (rip-up on by default), writes SES. Accepts FreeRouting-compatible -de/-do flags so it can be dropped in wherever 'java -jar freerouting.jar' was called. --- src/freeroute/cli.py | 66 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_cli.py | 37 +++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/freeroute/cli.py create mode 100644 tests/test_cli.py diff --git a/src/freeroute/cli.py b/src/freeroute/cli.py new file mode 100644 index 0000000..b2fc3da --- /dev/null +++ b/src/freeroute/cli.py @@ -0,0 +1,66 @@ +"""freeroute command-line interface: Specctra DSN in, routed SES out. + +Flag-compatible with the FreeRouting JAR (``-de`` / ``-do``) so it can be +dropped in wherever ``java -jar freerouting.jar`` was invoked. +""" + +from __future__ import annotations + +import argparse +import sys + +from freeroute.dsn.reader import parse_dsn +from freeroute.route.pipeline import build_routing_result +from freeroute.ses import write_ses + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="freeroute", + description="Native PCB autorouter — route a Specctra DSN and write an SES session file.", + ) + parser.add_argument("dsn", nargs="?", help="input Specctra .dsn file") + parser.add_argument( + "-de", "--design", dest="design", help="input .dsn (FreeRouting-compatible alias)" + ) + parser.add_argument( + "-do", "--output", dest="output", help="output .ses file (default: stdout)" + ) + parser.add_argument( + "-mp", + "--max-passes", + type=int, + default=None, + help="accepted for FreeRouting CLI compatibility (advisory; freeroute self-bounds passes)", + ) + parser.add_argument("--no-rip-up", action="store_true", help="disable rip-up-and-retry") + parser.add_argument("--layers", help="comma-separated signal layer indices to route on") + args = parser.parse_args(argv) + + dsn_path = args.design or args.dsn + if not dsn_path: + parser.error("no input DSN (pass a path, or -de )") + + layers = [int(x) for x in args.layers.split(",")] if args.layers else None + + try: + with open(dsn_path, encoding="utf-8") as f: + dsn_text = f.read() + except OSError as e: + print(f"freeroute: cannot read {dsn_path}: {e}", file=sys.stderr) + return 2 + + dsn = parse_dsn(dsn_text) + result = build_routing_result(dsn, layers=layers, rip_up=not args.no_rip_up) + ses_text = write_ses(dsn, result) + + if args.output: + with open(args.output, "w", encoding="utf-8") as f: + f.write(ses_text) + else: + sys.stdout.write(ses_text) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..1657207 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,37 @@ +"""Tests for the freeroute CLI (Specctra DSN in, SES out).""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from freeroute.cli import main + +FIXTURE = Path(__file__).parent / "dsn" / "fixtures" / "simple_2net.dsn" + + +def test_cli_jar_compatible_flags_write_ses(tmp_path): + out = tmp_path / "out.ses" + rc = main(["-de", str(FIXTURE), "-do", str(out)]) + assert rc == 0 + text = out.read_text() + assert text.startswith("(session") + assert "(net" in text and "(wire" in text + + +def test_cli_positional_input_to_stdout(capsys): + rc = main([str(FIXTURE)]) + assert rc == 0 + assert "(session" in capsys.readouterr().out + + +def test_cli_missing_input_errors(): + with pytest.raises(SystemExit): + main([]) + + +def test_cli_unreadable_input_returns_2(capsys): + rc = main(["/nonexistent/board.dsn"]) + assert rc == 2 + assert "cannot read" in capsys.readouterr().err