rocket-engine 0.0.2b1__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.
- rocket_engine/__init__.py +8 -0
- rocket_engine/__main__.py +12 -0
- rocket_engine/adapters/README.md +54 -0
- rocket_engine/adapters/__init__.py +7 -0
- rocket_engine/adapters/api.py +39 -0
- rocket_engine/adapters/base.py +46 -0
- rocket_engine/adapters/cace/__init__.py +163 -0
- rocket_engine/adapters/circulax/README.md +10 -0
- rocket_engine/adapters/circulax/__init__.py +112 -0
- rocket_engine/adapters/circulax/_driver.py +190 -0
- rocket_engine/adapters/circulax/adapter.py +538 -0
- rocket_engine/adapters/circulax/results.py +291 -0
- rocket_engine/adapters/cli/README.md +34 -0
- rocket_engine/adapters/cli/__init__.py +305 -0
- rocket_engine/adapters/connectivity/__init__.py +196 -0
- rocket_engine/adapters/connectivity/results.py +214 -0
- rocket_engine/adapters/cosim/README.md +9 -0
- rocket_engine/adapters/cosim/__init__.py +1390 -0
- rocket_engine/adapters/cosim/results.py +214 -0
- rocket_engine/adapters/devsim/__init__.py +171 -0
- rocket_engine/adapters/digital_flow/README.md +9 -0
- rocket_engine/adapters/digital_flow/__init__.py +1121 -0
- rocket_engine/adapters/digital_flow/results.py +227 -0
- rocket_engine/adapters/digital_flow/state.py +462 -0
- rocket_engine/adapters/drc/README.klayout.md +7 -0
- rocket_engine/adapters/drc/README.md +7 -0
- rocket_engine/adapters/drc/__init__.py +1 -0
- rocket_engine/adapters/drc/klayout.py +952 -0
- rocket_engine/adapters/drc/magic.py +287 -0
- rocket_engine/adapters/drc/results.py +184 -0
- rocket_engine/adapters/drc/results_kl.py +188 -0
- rocket_engine/adapters/elmer/__init__.py +163 -0
- rocket_engine/adapters/envelope.py +120 -0
- rocket_engine/adapters/equivalence/__init__.py +156 -0
- rocket_engine/adapters/errors.py +181 -0
- rocket_engine/adapters/extract_magic/README.md +15 -0
- rocket_engine/adapters/extract_magic/__init__.py +425 -0
- rocket_engine/adapters/extract_magic/results.py +209 -0
- rocket_engine/adapters/formal_sby/README.md +7 -0
- rocket_engine/adapters/formal_sby/__init__.py +773 -0
- rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
- rocket_engine/adapters/formal_sby/results.py +185 -0
- rocket_engine/adapters/gdsfactory/__init__.py +194 -0
- rocket_engine/adapters/gdsfactory/_driver.py +459 -0
- rocket_engine/adapters/gdsfactory/adapter.py +609 -0
- rocket_engine/adapters/gdsfactory/results.py +181 -0
- rocket_engine/adapters/hdl_sim/README.md +7 -0
- rocket_engine/adapters/hdl_sim/__init__.py +369 -0
- rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
- rocket_engine/adapters/hdl_sim/results.py +178 -0
- rocket_engine/adapters/hdl_sim/verilator.py +167 -0
- rocket_engine/adapters/http/README.md +15 -0
- rocket_engine/adapters/http/__init__.py +458 -0
- rocket_engine/adapters/http/events.py +63 -0
- rocket_engine/adapters/ipc/README.md +9 -0
- rocket_engine/adapters/ipc/__init__.py +111 -0
- rocket_engine/adapters/lvs_netgen/README.md +20 -0
- rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
- rocket_engine/adapters/lvs_netgen/results.py +168 -0
- rocket_engine/adapters/mcp/README.md +19 -0
- rocket_engine/adapters/mcp/__init__.py +85 -0
- rocket_engine/adapters/mcp/api-schema.json +6107 -0
- rocket_engine/adapters/mcp/pull.py +246 -0
- rocket_engine/adapters/mcp/server.py +794 -0
- rocket_engine/adapters/meep/__init__.py +167 -0
- rocket_engine/adapters/meow/README.md +7 -0
- rocket_engine/adapters/meow/__init__.py +190 -0
- rocket_engine/adapters/meow/_driver.py +227 -0
- rocket_engine/adapters/meow/_envelope.py +108 -0
- rocket_engine/adapters/meow/adapter.py +491 -0
- rocket_engine/adapters/meow/results.py +218 -0
- rocket_engine/adapters/mutation/__init__.py +192 -0
- rocket_engine/adapters/ngspice/README.md +28 -0
- rocket_engine/adapters/ngspice/__init__.py +627 -0
- rocket_engine/adapters/ngspice/results.py +215 -0
- rocket_engine/adapters/postlayout/README.md +28 -0
- rocket_engine/adapters/postlayout/__init__.py +17 -0
- rocket_engine/adapters/postlayout/adapter.py +1062 -0
- rocket_engine/adapters/postlayout/results.py +330 -0
- rocket_engine/adapters/postlayout/translate.py +71 -0
- rocket_engine/adapters/profile.py +220 -0
- rocket_engine/adapters/sax/README.md +23 -0
- rocket_engine/adapters/sax/__init__.py +516 -0
- rocket_engine/adapters/sax/_driver.py +148 -0
- rocket_engine/adapters/sax/results.py +291 -0
- rocket_engine/adapters/stdio_console.py +1151 -0
- rocket_engine/adapters/syntax/README.md +5 -0
- rocket_engine/adapters/syntax/__init__.py +366 -0
- rocket_engine/adapters/syntax/results.py +181 -0
- rocket_engine/adapters/synth_yosys/README.md +7 -0
- rocket_engine/adapters/synth_yosys/__init__.py +655 -0
- rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
- rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
- rocket_engine/adapters/synth_yosys/results.py +173 -0
- rocket_engine/adapters/timing_sta/README.md +9 -0
- rocket_engine/adapters/timing_sta/__init__.py +726 -0
- rocket_engine/adapters/view_sync/__init__.py +185 -0
- rocket_engine/adapters/view_sync/results.py +189 -0
- rocket_engine/adapters/vlsir/__init__.py +142 -0
- rocket_engine/adapters/xschem/__init__.py +440 -0
- rocket_engine/adapters/xschem/deck.py +266 -0
- rocket_engine/clients/__init__.py +5 -0
- rocket_engine/clients/runtime.py +255 -0
- rocket_engine/composition.py +2279 -0
- rocket_engine/execution/README.md +30 -0
- rocket_engine/execution/__init__.py +150 -0
- rocket_engine/execution/apig/__init__.py +1 -0
- rocket_engine/execution/apig/pool.py +463 -0
- rocket_engine/execution/apig/submit.py +124 -0
- rocket_engine/execution/block_runs_scope.py +479 -0
- rocket_engine/execution/coalesce.py +414 -0
- rocket_engine/execution/console/__init__.py +1 -0
- rocket_engine/execution/console/apply_session.py +318 -0
- rocket_engine/execution/console/lifecycle.py +265 -0
- rocket_engine/execution/dispatch.py +205 -0
- rocket_engine/execution/errors.py +31 -0
- rocket_engine/execution/ports.py +797 -0
- rocket_engine/execution/pyexec/__init__.py +1 -0
- rocket_engine/execution/pyexec/apply_context.py +547 -0
- rocket_engine/execution/pyexec/approvals.py +173 -0
- rocket_engine/execution/pyexec/runner.py +404 -0
- rocket_engine/execution/pyexec/support.py +464 -0
- rocket_engine/execution/python_exec.py +897 -0
- rocket_engine/execution/queue.py +85 -0
- rocket_engine/execution/replay_safe.py +601 -0
- rocket_engine/execution/routing/__init__.py +1 -0
- rocket_engine/execution/routing/calls.py +364 -0
- rocket_engine/execution/routing/namespace.py +785 -0
- rocket_engine/execution/routing/ports.py +102 -0
- rocket_engine/execution/routing/tools.py +346 -0
- rocket_engine/execution/runner.py +311 -0
- rocket_engine/execution/runtime.py +1149 -0
- rocket_engine/execution/service.py +279 -0
- rocket_engine/execution/worker.py +464 -0
- rocket_engine/interfaces/README.md +26 -0
- rocket_engine/interfaces/__init__.py +58 -0
- rocket_engine/interfaces/apig_assets.py +407 -0
- rocket_engine/interfaces/celllink.py +461 -0
- rocket_engine/interfaces/documents.py +92 -0
- rocket_engine/interfaces/envelopes.py +131 -0
- rocket_engine/interfaces/errors.py +16 -0
- rocket_engine/interfaces/layout.py +410 -0
- rocket_engine/interfaces/layout_schema.yaml +278 -0
- rocket_engine/interfaces/paths.py +223 -0
- rocket_engine/interfaces/pdks.py +550 -0
- rocket_engine/interfaces/ports.py +72 -0
- rocket_engine/interfaces/references.py +115 -0
- rocket_engine/interfaces/refs.py +85 -0
- rocket_engine/interfaces/runparams.py +304 -0
- rocket_engine/interfaces/setup.py +552 -0
- rocket_engine/interfaces/setup_catalogue.yaml +243 -0
- rocket_engine/interfaces/setup_probes.py +66 -0
- rocket_engine/interfaces/vocabulary.py +58 -0
- rocket_engine/interfaces/yaml_dump.py +31 -0
- rocket_engine/library/README.md +22 -0
- rocket_engine/library/__init__.py +13 -0
- rocket_engine/library/apig_catalog.py +371 -0
- rocket_engine/library/catalog.py +185 -0
- rocket_engine/library/errors.py +16 -0
- rocket_engine/mutate/__init__.py +776 -0
- rocket_engine/mutate/operators.py +700 -0
- rocket_engine/ops/__init__.py +1 -0
- rocket_engine/ops/deferred.py +279 -0
- rocket_engine/ops/editors.py +404 -0
- rocket_engine/ops/evidence.py +194 -0
- rocket_engine/ops/gates.py +262 -0
- rocket_engine/ops/ports.py +76 -0
- rocket_engine/ops/queries.py +417 -0
- rocket_engine/ops/runs.py +553 -0
- rocket_engine/ops/submit.py +614 -0
- rocket_engine/project/README.md +27 -0
- rocket_engine/project/__init__.py +6 -0
- rocket_engine/project/canvas/__init__.py +55 -0
- rocket_engine/project/canvas/compile.py +109 -0
- rocket_engine/project/canvas/integrate.py +48 -0
- rocket_engine/project/canvas/schema.py +241 -0
- rocket_engine/project/canvas/templates.py +46 -0
- rocket_engine/project/canvas/validate.py +603 -0
- rocket_engine/project/edit/__init__.py +5 -0
- rocket_engine/project/edit/cas.py +87 -0
- rocket_engine/project/edit/transactions.py +282 -0
- rocket_engine/project/errors.py +16 -0
- rocket_engine/project/frontend13.py +685 -0
- rocket_engine/project/geometry/__init__.py +23 -0
- rocket_engine/project/geometry/ops.py +451 -0
- rocket_engine/project/geometry/snap.py +103 -0
- rocket_engine/project/ip/__init__.py +1 -0
- rocket_engine/project/ip/blocks.py +195 -0
- rocket_engine/project/ip/entry.schema.json +78 -0
- rocket_engine/project/ip/lifecycle.py +402 -0
- rocket_engine/project/ip/verbs.py +123 -0
- rocket_engine/project/layers/__init__.py +6 -0
- rocket_engine/project/layers/bindings.py +115 -0
- rocket_engine/project/layers/catalog.py +228 -0
- rocket_engine/project/layers/presets.py +43 -0
- rocket_engine/project/ledger/__init__.py +1 -0
- rocket_engine/project/ledger/docs.py +167 -0
- rocket_engine/project/ledger/entities.py +176 -0
- rocket_engine/project/ledger/events.py +29 -0
- rocket_engine/project/ledger/runs.py +257 -0
- rocket_engine/project/ledger/shared.py +174 -0
- rocket_engine/project/ledger/snapshots.py +385 -0
- rocket_engine/project/ledger/wires.py +552 -0
- rocket_engine/project/process/__init__.py +14 -0
- rocket_engine/project/process/model.py +193 -0
- rocket_engine/project/process/recipes.py +121 -0
- rocket_engine/project/schematic/__init__.py +1 -0
- rocket_engine/project/schematic/hierarchy.py +312 -0
- rocket_engine/project/schematic/instances.py +393 -0
- rocket_engine/project/schematic/sheets.py +198 -0
- rocket_engine/project/service.py +2941 -0
- rocket_engine/project/symbols/__init__.py +1 -0
- rocket_engine/project/symbols/cells.py +324 -0
- rocket_engine/project/symbols/linkage.py +356 -0
- rocket_engine/project/symbols/symbols.py +352 -0
- rocket_engine/project/templates/__init__.py +303 -0
- rocket_engine/project/templates/errors.py +25 -0
- rocket_engine/project/testbench/__init__.py +49 -0
- rocket_engine/project/testbench/blocks.py +619 -0
- rocket_engine/project/testbench/templates.py +134 -0
- rocket_engine/stores/README.md +12 -0
- rocket_engine/stores/__init__.py +6 -0
- rocket_engine/stores/ecs.py +1508 -0
- rocket_engine/stores/layers.py +1023 -0
- rocket_engine/stores/settings.py +1188 -0
- rocket_engine/stores/wireplace.py +1324 -0
- rocket_engine/workflows/README.md +18 -0
- rocket_engine/workflows/__init__.py +53 -0
- rocket_engine/workflows/capability_plan.py +102 -0
- rocket_engine/workflows/completion.py +228 -0
- rocket_engine/workflows/connectgraph.py +734 -0
- rocket_engine/workflows/connectgraph_emit.py +336 -0
- rocket_engine/workflows/gates.py +144 -0
- rocket_engine/workflows/photonic_wire.py +76 -0
- rocket_engine/workflows/stages.py +324 -0
- rocket_engine/workflows/viewsync.py +399 -0
- rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
- rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
- rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
- rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
- rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
"""OpenSTA timing adapter: run_timing behind the ToolAdapter port.
|
|
2
|
+
|
|
3
|
+
Protocol, best-knowledge OpenSTA (live proof deferred: `sta` is not
|
|
4
|
+
provisioned in this sandbox, so the report flags every unverified
|
|
5
|
+
command-line assumption for the gate host): write a timing script
|
|
6
|
+
(`read_liberty` the libs, `read_verilog` the mapped netlist files,
|
|
7
|
+
`link_design` the top, `read_spef` when a top-level SPEF is given,
|
|
8
|
+
`read_sdc` the constraints), then marker-delimited report sections
|
|
9
|
+
(`report_checks -path_delay min_max` for setup+hold critical paths,
|
|
10
|
+
`report_wns`/`report_tns` per corner for WNS/TNS, bare
|
|
11
|
+
`report_check_types` for slew/load violators, `check_setup -verbose`
|
|
12
|
+
unconstrained endpoints) closed by a trailing `exit` so `sta` can
|
|
13
|
+
never linger at its interactive prompt. Parse splits the log on the
|
|
14
|
+
markers: per-path slack records with MET/VIOLATED status, the four
|
|
15
|
+
WNS/TNS margins, constraint violators, and check_setup evidence.
|
|
16
|
+
Unconstrained endpoints are findings (rule
|
|
17
|
+
`unconstrained_endpoint`) plus raw `check_setup` text that is always
|
|
18
|
+
attached — listed, never silent, never assumed.
|
|
19
|
+
|
|
20
|
+
Tool resolution: an explicit job `tool_dir` wins, then
|
|
21
|
+
`$OPENSTA_PREFIX/bin` (the PROV consumer contract), else PATH, and
|
|
22
|
+
the resolved binary must answer its version probe with an `OpenSTA`
|
|
23
|
+
banner — ambient installs are kept and version-asserted, never
|
|
24
|
+
reinstalled. Where the tools live on disk is B10 integration scope.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
import shutil
|
|
30
|
+
import subprocess
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from ...execution.errors import ExecutionError
|
|
34
|
+
from ...execution.runner import tool_roots
|
|
35
|
+
from ...interfaces import ToolAdapter
|
|
36
|
+
from ..errors import AdapterError
|
|
37
|
+
|
|
38
|
+
OPERATION = "run_timing"
|
|
39
|
+
_TOOL = "sta"
|
|
40
|
+
_TIMEOUT_S = 300
|
|
41
|
+
_VERSION_TIMEOUT_S = 30
|
|
42
|
+
|
|
43
|
+
_SCRIPT_NAME = "timing.tcl"
|
|
44
|
+
_LOG_NAME = "timing.log"
|
|
45
|
+
|
|
46
|
+
_WNS_TNS_RE = re.compile(
|
|
47
|
+
r"^(?:wns|tns)\s+(?:max|min)\s+"
|
|
48
|
+
r"([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*$",
|
|
49
|
+
re.M,
|
|
50
|
+
)
|
|
51
|
+
_TOP_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
52
|
+
_VERSION_RE = re.compile(r"OpenSTA", re.IGNORECASE)
|
|
53
|
+
# Real `sta -version` prints a bare number (observed: "2.7.0");
|
|
54
|
+
# the OpenSTA banner only appears on startup. Accept either —
|
|
55
|
+
# the binary path is already the pinned provisioned one.
|
|
56
|
+
_BARE_VERSION_RE = re.compile(r"^\d+\.\d+(?:\.\d+)?$")
|
|
57
|
+
_ERROR_RE = re.compile(r"^(?:Error|ERROR):\s*(.*)$", re.M)
|
|
58
|
+
_WARNING_RE = re.compile(r"^Warning:\s*(.*)$", re.M)
|
|
59
|
+
_MARKER_RE = re.compile(r"^===== TIMING_STA ([A-Z_]+) =====\s*$", re.M)
|
|
60
|
+
_MARGIN_RE = re.compile(
|
|
61
|
+
r"^\s*(?:wns|tns)\s+([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)",
|
|
62
|
+
re.M,
|
|
63
|
+
)
|
|
64
|
+
_STARTPOINT_RE = re.compile(r"^Startpoint:\s*(\S+)", re.M)
|
|
65
|
+
_ENDPOINT_RE = re.compile(r"^Endpoint:\s*(\S+)", re.M)
|
|
66
|
+
_GROUP_RE = re.compile(r"^Path Group:\s*(.+?)\s*$", re.M)
|
|
67
|
+
_TYPE_RE = re.compile(r"^Path Type:\s*(max|min)\s*$", re.M)
|
|
68
|
+
_SLACK_RE = re.compile(
|
|
69
|
+
r"^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)"
|
|
70
|
+
r"\s+slack\s+\((MET|VIOLATED)\)",
|
|
71
|
+
re.M,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _parse_errors(text):
|
|
76
|
+
"""Collect sta_error violations from sta Error lines.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
text: Combined sta log/stdout/stderr text.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Error violations in log order.
|
|
83
|
+
"""
|
|
84
|
+
return [
|
|
85
|
+
{"rule": "sta_error", "severity": "error", "message": message}
|
|
86
|
+
for message in _ERROR_RE.findall(text)
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_warnings(text):
|
|
91
|
+
"""Collect warning findings, flagging unconstrained endpoints.
|
|
92
|
+
|
|
93
|
+
Warnings whose message names something unconstrained become
|
|
94
|
+
`unconstrained_endpoint` findings; every other warning is a
|
|
95
|
+
generic `sta_warning`. Neither fails the verdict on its own.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
text: Combined sta log/stdout/stderr text.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Warning violations in log order.
|
|
102
|
+
"""
|
|
103
|
+
violations = []
|
|
104
|
+
for message in _WARNING_RE.findall(text):
|
|
105
|
+
rule = (
|
|
106
|
+
"unconstrained_endpoint"
|
|
107
|
+
if "unconstrained" in message.lower()
|
|
108
|
+
else "sta_warning"
|
|
109
|
+
)
|
|
110
|
+
violations.append(
|
|
111
|
+
{"rule": rule, "severity": "warning", "message": message}
|
|
112
|
+
)
|
|
113
|
+
return violations
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _parse_sections(text):
|
|
117
|
+
"""Split the log on TIMING_STA markers into {name: body}.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
text: Combined sta log/stdout/stderr text.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Mapping of section name to section body (possibly empty).
|
|
124
|
+
"""
|
|
125
|
+
sections = {}
|
|
126
|
+
marks = list(_MARKER_RE.finditer(text))
|
|
127
|
+
for index, mark in enumerate(marks):
|
|
128
|
+
start = mark.end()
|
|
129
|
+
end = marks[index + 1].start() if index + 1 < len(marks) else len(text)
|
|
130
|
+
sections[mark.group(1)] = text[start:end]
|
|
131
|
+
return sections
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _parse_paths(text):
|
|
135
|
+
"""Parse critical paths from the report_checks section.
|
|
136
|
+
|
|
137
|
+
Each `Startpoint:` chunk yields startpoint, endpoint, path group,
|
|
138
|
+
path type (max = setup, min = hold), slack in ns, and the
|
|
139
|
+
violated flag. Chunks without a slack line are skipped: an empty
|
|
140
|
+
or pathless section parses as no paths, never as an error.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
text: REPORT_CHECKS section body.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Path records in report order.
|
|
147
|
+
"""
|
|
148
|
+
paths = []
|
|
149
|
+
starts = list(_STARTPOINT_RE.finditer(text))
|
|
150
|
+
for index, start in enumerate(starts):
|
|
151
|
+
end = starts[index + 1].start() if index + 1 < len(starts) else len(text)
|
|
152
|
+
chunk = text[start.start() : end]
|
|
153
|
+
slack = _SLACK_RE.search(chunk)
|
|
154
|
+
if slack is None:
|
|
155
|
+
continue
|
|
156
|
+
endpoint = _ENDPOINT_RE.search(chunk)
|
|
157
|
+
group = _GROUP_RE.search(chunk)
|
|
158
|
+
path_type = _TYPE_RE.search(chunk)
|
|
159
|
+
value = float(slack.group(1))
|
|
160
|
+
paths.append(
|
|
161
|
+
{
|
|
162
|
+
"startpoint": start.group(1),
|
|
163
|
+
"endpoint": endpoint.group(1) if endpoint else "",
|
|
164
|
+
"group": group.group(1) if group else "",
|
|
165
|
+
"type": path_type.group(1) if path_type else "",
|
|
166
|
+
"slack": value,
|
|
167
|
+
"violated": slack.group(2) == "VIOLATED" or value < 0,
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
return paths
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _parse_margin(text):
|
|
174
|
+
"""Read the first wns/tns value from one margin section.
|
|
175
|
+
|
|
176
|
+
Real `report_wns`/`report_tns` print `wns max <v>` lines
|
|
177
|
+
(observed); the slack-parenthetical form is kept as a
|
|
178
|
+
fallback for path-row text.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
text: One WNS_*/TNS_* section body.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Margin in ns, or None when the section carries no value.
|
|
185
|
+
"""
|
|
186
|
+
match = _WNS_TNS_RE.search(text)
|
|
187
|
+
if match is None:
|
|
188
|
+
match = _MARGIN_RE.search(text)
|
|
189
|
+
return float(match.group(1)) if match is not None else None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _parse_constraint_violations(text):
|
|
193
|
+
"""Collect constraint_violation errors from VIOLATED lines.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
text: CONSTRAINTS section body.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Error violations, one per VIOLATED line.
|
|
200
|
+
"""
|
|
201
|
+
return [
|
|
202
|
+
{
|
|
203
|
+
"rule": "constraint_violation",
|
|
204
|
+
"severity": "error",
|
|
205
|
+
"message": line.strip(),
|
|
206
|
+
}
|
|
207
|
+
for line in text.splitlines()
|
|
208
|
+
if "VIOLATED" in line
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _quote(path):
|
|
213
|
+
"""Brace-quote one path for the sta TCL script.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
path: Filesystem path to embed in the script.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Brace-quoted path (no TCL substitution inside braces).
|
|
220
|
+
"""
|
|
221
|
+
return "{" + str(path) + "}"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class OpenStaTimingAdapter(ToolAdapter):
|
|
225
|
+
"""run_timing via OpenSTA: top-level STA recheck plus evidence.
|
|
226
|
+
|
|
227
|
+
Serves the run_timing operation with deck.tool `opensta` (the
|
|
228
|
+
only tool). Reads the timing libs, the mapped netlist files, the
|
|
229
|
+
optional top-level SPEF, and the SDC constraints, then reports
|
|
230
|
+
setup/hold WNS/TNS margins, critical paths, slew/load
|
|
231
|
+
violators, and unconstrained endpoints. Evidence is the four
|
|
232
|
+
margins, the critical-path records, the unconstrained-endpoint
|
|
233
|
+
findings with the raw check_setup text, and the sta version.
|
|
234
|
+
|
|
235
|
+
Job keys:
|
|
236
|
+
netlist (required): Mapped Verilog netlist paths, non-empty
|
|
237
|
+
list, every file must exist.
|
|
238
|
+
libs (required): Timing liberty (.lib) paths, non-empty
|
|
239
|
+
list, every file must exist.
|
|
240
|
+
sdc (required): SDC constraints path, must exist. Passed to
|
|
241
|
+
sta opaquely — generated clocks and boundary
|
|
242
|
+
constraints flow through untouched.
|
|
243
|
+
spef: Top-level SPEF path for extracted-RC annotation;
|
|
244
|
+
no parasitics without it.
|
|
245
|
+
top (required): Top module name (plain identifier).
|
|
246
|
+
timeout_s: STA budget in seconds (default 300).
|
|
247
|
+
tool_dir: Directory holding sta; $OPENSTA_PREFIX/bin then
|
|
248
|
+
PATH otherwise.
|
|
249
|
+
deck: Optional mapping; deck.tool must be opensta here.
|
|
250
|
+
workdir: Run directory (default ".").
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
runner: Optional isolated runner; direct subprocess when None.
|
|
254
|
+
|
|
255
|
+
Caution: Exit zero is not PASS — sta keeps going past command
|
|
256
|
+
errors, so any `Error:` line fails the run however it
|
|
257
|
+
exited, and a clean run without all four WNS/TNS margins
|
|
258
|
+
is runner_unavailable, never a clean verdict.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def __init__(self, runner=None):
|
|
262
|
+
self._runner = runner
|
|
263
|
+
|
|
264
|
+
def capabilities(self, target):
|
|
265
|
+
"""Report the served operation, tool, and SPEF support.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
target: Target the capabilities are reported for
|
|
269
|
+
(accepted, unused — one top per job).
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Capability mapping with operations, tool, and spef
|
|
273
|
+
support.
|
|
274
|
+
"""
|
|
275
|
+
return {
|
|
276
|
+
"operations": [OPERATION],
|
|
277
|
+
"tool": "opensta",
|
|
278
|
+
"spef": True,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
def preflight(self, job):
|
|
282
|
+
"""Check operation, top, inputs, timeout, and sta.
|
|
283
|
+
|
|
284
|
+
Cheap deterministic checks first, tool resolution last; the
|
|
285
|
+
resolved binary must answer its version probe with an
|
|
286
|
+
OpenSTA banner.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
job: Solver job with operation run_timing plus netlist,
|
|
290
|
+
libs, sdc, and optional spef/top/timeout inputs.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Checked mapping with the resolved sta path and
|
|
294
|
+
version, netlist, libs, sdc, spef, top, timeout_s.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
AdapterError: unsupported_operation for another
|
|
298
|
+
operation, invalid_command for a bad top or
|
|
299
|
+
timeout, invalid_reference for missing or
|
|
300
|
+
non-list netlist/libs, a missing sdc/spef, or a
|
|
301
|
+
path unsafe for the TCL script, missing_dependency
|
|
302
|
+
for an unresolvable or unassertable sta.
|
|
303
|
+
"""
|
|
304
|
+
# Cheap deterministic checks first: operation, top, timeout,
|
|
305
|
+
# netlist, libs, sdc, spef. Tool resolution last, since it
|
|
306
|
+
# depends on the machine.
|
|
307
|
+
if job.get("operation") != OPERATION:
|
|
308
|
+
raise AdapterError(
|
|
309
|
+
"unsupported_operation",
|
|
310
|
+
f"OpenSTA timing handles {OPERATION}, got {job.get('operation')}.",
|
|
311
|
+
)
|
|
312
|
+
top = job.get("top")
|
|
313
|
+
if not isinstance(top, str) or not _TOP_RE.fullmatch(top):
|
|
314
|
+
raise AdapterError("invalid_command", f"Bad top: {top!r}.")
|
|
315
|
+
timeout = job.get("timeout_s", _TIMEOUT_S)
|
|
316
|
+
if (
|
|
317
|
+
not isinstance(timeout, (int, float))
|
|
318
|
+
or isinstance(timeout, bool)
|
|
319
|
+
or not timeout > 0
|
|
320
|
+
):
|
|
321
|
+
raise AdapterError("invalid_command", f"Bad timeout_s: {timeout!r}.")
|
|
322
|
+
netlist = self._checked_paths(job.get("netlist"), "netlist")
|
|
323
|
+
libs = self._checked_paths(job.get("libs"), "libs")
|
|
324
|
+
sdc = job.get("sdc")
|
|
325
|
+
if not isinstance(sdc, (str, Path)) or not Path(sdc).is_file():
|
|
326
|
+
raise AdapterError("invalid_reference", f"SDC not found: {sdc}.")
|
|
327
|
+
spef = job.get("spef")
|
|
328
|
+
if spef is not None and (
|
|
329
|
+
not isinstance(spef, (str, Path)) or not Path(spef).is_file()
|
|
330
|
+
):
|
|
331
|
+
raise AdapterError("invalid_reference", f"SPEF not found: {spef}.")
|
|
332
|
+
for path in [sdc] + ([spef] if spef is not None else []):
|
|
333
|
+
self._checked_scriptable(str(path))
|
|
334
|
+
sta = self._resolve(job, _TOOL)
|
|
335
|
+
checked = {
|
|
336
|
+
"sta": sta,
|
|
337
|
+
"sta_version": self._version(sta),
|
|
338
|
+
"netlist": netlist,
|
|
339
|
+
"libs": libs,
|
|
340
|
+
"sdc": str(sdc),
|
|
341
|
+
"spef": str(spef) if spef is not None else None,
|
|
342
|
+
"top": top,
|
|
343
|
+
"timeout_s": timeout,
|
|
344
|
+
}
|
|
345
|
+
if self._runner is not None:
|
|
346
|
+
try:
|
|
347
|
+
self._runner.preflight()
|
|
348
|
+
except ExecutionError as exc:
|
|
349
|
+
raise AdapterError(exc.code, str(exc)) from exc
|
|
350
|
+
return checked
|
|
351
|
+
|
|
352
|
+
def execute(self, job):
|
|
353
|
+
"""Run the timing script under sta with the job timeout.
|
|
354
|
+
|
|
355
|
+
Writes timing.tcl into the workdir, runs it (stdin closed
|
|
356
|
+
so sta can never linger at its prompt), and returns the
|
|
357
|
+
captured log plus the run inputs. A failed run still
|
|
358
|
+
returns its log — the verdict comes from parse, never the
|
|
359
|
+
exit code alone.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
job: Solver job with operation run_timing plus netlist,
|
|
363
|
+
libs, sdc, and optional spef/top/timeout inputs.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Native output with exit_code, stdout/stderr, log,
|
|
367
|
+
top, netlist, libs, sdc, spef, and the sta version.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
AdapterError: invalid_reference for a missing workdir,
|
|
371
|
+
runner_unavailable on an STA timeout, plus the
|
|
372
|
+
preflight codes (execute re-checks first).
|
|
373
|
+
"""
|
|
374
|
+
checked = self.preflight(job)
|
|
375
|
+
workdir = job.get("workdir", ".")
|
|
376
|
+
if not Path(workdir).is_dir():
|
|
377
|
+
raise AdapterError(
|
|
378
|
+
"invalid_reference", f"Workdir not found: {workdir}."
|
|
379
|
+
)
|
|
380
|
+
(Path(workdir) / _SCRIPT_NAME).write_text(self._script(checked))
|
|
381
|
+
roots = tool_roots([checked["sta"]])
|
|
382
|
+
proc = self._run(
|
|
383
|
+
[checked["sta"], _SCRIPT_NAME],
|
|
384
|
+
workdir,
|
|
385
|
+
checked["timeout_s"],
|
|
386
|
+
roots,
|
|
387
|
+
)
|
|
388
|
+
log = proc.stdout or ""
|
|
389
|
+
(Path(workdir) / _LOG_NAME).write_text(log)
|
|
390
|
+
return {
|
|
391
|
+
"exit_code": proc.returncode,
|
|
392
|
+
"stdout": proc.stdout,
|
|
393
|
+
"stderr": proc.stderr,
|
|
394
|
+
"log": log,
|
|
395
|
+
"top": checked["top"],
|
|
396
|
+
"netlist": list(checked["netlist"]),
|
|
397
|
+
"libs": list(checked["libs"]),
|
|
398
|
+
"sdc": checked["sdc"],
|
|
399
|
+
"spef": checked["spef"],
|
|
400
|
+
"sta_version": checked["sta_version"],
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
def parse(self, native_output):
|
|
404
|
+
"""Parse margins, paths, violations, and endpoint evidence.
|
|
405
|
+
|
|
406
|
+
Failed runs (nonzero exit or any `Error:` line) surface
|
|
407
|
+
their errors plus warnings with no measurements — a failed
|
|
408
|
+
run never parses as clean. Clean runs require all four
|
|
409
|
+
WNS/TNS margins; violated paths become setup/hold error
|
|
410
|
+
findings, constraint VIOLATED lines become errors, and
|
|
411
|
+
warnings (including unconstrained endpoints) never fail
|
|
412
|
+
alone. Raw check_setup text always rides along so missing
|
|
413
|
+
constraints stay visible however the parse lands.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
native_output: Native output with exit_code and log
|
|
417
|
+
text.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
{total, violations[], measurements[]} plus
|
|
421
|
+
critical_paths, unconstrained_endpoints, the four
|
|
422
|
+
margins, check_setup/constraints raw text, top,
|
|
423
|
+
netlist, libs, sdc, spef, and sta_version evidence.
|
|
424
|
+
|
|
425
|
+
Raises:
|
|
426
|
+
AdapterError: runner_unavailable when the output has no
|
|
427
|
+
exit code, or a clean run left no parseable
|
|
428
|
+
WNS/TNS verdict — unparseable never parses as
|
|
429
|
+
clean.
|
|
430
|
+
"""
|
|
431
|
+
exit_code = native_output.get("exit_code")
|
|
432
|
+
if exit_code is None:
|
|
433
|
+
raise AdapterError(
|
|
434
|
+
"runner_unavailable", "Native output has no exit code."
|
|
435
|
+
)
|
|
436
|
+
text = "\n".join(
|
|
437
|
+
native_output.get(key, "") or ""
|
|
438
|
+
for key in ("log", "stdout", "stderr")
|
|
439
|
+
)
|
|
440
|
+
errors = _parse_errors(text)
|
|
441
|
+
warnings = _parse_warnings(text)
|
|
442
|
+
sections = _parse_sections(text)
|
|
443
|
+
paths = _parse_paths(sections.get("REPORT_CHECKS", ""))
|
|
444
|
+
unconstrained = [
|
|
445
|
+
v["message"] for v in warnings if v["rule"] == "unconstrained_endpoint"
|
|
446
|
+
]
|
|
447
|
+
evidence = {
|
|
448
|
+
"critical_paths": paths,
|
|
449
|
+
"unconstrained_endpoints": unconstrained,
|
|
450
|
+
"check_setup": sections.get("CHECK_SETUP", ""),
|
|
451
|
+
"constraints": sections.get("CONSTRAINTS", ""),
|
|
452
|
+
"top": native_output.get("top"),
|
|
453
|
+
"netlist": native_output.get("netlist"),
|
|
454
|
+
"libs": native_output.get("libs"),
|
|
455
|
+
"sdc": native_output.get("sdc"),
|
|
456
|
+
"spef": native_output.get("spef"),
|
|
457
|
+
"sta_version": native_output.get("sta_version"),
|
|
458
|
+
}
|
|
459
|
+
if exit_code != 0 or errors:
|
|
460
|
+
if exit_code != 0 and not errors:
|
|
461
|
+
# Defensive: a failed run must never parse as clean.
|
|
462
|
+
errors = [
|
|
463
|
+
{
|
|
464
|
+
"rule": "sta_error",
|
|
465
|
+
"severity": "error",
|
|
466
|
+
"message": f"sta exited {exit_code}",
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
violations = errors + warnings
|
|
470
|
+
return {
|
|
471
|
+
"total": len(violations),
|
|
472
|
+
"violations": violations,
|
|
473
|
+
"measurements": [],
|
|
474
|
+
**evidence,
|
|
475
|
+
}
|
|
476
|
+
margins = {
|
|
477
|
+
"wns_setup": _parse_margin(sections.get("WNS_MAX", "")),
|
|
478
|
+
"tns_setup": _parse_margin(sections.get("TNS_MAX", "")),
|
|
479
|
+
"wns_hold": _parse_margin(sections.get("WNS_MIN", "")),
|
|
480
|
+
"tns_hold": _parse_margin(sections.get("TNS_MIN", "")),
|
|
481
|
+
}
|
|
482
|
+
missing = sorted(name for name, value in margins.items() if value is None)
|
|
483
|
+
if missing:
|
|
484
|
+
raise AdapterError(
|
|
485
|
+
"runner_unavailable",
|
|
486
|
+
f"sta output has no parseable verdict: {', '.join(missing)}.",
|
|
487
|
+
)
|
|
488
|
+
violations = warnings + _parse_constraint_violations(
|
|
489
|
+
sections.get("CONSTRAINTS", "")
|
|
490
|
+
)
|
|
491
|
+
for path in paths:
|
|
492
|
+
if not path["violated"]:
|
|
493
|
+
continue
|
|
494
|
+
rule = "setup_violation" if path["type"] == "max" else "hold_violation"
|
|
495
|
+
violations.append(
|
|
496
|
+
{
|
|
497
|
+
"rule": rule,
|
|
498
|
+
"severity": "error",
|
|
499
|
+
"message": (
|
|
500
|
+
f"{path['type']} path {path['startpoint']} -> "
|
|
501
|
+
f"{path['endpoint']} slack {path['slack']} ns"
|
|
502
|
+
+ (f" ({path['group']})" if path["group"] else "")
|
|
503
|
+
),
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
violated = sum(1 for path in paths if path["violated"])
|
|
507
|
+
measurements = [
|
|
508
|
+
{"name": name, "value": value, "unit": "ns"}
|
|
509
|
+
for name, value in margins.items()
|
|
510
|
+
]
|
|
511
|
+
measurements.extend(
|
|
512
|
+
[
|
|
513
|
+
{
|
|
514
|
+
"name": "critical_path_count",
|
|
515
|
+
"value": len(paths),
|
|
516
|
+
"unit": "count",
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"name": "violated_path_count",
|
|
520
|
+
"value": violated,
|
|
521
|
+
"unit": "count",
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"name": "unconstrained_count",
|
|
525
|
+
"value": len(unconstrained),
|
|
526
|
+
"unit": "count",
|
|
527
|
+
},
|
|
528
|
+
]
|
|
529
|
+
)
|
|
530
|
+
return {
|
|
531
|
+
"total": len(violations),
|
|
532
|
+
"violations": violations,
|
|
533
|
+
"measurements": measurements,
|
|
534
|
+
**margins,
|
|
535
|
+
**evidence,
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
@staticmethod
|
|
539
|
+
def _checked_paths(value, key):
|
|
540
|
+
"""Require a non-empty list of existing, scriptable paths.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
value: Job value for the key, expected to be a list of
|
|
544
|
+
paths.
|
|
545
|
+
key: Job key name for error messages.
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
The paths as plain strings.
|
|
549
|
+
|
|
550
|
+
Raises:
|
|
551
|
+
AdapterError: invalid_reference when the value is not a
|
|
552
|
+
non-empty list, a path is missing, or a path is
|
|
553
|
+
unsafe for the TCL script.
|
|
554
|
+
"""
|
|
555
|
+
if not isinstance(value, list) or not value:
|
|
556
|
+
raise AdapterError(
|
|
557
|
+
"invalid_reference", f"{key} must be a non-empty list: {value!r}."
|
|
558
|
+
)
|
|
559
|
+
missing = [
|
|
560
|
+
item
|
|
561
|
+
for item in value
|
|
562
|
+
if not isinstance(item, (str, Path)) or not Path(item).is_file()
|
|
563
|
+
]
|
|
564
|
+
if missing:
|
|
565
|
+
raise AdapterError("invalid_reference", f"{key} not found: {missing}.")
|
|
566
|
+
for item in value:
|
|
567
|
+
OpenStaTimingAdapter._checked_scriptable(str(item))
|
|
568
|
+
return [str(item) for item in value]
|
|
569
|
+
|
|
570
|
+
@staticmethod
|
|
571
|
+
def _checked_scriptable(path):
|
|
572
|
+
"""Refuse paths that brace-quoting cannot carry safely.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
path: Filesystem path destined for the TCL script.
|
|
576
|
+
|
|
577
|
+
Raises:
|
|
578
|
+
AdapterError: invalid_reference when the path holds
|
|
579
|
+
braces or newlines (TCL injection surface).
|
|
580
|
+
"""
|
|
581
|
+
if any(char in path for char in ("{", "}", "\n", "\r")):
|
|
582
|
+
raise AdapterError(
|
|
583
|
+
"invalid_reference", f"Path unsafe for the TCL script: {path}."
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
@staticmethod
|
|
587
|
+
def _script(checked):
|
|
588
|
+
"""Render the sta timing script for the checked job.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
checked: Preflight mapping with netlist, libs, sdc,
|
|
592
|
+
spef, and top.
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
TCL script text (read, link, constrain, report, exit).
|
|
596
|
+
"""
|
|
597
|
+
lines = [f"read_liberty {_quote(lib)}" for lib in checked["libs"]]
|
|
598
|
+
lines += [f"read_verilog {_quote(net)}" for net in checked["netlist"]]
|
|
599
|
+
lines.append(f"link_design {checked['top']}")
|
|
600
|
+
if checked["spef"] is not None:
|
|
601
|
+
lines.append(f"read_spef {_quote(checked['spef'])}")
|
|
602
|
+
lines.append(f"read_sdc {_quote(checked['sdc'])}")
|
|
603
|
+
reports = [
|
|
604
|
+
(
|
|
605
|
+
"REPORT_CHECKS",
|
|
606
|
+
"report_checks -path_delay min_max -format full_clock_expanded "
|
|
607
|
+
"-fields {slew cap} -group_count 50",
|
|
608
|
+
),
|
|
609
|
+
("WNS_MAX", "report_wns -max"),
|
|
610
|
+
("TNS_MAX", "report_tns -max"),
|
|
611
|
+
("WNS_MIN", "report_wns -min"),
|
|
612
|
+
("TNS_MIN", "report_tns -min"),
|
|
613
|
+
# report_check_types (bare): slew/load/width tables whose
|
|
614
|
+
# violated rows carry the VIOLATED marker the parse keys
|
|
615
|
+
# on. Pinned OpenSTA has no report_constraint command.
|
|
616
|
+
("CONSTRAINTS", "report_check_types"),
|
|
617
|
+
# -verbose names the offending pins on the lines after
|
|
618
|
+
# each warning; bare check_setup only counts them.
|
|
619
|
+
("CHECK_SETUP", "check_setup -verbose"),
|
|
620
|
+
]
|
|
621
|
+
for name, command in reports:
|
|
622
|
+
lines.append(f"puts {{===== TIMING_STA {name} =====}}")
|
|
623
|
+
lines.append(command)
|
|
624
|
+
lines.append("exit")
|
|
625
|
+
return "\n".join(lines) + "\n"
|
|
626
|
+
|
|
627
|
+
@staticmethod
|
|
628
|
+
def _resolve(job, binary):
|
|
629
|
+
tool_dir = job.get("tool_dir")
|
|
630
|
+
if tool_dir:
|
|
631
|
+
candidate = Path(tool_dir) / binary
|
|
632
|
+
if candidate.is_file():
|
|
633
|
+
return str(candidate)
|
|
634
|
+
raise AdapterError(
|
|
635
|
+
"missing_dependency",
|
|
636
|
+
f"{binary} not found in {tool_dir} — install the pinned "
|
|
637
|
+
"OpenSTA build via scripts/provision_toolchain.sh --install "
|
|
638
|
+
"(PROV phase 1; see docs/work/PROV/report.md).",
|
|
639
|
+
)
|
|
640
|
+
prefix = os.environ.get("OPENSTA_PREFIX")
|
|
641
|
+
if prefix:
|
|
642
|
+
candidate = Path(prefix) / "bin" / binary
|
|
643
|
+
if candidate.is_file():
|
|
644
|
+
return str(candidate)
|
|
645
|
+
raise AdapterError(
|
|
646
|
+
"missing_dependency",
|
|
647
|
+
f"{binary} not found under $OPENSTA_PREFIX={prefix} — rebuild "
|
|
648
|
+
"the pinned OpenSTA build via "
|
|
649
|
+
"scripts/provision_toolchain.sh --install (PROV phase 1; "
|
|
650
|
+
"see docs/work/PROV/report.md).",
|
|
651
|
+
)
|
|
652
|
+
found = shutil.which(binary)
|
|
653
|
+
if found is None:
|
|
654
|
+
raise AdapterError(
|
|
655
|
+
"missing_dependency",
|
|
656
|
+
f"{binary} not on PATH and $OPENSTA_PREFIX unset — install "
|
|
657
|
+
"the pinned OpenSTA build via "
|
|
658
|
+
"scripts/provision_toolchain.sh --install (PROV phase 1; "
|
|
659
|
+
"see docs/work/PROV/report.md) or pass tool_dir.",
|
|
660
|
+
)
|
|
661
|
+
return found
|
|
662
|
+
|
|
663
|
+
@staticmethod
|
|
664
|
+
def _version(sta):
|
|
665
|
+
"""Assert the binary answers with a parseable OpenSTA banner.
|
|
666
|
+
|
|
667
|
+
Probes `-version` first, then `--version` (same fallback the
|
|
668
|
+
PROV provisioner uses).
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
sta: Resolved sta binary path.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
OpenSTA banner line (kept as evidence).
|
|
675
|
+
|
|
676
|
+
Raises:
|
|
677
|
+
AdapterError: missing_dependency when the probes fail,
|
|
678
|
+
time out, or answer no OpenSTA banner.
|
|
679
|
+
"""
|
|
680
|
+
for flag in ("-version", "--version"):
|
|
681
|
+
try:
|
|
682
|
+
proc = subprocess.run(
|
|
683
|
+
[sta, flag],
|
|
684
|
+
capture_output=True,
|
|
685
|
+
text=True,
|
|
686
|
+
timeout=_VERSION_TIMEOUT_S,
|
|
687
|
+
)
|
|
688
|
+
except subprocess.TimeoutExpired as exc:
|
|
689
|
+
raise AdapterError(
|
|
690
|
+
"missing_dependency",
|
|
691
|
+
f"sta version probe timed out at {sta}.",
|
|
692
|
+
) from exc
|
|
693
|
+
banner = (proc.stdout + "\n" + proc.stderr).strip().splitlines()
|
|
694
|
+
versioned = next(
|
|
695
|
+
(line for line in banner
|
|
696
|
+
if _VERSION_RE.search(line)
|
|
697
|
+
or _BARE_VERSION_RE.search(line.strip())), None
|
|
698
|
+
)
|
|
699
|
+
if proc.returncode == 0 and versioned is not None:
|
|
700
|
+
return versioned.strip()
|
|
701
|
+
raise AdapterError(
|
|
702
|
+
"missing_dependency",
|
|
703
|
+
f"sta at {sta} answered no OpenSTA banner — install the "
|
|
704
|
+
"pinned OpenSTA build via scripts/provision_toolchain.sh "
|
|
705
|
+
"--install (PROV phase 1; see docs/work/PROV/report.md).",
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
def _run(self, argv, workdir, timeout_s, extra_binds=()):
|
|
709
|
+
if self._runner is not None:
|
|
710
|
+
try:
|
|
711
|
+
return self._runner.run(argv, workdir, timeout_s, extra_binds)
|
|
712
|
+
except ExecutionError as exc:
|
|
713
|
+
raise AdapterError(exc.code, str(exc)) from exc
|
|
714
|
+
try:
|
|
715
|
+
return subprocess.run(
|
|
716
|
+
argv,
|
|
717
|
+
capture_output=True,
|
|
718
|
+
text=True,
|
|
719
|
+
timeout=timeout_s,
|
|
720
|
+
cwd=workdir,
|
|
721
|
+
stdin=subprocess.DEVNULL,
|
|
722
|
+
)
|
|
723
|
+
except subprocess.TimeoutExpired as exc:
|
|
724
|
+
raise AdapterError(
|
|
725
|
+
"runner_unavailable", f"timing timed out after {timeout_s}s."
|
|
726
|
+
) from exc
|