klayout-tools 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klayout_tools/__init__.py +3 -0
- klayout_tools/cells.py +148 -0
- klayout_tools/cli/__init__.py +28 -0
- klayout_tools/cli/__main__.py +8 -0
- klayout_tools/cli/cells_cmd.py +84 -0
- klayout_tools/cli/drc_cmd.py +59 -0
- klayout_tools/cli/layers_cmd.py +54 -0
- klayout_tools/cli/output.py +79 -0
- klayout_tools/cli/parser.py +222 -0
- klayout_tools/cli/pdk_cmd.py +82 -0
- klayout_tools/cli/stats_cmd.py +82 -0
- klayout_tools/decks/__init__.py +84 -0
- klayout_tools/decks/gf180mcu.py +208 -0
- klayout_tools/decks/sky130.py +167 -0
- klayout_tools/drc.py +185 -0
- klayout_tools/layers.py +98 -0
- klayout_tools/pdk.py +254 -0
- klayout_tools/stats.py +207 -0
- klayout_tools-0.1.0.dist-info/METADATA +127 -0
- klayout_tools-0.1.0.dist-info/RECORD +23 -0
- klayout_tools-0.1.0.dist-info/WHEEL +4 -0
- klayout_tools-0.1.0.dist-info/entry_points.txt +2 -0
- klayout_tools-0.1.0.dist-info/licenses/LICENSE +21 -0
klayout_tools/cells.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Enumerate the cell hierarchy of a GDSII/OASIS stream.
|
|
2
|
+
|
|
3
|
+
Pure library: :func:`cells_report` returns plain Python data (a ``dict`` of
|
|
4
|
+
JSON-serialisable primitives) and never prints. Serialisation and human-readable
|
|
5
|
+
formatting live in the CLI command module so this function stays reusable (e.g.
|
|
6
|
+
by a future MCP server).
|
|
7
|
+
|
|
8
|
+
Headless invariant: uses the pip ``klayout`` package's batch database API
|
|
9
|
+
(``klayout.db``) only — no GUI, no Qt. Runnable in CI.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CellsError(Exception):
|
|
19
|
+
"""Raised when a layout file cannot be read or enumerated.
|
|
20
|
+
|
|
21
|
+
The CLI turns this into a clean stderr message + exit code 1, never a
|
|
22
|
+
traceback.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cells_report(path: str, top: bool = False) -> dict[str, Any]:
|
|
27
|
+
"""Enumerate the cell hierarchy of a GDSII or OASIS stream.
|
|
28
|
+
|
|
29
|
+
KLayout auto-detects the stream format on read, so both ``.gds`` and
|
|
30
|
+
``.oas`` inputs are handled by the same code path.
|
|
31
|
+
|
|
32
|
+
Returns a dict matching the documented JSON schema (see
|
|
33
|
+
``docs/cli/cells.md``)::
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
"schema_version": 1,
|
|
37
|
+
"file": <path as provided>,
|
|
38
|
+
"dbu_um": <database unit in micrometres, float>,
|
|
39
|
+
"cell_count": <total cells in the whole layout, int>,
|
|
40
|
+
"top_cell_count": <total top cells in the whole layout, int>,
|
|
41
|
+
"cells": [
|
|
42
|
+
{
|
|
43
|
+
"name": str,
|
|
44
|
+
"index": int,
|
|
45
|
+
"is_top": bool,
|
|
46
|
+
"shapes": int,
|
|
47
|
+
"instances": int,
|
|
48
|
+
"children": [str, ...],
|
|
49
|
+
"parents": [str, ...],
|
|
50
|
+
"bbox_um": {"left": float, "bottom": float,
|
|
51
|
+
"right": float, "top": float} | None,
|
|
52
|
+
},
|
|
53
|
+
...
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
``cells`` is sorted by ``index`` ascending for deterministic output.
|
|
58
|
+
``cell_count``/``top_cell_count`` always describe the whole layout,
|
|
59
|
+
regardless of the ``top`` filter. When ``top`` is true, ``cells`` is
|
|
60
|
+
narrowed to entries with ``is_top: True`` only.
|
|
61
|
+
|
|
62
|
+
Field semantics:
|
|
63
|
+
|
|
64
|
+
- ``shapes`` is the count of shapes owned by this cell's own definition,
|
|
65
|
+
summed across all layers — not multiplied by instantiation (same
|
|
66
|
+
convention as ``klt layers``' ``shapes`` field).
|
|
67
|
+
- ``instances`` is the count of child-instance *placement records*
|
|
68
|
+
(``Cell.each_inst()``) — an arrayed instance counts as one placement
|
|
69
|
+
record, not its expanded row-by-column total.
|
|
70
|
+
- ``children``/``parents`` are deduplicated, sorted cell names one level
|
|
71
|
+
of instantiation away (direct only, not transitive).
|
|
72
|
+
- ``bbox_um`` is ``None`` when the cell (including its children) has no
|
|
73
|
+
geometry at all, rather than a degenerate/inverted box.
|
|
74
|
+
|
|
75
|
+
Raises :class:`CellsError` if the file is missing, unreadable, or not a
|
|
76
|
+
recognisable layout stream.
|
|
77
|
+
"""
|
|
78
|
+
if not os.path.exists(path):
|
|
79
|
+
raise CellsError(f"file not found: {path}")
|
|
80
|
+
if os.path.isdir(path):
|
|
81
|
+
raise CellsError(f"not a file: {path}")
|
|
82
|
+
|
|
83
|
+
# Imported lazily so that `klt --version` and argument parsing do not pay
|
|
84
|
+
# the cost of loading the KLayout database module.
|
|
85
|
+
import klayout.db as kdb
|
|
86
|
+
|
|
87
|
+
layout = kdb.Layout()
|
|
88
|
+
try:
|
|
89
|
+
layout.read(path)
|
|
90
|
+
except Exception as exc: # klayout raises RuntimeError for bad/unknown streams
|
|
91
|
+
raise CellsError(f"could not read layout '{path}': {exc}") from exc
|
|
92
|
+
|
|
93
|
+
top_cell_indices = {cell.cell_index() for cell in layout.top_cells()}
|
|
94
|
+
|
|
95
|
+
cells: list[dict[str, Any]] = []
|
|
96
|
+
for cell in layout.each_cell():
|
|
97
|
+
index = cell.cell_index()
|
|
98
|
+
shape_count = sum(
|
|
99
|
+
cell.shapes(layer_index).size() for layer_index in layout.layer_indexes()
|
|
100
|
+
)
|
|
101
|
+
instance_count = sum(1 for _ in cell.each_inst())
|
|
102
|
+
children = sorted(
|
|
103
|
+
{layout.cell(child_index).name for child_index in cell.each_child_cell()}
|
|
104
|
+
)
|
|
105
|
+
parents = sorted(
|
|
106
|
+
{layout.cell(parent_index).name for parent_index in cell.each_parent_cell()}
|
|
107
|
+
)
|
|
108
|
+
bbox = cell.dbbox()
|
|
109
|
+
bbox_um = (
|
|
110
|
+
None
|
|
111
|
+
if bbox.empty()
|
|
112
|
+
else {
|
|
113
|
+
"left": bbox.left,
|
|
114
|
+
"bottom": bbox.bottom,
|
|
115
|
+
"right": bbox.right,
|
|
116
|
+
"top": bbox.top,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
cells.append(
|
|
121
|
+
{
|
|
122
|
+
"name": cell.name,
|
|
123
|
+
"index": index,
|
|
124
|
+
"is_top": index in top_cell_indices,
|
|
125
|
+
"shapes": shape_count,
|
|
126
|
+
"instances": instance_count,
|
|
127
|
+
"children": children,
|
|
128
|
+
"parents": parents,
|
|
129
|
+
"bbox_um": bbox_um,
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
cells.sort(key=lambda entry: entry["index"])
|
|
134
|
+
|
|
135
|
+
cell_count = len(cells)
|
|
136
|
+
top_cell_count = len(top_cell_indices)
|
|
137
|
+
|
|
138
|
+
if top:
|
|
139
|
+
cells = [entry for entry in cells if entry["is_top"]]
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
"schema_version": 1,
|
|
143
|
+
"file": path,
|
|
144
|
+
"dbu_um": layout.dbu,
|
|
145
|
+
"cell_count": cell_count,
|
|
146
|
+
"top_cell_count": top_cell_count,
|
|
147
|
+
"cells": cells,
|
|
148
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""klt — CLI entry point.
|
|
2
|
+
|
|
3
|
+
Subcommands live in ``<verb>_cmd.py`` modules (mirroring kicad-tools' CLI
|
|
4
|
+
package). The argument parser is built in :mod:`parser`; this module dispatches
|
|
5
|
+
to the selected subcommand. Every subcommand supports ``--format json`` — JSON
|
|
6
|
+
is the API.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .. import __version__
|
|
12
|
+
from .parser import create_parser
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv: list[str] | None = None) -> int:
|
|
16
|
+
parser = create_parser()
|
|
17
|
+
args = parser.parse_args(argv)
|
|
18
|
+
|
|
19
|
+
# No subcommand: preserve the scaffold status blurb (and exit 0).
|
|
20
|
+
if getattr(args, "func", None) is None:
|
|
21
|
+
print(f"klt {__version__} — scaffold; run `klt --help` for available commands")
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
return args.func(args)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
sys.exit(main())
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""``klt cells`` command: serialise the cells report as text or JSON.
|
|
2
|
+
|
|
3
|
+
Output goes through the shared envelope helpers in :mod:`.output`, as with
|
|
4
|
+
every other ``klt`` subcommand — see ``docs/json-contract.md``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
|
|
9
|
+
from ..cells import CellsError, cells_report
|
|
10
|
+
from .output import emit_error, emit_success
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(args: argparse.Namespace) -> int:
|
|
14
|
+
try:
|
|
15
|
+
report = cells_report(args.file, top=args.top)
|
|
16
|
+
except CellsError as exc:
|
|
17
|
+
return emit_error("cells", str(exc), args.format)
|
|
18
|
+
|
|
19
|
+
emit_success(report, args.format, _print_text)
|
|
20
|
+
return 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _print_text(report: dict) -> None:
|
|
24
|
+
print(f"file: {report['file']}")
|
|
25
|
+
print(f"dbu_um: {report['dbu_um']}")
|
|
26
|
+
print(f"cells: {report['cell_count']}")
|
|
27
|
+
print(f"top_cells: {report['top_cell_count']}")
|
|
28
|
+
|
|
29
|
+
cells = report["cells"]
|
|
30
|
+
if not cells:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
def fmt_bbox(bbox: dict | None) -> str:
|
|
34
|
+
if bbox is None:
|
|
35
|
+
return "-"
|
|
36
|
+
return (
|
|
37
|
+
f"({bbox['left']:g}, {bbox['bottom']:g}, "
|
|
38
|
+
f"{bbox['right']:g}, {bbox['top']:g})"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
rows = [
|
|
42
|
+
(
|
|
43
|
+
str(entry["index"]),
|
|
44
|
+
entry["name"],
|
|
45
|
+
"yes" if entry["is_top"] else "no",
|
|
46
|
+
str(entry["shapes"]),
|
|
47
|
+
str(entry["instances"]),
|
|
48
|
+
",".join(entry["children"]) if entry["children"] else "-",
|
|
49
|
+
",".join(entry["parents"]) if entry["parents"] else "-",
|
|
50
|
+
fmt_bbox(entry["bbox_um"]),
|
|
51
|
+
)
|
|
52
|
+
for entry in cells
|
|
53
|
+
]
|
|
54
|
+
headers = (
|
|
55
|
+
"index",
|
|
56
|
+
"name",
|
|
57
|
+
"is_top",
|
|
58
|
+
"shapes",
|
|
59
|
+
"instances",
|
|
60
|
+
"children",
|
|
61
|
+
"parents",
|
|
62
|
+
"bbox_um",
|
|
63
|
+
)
|
|
64
|
+
widths = [
|
|
65
|
+
max(len(headers[col]), max(len(row[col]) for row in rows))
|
|
66
|
+
for col in range(len(headers))
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
def fmt(row: tuple[str, ...]) -> str:
|
|
70
|
+
# Numeric-ish columns (index, is_top, shapes, instances) right-aligned;
|
|
71
|
+
# name/children/parents/bbox_um left-aligned.
|
|
72
|
+
right_aligned = {0, 2, 3, 4}
|
|
73
|
+
return " ".join(
|
|
74
|
+
row[col].rjust(widths[col])
|
|
75
|
+
if col in right_aligned
|
|
76
|
+
else row[col].ljust(widths[col])
|
|
77
|
+
for col in range(len(headers))
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
print()
|
|
81
|
+
print(fmt(headers))
|
|
82
|
+
print(" ".join("-" * widths[col] for col in range(len(headers))))
|
|
83
|
+
for row in rows:
|
|
84
|
+
print(fmt(row))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""``klt drc`` command: serialise the DRC report as text or JSON.
|
|
2
|
+
|
|
3
|
+
Output goes through the shared envelope helpers in :mod:`.output`, as with
|
|
4
|
+
every other ``klt`` subcommand — see ``docs/json-contract.md``.
|
|
5
|
+
|
|
6
|
+
Exit codes (see ``docs/cli/drc.md`` for the full table):
|
|
7
|
+
0 - ran clean, no violations
|
|
8
|
+
1 - failed to run (bad file, unknown deck, engine error) — returned by
|
|
9
|
+
``emit_error`` as ``output.ERROR_EXIT_CODE``
|
|
10
|
+
3 - ran successfully, violations found
|
|
11
|
+
(2 is reserved for argparse usage errors, as with every other ``klt`` subcommand.)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
|
|
16
|
+
from ..drc import DrcError, run_drc
|
|
17
|
+
from .output import emit_error, emit_success
|
|
18
|
+
|
|
19
|
+
EXIT_CLEAN = 0
|
|
20
|
+
EXIT_VIOLATIONS = 3
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run(args: argparse.Namespace) -> int:
|
|
24
|
+
try:
|
|
25
|
+
report = run_drc(args.file, args.deck)
|
|
26
|
+
except DrcError as exc:
|
|
27
|
+
return emit_error("drc", str(exc), args.format)
|
|
28
|
+
|
|
29
|
+
emit_success(report, args.format, _print_text)
|
|
30
|
+
|
|
31
|
+
return EXIT_VIOLATIONS if report["status"] == "violations" else EXIT_CLEAN
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _print_text(report: dict) -> None:
|
|
35
|
+
print(f"file: {report['file']}")
|
|
36
|
+
print(f"deck: {report['deck']}")
|
|
37
|
+
print(f"dbu_um: {report['dbu_um']}")
|
|
38
|
+
print(f"status: {report['status']}")
|
|
39
|
+
print(f"violations: {report['violation_count']}")
|
|
40
|
+
|
|
41
|
+
rule_counts = report["rule_counts"]
|
|
42
|
+
if rule_counts:
|
|
43
|
+
print()
|
|
44
|
+
print("rule_counts:")
|
|
45
|
+
for rule_id in sorted(rule_counts):
|
|
46
|
+
print(f" {rule_id}: {rule_counts[rule_id]}")
|
|
47
|
+
|
|
48
|
+
violations = report["violations"]
|
|
49
|
+
if not violations:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
print()
|
|
53
|
+
for entry in violations:
|
|
54
|
+
bbox = entry["bbox"]
|
|
55
|
+
print(
|
|
56
|
+
f"{entry['rule']} {entry['cell']} {entry['layer']} "
|
|
57
|
+
f"({bbox['left']},{bbox['bottom']})-({bbox['right']},{bbox['top']}) "
|
|
58
|
+
f"{entry['description']}"
|
|
59
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""``klt layers`` command: serialise the layers report as text or JSON."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from ..layers import LayersError, layers_report
|
|
6
|
+
from .output import emit_error, emit_success
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run(args: argparse.Namespace) -> int:
|
|
10
|
+
try:
|
|
11
|
+
report = layers_report(args.file)
|
|
12
|
+
except LayersError as exc:
|
|
13
|
+
return emit_error("layers", str(exc), args.format)
|
|
14
|
+
|
|
15
|
+
emit_success(report, args.format, _print_text)
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _print_text(report: dict) -> None:
|
|
20
|
+
print(f"file: {report['file']}")
|
|
21
|
+
print(f"dbu_um: {report['dbu_um']}")
|
|
22
|
+
print(f"layers: {report['layer_count']}")
|
|
23
|
+
|
|
24
|
+
layers = report["layers"]
|
|
25
|
+
if not layers:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
rows = [
|
|
29
|
+
(
|
|
30
|
+
str(entry["layer"]),
|
|
31
|
+
str(entry["datatype"]),
|
|
32
|
+
entry["name"] if entry["name"] is not None else "-",
|
|
33
|
+
str(entry["shapes"]),
|
|
34
|
+
)
|
|
35
|
+
for entry in layers
|
|
36
|
+
]
|
|
37
|
+
headers = ("layer", "datatype", "name", "shapes")
|
|
38
|
+
widths = [
|
|
39
|
+
max(len(headers[col]), max(len(row[col]) for row in rows))
|
|
40
|
+
for col in range(len(headers))
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
def fmt(row: tuple[str, ...]) -> str:
|
|
44
|
+
# layer/datatype/shapes right-aligned (numeric), name left-aligned.
|
|
45
|
+
return " ".join(
|
|
46
|
+
row[col].rjust(widths[col]) if col != 2 else row[col].ljust(widths[col])
|
|
47
|
+
for col in range(len(headers))
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
print()
|
|
51
|
+
print(fmt(headers))
|
|
52
|
+
print(" ".join("-" * widths[col] for col in range(len(headers))))
|
|
53
|
+
for row in rows:
|
|
54
|
+
print(fmt(row))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Shared JSON/text output helper for ``klt`` subcommands.
|
|
2
|
+
|
|
3
|
+
Every ``*_cmd.py`` module emits through :func:`emit_success` /
|
|
4
|
+
:func:`emit_error` instead of hand-rolling ``json.dump``/``print`` — this is
|
|
5
|
+
the one place that knows the documented envelope shape (see
|
|
6
|
+
``docs/json-contract.md``).
|
|
7
|
+
|
|
8
|
+
Design notes (additive envelope, not a wrapping one):
|
|
9
|
+
|
|
10
|
+
- Success payloads stay **flat** at the top level (no ``{"result": {...}}``
|
|
11
|
+
nesting) so existing, already-documented command shapes (e.g. ``klt
|
|
12
|
+
layers``) are unaffected. Commands add their own ``schema_version`` field to
|
|
13
|
+
the payload dict *before* calling :func:`emit_success` — this module does
|
|
14
|
+
not inject it, since the version is owned by the library function that
|
|
15
|
+
builds the payload (see ``layers.py``'s docstring on MCP reuse).
|
|
16
|
+
- ``--format json`` output on success goes to **stdout only**; on error, the
|
|
17
|
+
JSON error object goes to **stderr**, and stdout is left empty. This means
|
|
18
|
+
a caller never has to inspect stdout content to distinguish success from
|
|
19
|
+
failure under ``--format json`` — check the exit code.
|
|
20
|
+
- ``--format text`` is a courtesy rendering, not the contract: success calls
|
|
21
|
+
a command-supplied ``text_renderer`` callback, and errors print a plain
|
|
22
|
+
``klt <command>: <message>`` line to stderr, matching pre-existing
|
|
23
|
+
behaviour.
|
|
24
|
+
- Exit code ``1`` is returned by :func:`emit_error` for application-level
|
|
25
|
+
errors. Argparse-level usage errors (exit code ``2``) are raised by
|
|
26
|
+
argparse itself before a command's ``run()`` executes, so they are out of
|
|
27
|
+
scope for this helper by construction.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import sys
|
|
34
|
+
from collections.abc import Callable
|
|
35
|
+
|
|
36
|
+
#: Application-level error exit code (as opposed to argparse's usage-error 2).
|
|
37
|
+
ERROR_EXIT_CODE = 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def emit_success(
|
|
41
|
+
payload: dict,
|
|
42
|
+
format: str,
|
|
43
|
+
text_renderer: Callable[[dict], None],
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Emit a successful command result in the requested ``format``.
|
|
46
|
+
|
|
47
|
+
``format == "json"`` writes ``payload`` as indented JSON to stdout (plus a
|
|
48
|
+
trailing newline). ``format == "text"`` delegates to ``text_renderer``,
|
|
49
|
+
which is responsible for printing whatever human-readable rendering the
|
|
50
|
+
command defines; ``text_renderer`` is not part of the JSON contract.
|
|
51
|
+
"""
|
|
52
|
+
if format == "json":
|
|
53
|
+
json.dump(payload, sys.stdout, indent=2)
|
|
54
|
+
print()
|
|
55
|
+
else:
|
|
56
|
+
text_renderer(payload)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def emit_error(command: str, message: str, format: str) -> int:
|
|
60
|
+
"""Emit an application-level error and return the exit code to use.
|
|
61
|
+
|
|
62
|
+
``format == "json"`` writes the documented error envelope to stderr:
|
|
63
|
+
``{"schema_version": 1, "error": {"command": ..., "message": ...}}``.
|
|
64
|
+
``format == "text"`` writes the pre-existing plain-text stderr line,
|
|
65
|
+
``klt <command>: <message>``.
|
|
66
|
+
|
|
67
|
+
Always returns :data:`ERROR_EXIT_CODE` (``1``) so a command's ``run()``
|
|
68
|
+
can simply ``return emit_error(...)``.
|
|
69
|
+
"""
|
|
70
|
+
if format == "json":
|
|
71
|
+
error_payload = {
|
|
72
|
+
"schema_version": 1,
|
|
73
|
+
"error": {"command": command, "message": message},
|
|
74
|
+
}
|
|
75
|
+
json.dump(error_payload, sys.stderr, indent=2)
|
|
76
|
+
print(file=sys.stderr)
|
|
77
|
+
else:
|
|
78
|
+
print(f"klt {command}: {message}", file=sys.stderr)
|
|
79
|
+
return ERROR_EXIT_CODE
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Argument parser for the ``klt`` CLI.
|
|
2
|
+
|
|
3
|
+
``--format {text,json}`` is a *per-subcommand* option (kicad-tools convention),
|
|
4
|
+
defaulting to ``text``. New subcommands register themselves here and point their
|
|
5
|
+
``func`` default at a ``run(args) -> int`` handler.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .. import __version__
|
|
12
|
+
from . import cells_cmd, drc_cmd, layers_cmd, pdk_cmd, stats_cmd
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="klt",
|
|
18
|
+
description="Tools for AI agents to work with IC layout.",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument("--version", action="version", version=f"klt {__version__}")
|
|
21
|
+
# No default handler: absence of a subcommand is handled by main().
|
|
22
|
+
parser.set_defaults(func=None)
|
|
23
|
+
|
|
24
|
+
subparsers = parser.add_subparsers(dest="command", metavar="<command>")
|
|
25
|
+
|
|
26
|
+
layers_parser = subparsers.add_parser(
|
|
27
|
+
"layers",
|
|
28
|
+
help="enumerate layers of a GDSII/OASIS stream",
|
|
29
|
+
description=(
|
|
30
|
+
"Report the layer/datatype pairs, names, and per-cell-definition "
|
|
31
|
+
"shape counts of a GDSII or OASIS layout file."
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
layers_parser.add_argument("file", help="path to a GDSII or OASIS layout file")
|
|
35
|
+
layers_parser.add_argument(
|
|
36
|
+
"--format",
|
|
37
|
+
choices=["text", "json"],
|
|
38
|
+
default="text",
|
|
39
|
+
help="output format (default: text)",
|
|
40
|
+
)
|
|
41
|
+
layers_parser.set_defaults(func=layers_cmd.run)
|
|
42
|
+
|
|
43
|
+
stats_parser = subparsers.add_parser(
|
|
44
|
+
"stats",
|
|
45
|
+
help="report area, density, and polygon/vertex counts of a GDSII/OASIS stream",
|
|
46
|
+
description=(
|
|
47
|
+
"Report bounding box, drawn area, density, and polygon/vertex "
|
|
48
|
+
"counts of a GDSII or OASIS layout file, in total and optionally "
|
|
49
|
+
"per layer."
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
stats_parser.add_argument("file", help="path to a GDSII or OASIS layout file")
|
|
53
|
+
stats_parser.add_argument(
|
|
54
|
+
"--per-layer",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="also report the same statistics broken down per layer",
|
|
57
|
+
)
|
|
58
|
+
stats_parser.add_argument(
|
|
59
|
+
"--format",
|
|
60
|
+
choices=["text", "json"],
|
|
61
|
+
default="text",
|
|
62
|
+
help="output format (default: text)",
|
|
63
|
+
)
|
|
64
|
+
stats_parser.set_defaults(func=stats_cmd.run)
|
|
65
|
+
|
|
66
|
+
cells_parser = subparsers.add_parser(
|
|
67
|
+
"cells",
|
|
68
|
+
help="report the cell hierarchy of a GDSII/OASIS stream",
|
|
69
|
+
description=(
|
|
70
|
+
"Report the cell hierarchy of a GDSII or OASIS layout file: "
|
|
71
|
+
"top-cell status, per-cell shape/instance counts, direct "
|
|
72
|
+
"children/parents, and bounding box."
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
cells_parser.add_argument("file", help="path to a GDSII or OASIS layout file")
|
|
76
|
+
cells_parser.add_argument(
|
|
77
|
+
"--top",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="only report top cells (cells with no parent instances)",
|
|
80
|
+
)
|
|
81
|
+
cells_parser.add_argument(
|
|
82
|
+
"--format",
|
|
83
|
+
choices=["text", "json"],
|
|
84
|
+
default="text",
|
|
85
|
+
help="output format (default: text)",
|
|
86
|
+
)
|
|
87
|
+
cells_parser.set_defaults(func=cells_cmd.run)
|
|
88
|
+
|
|
89
|
+
drc_parser = subparsers.add_parser(
|
|
90
|
+
"drc",
|
|
91
|
+
help="run a headless DRC deck against a GDSII/OASIS stream",
|
|
92
|
+
description=(
|
|
93
|
+
"Run a DRC rule deck against a GDSII or OASIS layout file and "
|
|
94
|
+
"report violations as structured data. Runs fully headless via "
|
|
95
|
+
"KLayout's native Region check primitives — no GUI, no Qt, no "
|
|
96
|
+
"standalone klayout binary."
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
drc_parser.add_argument("file", help="path to a GDSII or OASIS layout file")
|
|
100
|
+
drc_parser.add_argument(
|
|
101
|
+
"--deck",
|
|
102
|
+
required=True,
|
|
103
|
+
help=(
|
|
104
|
+
"DRC deck to run (currently: sky130, gf180mcu). Not validated by "
|
|
105
|
+
"argparse -- an unknown deck name exits 1 with a clean error, "
|
|
106
|
+
"per docs/cli/drc.md's exit-code contract, rather than "
|
|
107
|
+
"argparse's usage-error exit 2."
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
drc_parser.add_argument(
|
|
111
|
+
"--format",
|
|
112
|
+
choices=["text", "json"],
|
|
113
|
+
default="text",
|
|
114
|
+
help="output format (default: text)",
|
|
115
|
+
)
|
|
116
|
+
drc_parser.set_defaults(func=drc_cmd.run)
|
|
117
|
+
|
|
118
|
+
_add_pdk_parser(subparsers)
|
|
119
|
+
|
|
120
|
+
return parser
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _add_pdk_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
124
|
+
"""Register the ``pdk`` verb with nested ``find``/``list``/``env`` subcommands.
|
|
125
|
+
|
|
126
|
+
The other verbs are flat; ``pdk`` groups discovery operations under one
|
|
127
|
+
verb (kicad-tools convention for multi-operation capabilities), so it uses
|
|
128
|
+
argparse sub-subparsers. Each ``--format`` stays a per-subcommand option,
|
|
129
|
+
matching the flat verbs.
|
|
130
|
+
"""
|
|
131
|
+
pdk_parser = subparsers.add_parser(
|
|
132
|
+
"pdk",
|
|
133
|
+
help="discover and resolve an installed PDK",
|
|
134
|
+
description=(
|
|
135
|
+
"Locate an installed open_pdks-layout PDK (open_pdks, volare, or "
|
|
136
|
+
"ciel) and report its root, variant, version stamp, and per-tool "
|
|
137
|
+
"asset directories as structured data. This is the one shared "
|
|
138
|
+
"PDK_ROOT resolver every downstream tool imports instead of "
|
|
139
|
+
"re-implementing the lookup. Fully headless; safe in CI."
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
pdk_sub = pdk_parser.add_subparsers(dest="pdk_command", metavar="<subcommand>")
|
|
143
|
+
|
|
144
|
+
def _no_subcommand(_args: argparse.Namespace) -> int:
|
|
145
|
+
pdk_parser.print_help(sys.stderr)
|
|
146
|
+
return 2
|
|
147
|
+
|
|
148
|
+
pdk_parser.set_defaults(func=_no_subcommand)
|
|
149
|
+
|
|
150
|
+
find_parser = pdk_sub.add_parser(
|
|
151
|
+
"find",
|
|
152
|
+
help="resolve one PDK install/variant and report its paths",
|
|
153
|
+
description=(
|
|
154
|
+
"Resolve a single PDK install and variant via the documented "
|
|
155
|
+
"resolution order and emit its root, variant, version, how it was "
|
|
156
|
+
"resolved, and its per-tool asset directories."
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
find_parser.add_argument(
|
|
160
|
+
"--pdk",
|
|
161
|
+
help="variant to resolve (e.g. sky130A); overrides $PDK",
|
|
162
|
+
)
|
|
163
|
+
find_parser.add_argument(
|
|
164
|
+
"--pdk-root",
|
|
165
|
+
dest="pdk_root",
|
|
166
|
+
help="explicit install root; overrides $PDK_ROOT and the search order",
|
|
167
|
+
)
|
|
168
|
+
find_parser.add_argument(
|
|
169
|
+
"--format",
|
|
170
|
+
choices=["text", "json"],
|
|
171
|
+
default="text",
|
|
172
|
+
help="output format (default: text)",
|
|
173
|
+
)
|
|
174
|
+
find_parser.set_defaults(func=pdk_cmd.run_find)
|
|
175
|
+
|
|
176
|
+
list_parser = pdk_sub.add_parser(
|
|
177
|
+
"list",
|
|
178
|
+
help="enumerate every PDK install and variant discovered",
|
|
179
|
+
description=(
|
|
180
|
+
"Enumerate every install and variant found across the full search "
|
|
181
|
+
"order. An empty result is success (exit 0), not an error."
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
list_parser.add_argument(
|
|
185
|
+
"--pdk-root",
|
|
186
|
+
dest="pdk_root",
|
|
187
|
+
help="restrict the scan to this install root",
|
|
188
|
+
)
|
|
189
|
+
list_parser.add_argument(
|
|
190
|
+
"--format",
|
|
191
|
+
choices=["text", "json"],
|
|
192
|
+
default="text",
|
|
193
|
+
help="output format (default: text)",
|
|
194
|
+
)
|
|
195
|
+
list_parser.set_defaults(func=pdk_cmd.run_list)
|
|
196
|
+
|
|
197
|
+
env_parser = pdk_sub.add_parser(
|
|
198
|
+
"env",
|
|
199
|
+
help="emit the resolved paths as eval-able shell exports",
|
|
200
|
+
description=(
|
|
201
|
+
"Emit the resolved install as shell `export` lines "
|
|
202
|
+
'(`eval "$(klt pdk env)"`) so an interactive simulator or '
|
|
203
|
+
"schematic-editor session uses the same install the automated "
|
|
204
|
+
"tooling picked. --format json emits the same payload as `find`."
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
env_parser.add_argument(
|
|
208
|
+
"--pdk",
|
|
209
|
+
help="variant to resolve (e.g. sky130A); overrides $PDK",
|
|
210
|
+
)
|
|
211
|
+
env_parser.add_argument(
|
|
212
|
+
"--pdk-root",
|
|
213
|
+
dest="pdk_root",
|
|
214
|
+
help="explicit install root; overrides $PDK_ROOT and the search order",
|
|
215
|
+
)
|
|
216
|
+
env_parser.add_argument(
|
|
217
|
+
"--format",
|
|
218
|
+
choices=["text", "json"],
|
|
219
|
+
default="text",
|
|
220
|
+
help="output format (default: text; text emits shell exports)",
|
|
221
|
+
)
|
|
222
|
+
env_parser.set_defaults(func=pdk_cmd.run_env)
|