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.
This commit is contained in:
Ryan Malloy 2026-07-12 13:44:41 -06:00
parent da91382015
commit f01c44d807
2 changed files with 103 additions and 0 deletions

66
src/freeroute/cli.py Normal file
View File

@ -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 <file>)")
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())

37
tests/test_cli.py Normal file
View File

@ -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