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,1121 @@
|
|
|
1
|
+
"""LibreLane digital-flow solver adapter: run_digital_flow behind the ToolAdapter port.
|
|
2
|
+
|
|
3
|
+
Protocol (LibreLane 3.0.17 Classic, subprocess): stage the project RTL
|
|
4
|
+
into a design dir (`src/` plus a generated `digital_flow_config.json`
|
|
5
|
+
carrying DESIGN_NAME/VERILOG_FILES/CLOCK_PERIOD/CLOCK_PORT/PDK), run
|
|
6
|
+
`librelane <config> --run-tag <tag> --pdk <pdk> --overwrite`
|
|
7
|
+
(`--pdk-root`/`--scl` when the job names them) under the job timeout in
|
|
8
|
+
its own process group, then read `design/runs/<tag>/` step dirs for
|
|
9
|
+
per-stage machine files (state_out.json metrics plus state views,
|
|
10
|
+
metrics.csv, the signoff drc.klayout.json/lvs.netgen.json reports).
|
|
11
|
+
Every gate verdict comes from those machine files only: D11 passes on
|
|
12
|
+
the staged config plus the versioned binary, D12 on synthesis state
|
|
13
|
+
files, D22/D25/D26/D27 on merged step metrics (plus the signoff DRC
|
|
14
|
+
total when step metrics carry no DRC keys), D24 on a non-empty state
|
|
15
|
+
view GDS, the rest on per-step state files. Each gate otherwise fails
|
|
16
|
+
loudly naming the stage plus the report, or is force-bypassed via the
|
|
17
|
+
job `force` list (a warning, never silent). Report text and file names
|
|
18
|
+
ride as evidence pointers only and never decide; steps without machine
|
|
19
|
+
files fail loudly. A nonzero exit marks the first failing gate failed
|
|
20
|
+
and everything downstream not_run; a timeout SIGKILLs the whole tree
|
|
21
|
+
and reports digital_flow_timeout with whatever partial evidence
|
|
22
|
+
survived.
|
|
23
|
+
|
|
24
|
+
Tool resolution: an explicit job `tool_dir` wins, else PATH, and the
|
|
25
|
+
resolved binary must answer `librelane --version` with the pinned
|
|
26
|
+
3.0.17 banner — ambient installs are kept and version-asserted, never
|
|
27
|
+
reinstalled. Where the tools live on disk is B10/PROV scope.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import shutil
|
|
34
|
+
import signal
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from ...execution.errors import ExecutionError
|
|
40
|
+
from ...execution.runner import tool_roots
|
|
41
|
+
from ...interfaces import ToolAdapter
|
|
42
|
+
from ..errors import AdapterError
|
|
43
|
+
from .state import (
|
|
44
|
+
drc_verdict,
|
|
45
|
+
equiv_verdict,
|
|
46
|
+
lvs_verdict,
|
|
47
|
+
missing_reason,
|
|
48
|
+
read_run_states,
|
|
49
|
+
signoff_drc_total,
|
|
50
|
+
signoff_lvs_report,
|
|
51
|
+
sta_verdict,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
OPERATION = "run_digital_flow"
|
|
55
|
+
_TOOL = "librelane"
|
|
56
|
+
_FLOW = "Classic"
|
|
57
|
+
_PIN = "3.0.17"
|
|
58
|
+
_TIMEOUT_S = 3600
|
|
59
|
+
_VERSION_TIMEOUT_S = 30
|
|
60
|
+
|
|
61
|
+
_CONFIG_NAME = "digital_flow_config.json"
|
|
62
|
+
_LOG_NAME = "digital_flow.log"
|
|
63
|
+
_DESIGN_DIR = "design"
|
|
64
|
+
_SRC_DIR = "src"
|
|
65
|
+
_RUNS_DIR = "runs"
|
|
66
|
+
|
|
67
|
+
_MISSING_REMEDY = (
|
|
68
|
+
"install the pinned LibreLane 3.0.17 supported distribution (run "
|
|
69
|
+
"scripts/provision_toolchain.sh --install for the pinned AppImage per "
|
|
70
|
+
"the upstream install docs); inside a nix-shell env, `librelane "
|
|
71
|
+
"--version` must report 3.0.17 (see "
|
|
72
|
+
"src/interfaces/setup_catalogue.yaml id librelane)"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
_TOP_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
76
|
+
_TAG_RE = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_.-]*")
|
|
77
|
+
_PDK_RE = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_.-]*")
|
|
78
|
+
|
|
79
|
+
# PRD D11-D27 projected onto LibreLane Classic step dirs: lowercase
|
|
80
|
+
# substrings matched against the step-dir name (first path part under
|
|
81
|
+
# runs/<tag>/). The table locates steps only — verdicts never come
|
|
82
|
+
# from it. Step IDs drift across LibreLane versions; the locator stays
|
|
83
|
+
# substring on purpose, and a gate whose steps wrote no machine files
|
|
84
|
+
# fails loudly (never skipped). The artifact tuples select evidence
|
|
85
|
+
# pointers (plus filename fallbacks behind state views), never
|
|
86
|
+
# verdicts.
|
|
87
|
+
_STAGES = (
|
|
88
|
+
{"id": "D11", "label": "Start the LibreLane flow", "steps": (), "artifacts": ()},
|
|
89
|
+
{"id": "D12", "label": "Synthesize to standard cells",
|
|
90
|
+
"steps": ("synthesis",), "artifacts": (".v", "stat")},
|
|
91
|
+
{"id": "D13", "label": "Pre-placement constraint and timing check",
|
|
92
|
+
"steps": ("sdc", "constraint", "staprepnr"), "artifacts": (".rpt", ".sdc", ".log")},
|
|
93
|
+
{"id": "D14", "label": "Floorplan", "steps": ("floorplan",),
|
|
94
|
+
"artifacts": (".odb", ".def")},
|
|
95
|
+
{"id": "D15", "label": "Power grid", "steps": ("tapendcap", "pdn", "powergrid"),
|
|
96
|
+
"artifacts": (".def", ".rpt")},
|
|
97
|
+
{"id": "D16", "label": "Placement", "steps": ("placement", "repairdesign"),
|
|
98
|
+
"artifacts": (".odb", ".def", ".rpt")},
|
|
99
|
+
{"id": "D17", "label": "Clock tree", "steps": ("cts", "clocktree"),
|
|
100
|
+
"artifacts": (".odb", ".rpt", ".log")},
|
|
101
|
+
{"id": "D18", "label": "Global routing", "steps": ("globalrout", "groute"),
|
|
102
|
+
"artifacts": (".guide", ".rpt")},
|
|
103
|
+
{"id": "D19", "label": "Detail routing",
|
|
104
|
+
"steps": ("detailedrout", "detailroute", "droute", "antenna"),
|
|
105
|
+
"artifacts": (".odb", ".def", ".rpt")},
|
|
106
|
+
{"id": "D20", "label": "Filler and density",
|
|
107
|
+
"steps": ("filler", "densityfill", "fill"), "artifacts": (".odb", ".def")},
|
|
108
|
+
{"id": "D21", "label": "RC extraction", "steps": ("rcx", "parasitic"),
|
|
109
|
+
"artifacts": (".spef",)},
|
|
110
|
+
{"id": "D22", "label": "Post-PNR STA", "steps": ("stapostpnr", "stamidpnr"),
|
|
111
|
+
"artifacts": (".rpt",)},
|
|
112
|
+
{"id": "D23", "label": "Power and IR drop", "steps": ("irdrop", "power", "redist"),
|
|
113
|
+
"artifacts": (".rpt",)},
|
|
114
|
+
{"id": "D24", "label": "Stream GDS/LEF views",
|
|
115
|
+
"steps": ("writegds", "writeviews", "streamout", "gds"),
|
|
116
|
+
"artifacts": (".gds", ".lef")},
|
|
117
|
+
{"id": "D25", "label": "Foundry-deck DRC", "steps": ("drc", "cvc"),
|
|
118
|
+
"artifacts": (".rpt", ".lyrdb")},
|
|
119
|
+
{"id": "D26", "label": "LVS vs reference", "steps": ("lvs", "netgen", "extract"),
|
|
120
|
+
"artifacts": (".out", ".rpt", ".spice")},
|
|
121
|
+
{"id": "D27", "label": "Equivalence and final gates",
|
|
122
|
+
"steps": ("eqy", "equiv", "signoff", "finish"), "artifacts": (".rpt", ".log")},
|
|
123
|
+
)
|
|
124
|
+
_STAGE_IDS = tuple(stage["id"] for stage in _STAGES)
|
|
125
|
+
|
|
126
|
+
_EVIDENCE_CAP = 25
|
|
127
|
+
_STREAM_KEEP_BYTES = 131072
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _scan_run(run_dir):
|
|
131
|
+
"""Map step-dir names to their files under one run root.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
run_dir: runs/<tag>/ dir path, or None when the flow never
|
|
135
|
+
created it.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Step-dir name to sorted file-path list (empty when no run).
|
|
139
|
+
"""
|
|
140
|
+
found = {}
|
|
141
|
+
if run_dir is None or not Path(run_dir).is_dir():
|
|
142
|
+
return found
|
|
143
|
+
root = Path(run_dir)
|
|
144
|
+
for path in sorted(root.rglob("*")):
|
|
145
|
+
if not path.is_file():
|
|
146
|
+
continue
|
|
147
|
+
try:
|
|
148
|
+
rel = path.relative_to(root)
|
|
149
|
+
except ValueError:
|
|
150
|
+
continue
|
|
151
|
+
if len(rel.parts) < 2:
|
|
152
|
+
continue
|
|
153
|
+
found.setdefault(rel.parts[0], []).append(path)
|
|
154
|
+
return found
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _match_steps(step_names, needles):
|
|
158
|
+
"""Step-dir names containing any needle (case-insensitive).
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
step_names: Step-dir names from the run scan.
|
|
162
|
+
needles: Lowercase substrings to match.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Sorted matching step-dir names.
|
|
166
|
+
"""
|
|
167
|
+
return sorted(name for name in step_names if any(n in name.lower() for n in needles))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _evidence_files(scan, step_names, artifacts):
|
|
171
|
+
"""Artifact files under the matched step dirs, capped.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
scan: Step-dir to file-list mapping from the run scan.
|
|
175
|
+
step_names: Matched step-dir names for one stage.
|
|
176
|
+
artifacts: Lowercase filename substrings for the stage.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Sorted matching file paths, capped at _EVIDENCE_CAP.
|
|
180
|
+
"""
|
|
181
|
+
out = []
|
|
182
|
+
for name in step_names:
|
|
183
|
+
for path in scan.get(name, []):
|
|
184
|
+
lowered = path.name.lower()
|
|
185
|
+
if any(a in lowered for a in artifacts):
|
|
186
|
+
out.append(path)
|
|
187
|
+
return sorted(out)[:_EVIDENCE_CAP]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _tail_kept(text):
|
|
191
|
+
"""Bound one captured stream to its tail with a marker.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
text: Full captured stream text.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Tail text, marked when truncated.
|
|
198
|
+
"""
|
|
199
|
+
encoded = text.encode("utf-8", "replace")
|
|
200
|
+
if len(encoded) <= _STREAM_KEEP_BYTES:
|
|
201
|
+
return text
|
|
202
|
+
tail = encoded[-_STREAM_KEEP_BYTES:].decode("utf-8", "replace")
|
|
203
|
+
return f"...[truncated to the last {_STREAM_KEEP_BYTES} bytes]\n{tail}"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _step_metrics(states, step_names):
|
|
207
|
+
"""Merge machine metrics across one gate's matched step dirs.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
states: Step-dir to {metrics, files, views} mapping from the
|
|
211
|
+
run states readout.
|
|
212
|
+
step_names: Matched step-dir names for one stage.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
(metrics, files): merged metric mapping plus sorted state
|
|
216
|
+
file paths.
|
|
217
|
+
"""
|
|
218
|
+
metrics = {}
|
|
219
|
+
files = []
|
|
220
|
+
for name in step_names:
|
|
221
|
+
entry = states.get(name, {})
|
|
222
|
+
for key, value in entry.get("metrics", {}).items():
|
|
223
|
+
metrics.setdefault(key, value)
|
|
224
|
+
files.extend(entry.get("files", []))
|
|
225
|
+
return metrics, sorted(set(files))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _step_views(states, step_names, suffixes):
|
|
229
|
+
"""State view paths under one gate's matched step dirs, filtered.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
states: Step-dir to {metrics, files, views} mapping from the
|
|
233
|
+
run states readout.
|
|
234
|
+
step_names: Matched step-dir names for one stage.
|
|
235
|
+
suffixes: Filename suffixes a view must end with (lowercase
|
|
236
|
+
compared, so pass tuples like (".gds", ".gds.gz")).
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Sorted view paths ending with one of the suffixes.
|
|
240
|
+
"""
|
|
241
|
+
found = []
|
|
242
|
+
for name in step_names:
|
|
243
|
+
for view in states.get(name, {}).get("views", []):
|
|
244
|
+
if view.lower().endswith(suffixes):
|
|
245
|
+
found.append(view)
|
|
246
|
+
return sorted(set(found))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _machine_gate(stage_id, kind, steps, metrics, files, run_dir):
|
|
250
|
+
"""Judge one machine-gated stage from its merged metrics.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
stage_id: Gate id (D22/D25/D26/D27).
|
|
254
|
+
kind: Metric kind (STA/DRC/LVS/equivalence).
|
|
255
|
+
steps: Matched step-dir names for the stage.
|
|
256
|
+
metrics: Merged machine metrics across those steps.
|
|
257
|
+
files: State file paths backing the metrics.
|
|
258
|
+
run_dir: Run dir label for missing-evidence reasons.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
(ok, reason, evidence, reports) for the gate.
|
|
262
|
+
"""
|
|
263
|
+
if not files:
|
|
264
|
+
if not steps:
|
|
265
|
+
return (
|
|
266
|
+
False,
|
|
267
|
+
f"no {kind} machine-file metrics "
|
|
268
|
+
f"(state_out.json/metrics.csv) in {run_dir}; "
|
|
269
|
+
"keys present: none",
|
|
270
|
+
[],
|
|
271
|
+
[],
|
|
272
|
+
)
|
|
273
|
+
return (
|
|
274
|
+
False,
|
|
275
|
+
missing_reason(stage_id, kind, metrics),
|
|
276
|
+
[],
|
|
277
|
+
[],
|
|
278
|
+
)
|
|
279
|
+
if kind == "STA":
|
|
280
|
+
ok, detail = sta_verdict(metrics)
|
|
281
|
+
elif kind == "DRC":
|
|
282
|
+
ok, detail, _ = drc_verdict(metrics)
|
|
283
|
+
elif kind == "LVS":
|
|
284
|
+
ok, detail, _ = lvs_verdict(metrics)
|
|
285
|
+
else:
|
|
286
|
+
ok, detail, _ = equiv_verdict(metrics)
|
|
287
|
+
if ok is None:
|
|
288
|
+
return False, missing_reason(stage_id, kind, metrics), [], files
|
|
289
|
+
return bool(ok), detail, files, files
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _stage_gate(stage, scan, native, states):
|
|
293
|
+
"""Judge one stage gate from machine files only (D11 excepted).
|
|
294
|
+
|
|
295
|
+
D11 passes on the staged config plus the versioned binary (no run
|
|
296
|
+
dirs needed); every other gate judges its matched steps' machine
|
|
297
|
+
files — D12 on synthesis state files, D22/D25/D26/D27 on merged
|
|
298
|
+
state_out.json / metrics.csv metrics (plus the signoff DRC total
|
|
299
|
+
when step metrics carry no DRC keys), D24 on a non-empty state
|
|
300
|
+
view GDS, the rest on per-step state files. Report text and file
|
|
301
|
+
names ride as evidence pointers only and never decide; steps
|
|
302
|
+
without machine files fail loudly.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
stage: Stage row (id, label, steps, artifacts).
|
|
306
|
+
scan: Step-dir to file-list mapping from the run scan.
|
|
307
|
+
native: Native output with config, version, and run context.
|
|
308
|
+
states: Step-dir to {metrics, files, views} machine readout.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
(ok, reason, steps, evidence, reports) for the gate.
|
|
312
|
+
"""
|
|
313
|
+
stage_id = stage["id"]
|
|
314
|
+
run_dir = native.get("run_dir") or "run dir"
|
|
315
|
+
if stage_id == "D11":
|
|
316
|
+
config = native.get("config")
|
|
317
|
+
version = native.get("librelane_version") or ""
|
|
318
|
+
if config is not None and Path(config).is_file() and version:
|
|
319
|
+
count = len(scan)
|
|
320
|
+
evidence = [config]
|
|
321
|
+
return True, f"flow {native.get('flow')}, {count} step dirs", [], evidence, evidence
|
|
322
|
+
return False, "no staged flow config", [], [], []
|
|
323
|
+
steps = _match_steps(scan, stage["steps"])
|
|
324
|
+
evidence = _evidence_files(scan, steps, stage["artifacts"])
|
|
325
|
+
reports = [str(p) for p in evidence]
|
|
326
|
+
if stage_id == "D12":
|
|
327
|
+
_, files = _step_metrics(states, steps)
|
|
328
|
+
if not files:
|
|
329
|
+
logs = sorted(
|
|
330
|
+
str(path)
|
|
331
|
+
for name in steps
|
|
332
|
+
for path in scan.get(name, [])
|
|
333
|
+
if path.suffix == ".log"
|
|
334
|
+
)
|
|
335
|
+
return (
|
|
336
|
+
False,
|
|
337
|
+
"no synthesis machine-file metrics "
|
|
338
|
+
f"(state_out.json/metrics.csv) in {run_dir}",
|
|
339
|
+
steps,
|
|
340
|
+
[],
|
|
341
|
+
logs,
|
|
342
|
+
)
|
|
343
|
+
return (
|
|
344
|
+
True,
|
|
345
|
+
f"synthesis complete ({len(files)} state files)",
|
|
346
|
+
steps,
|
|
347
|
+
evidence,
|
|
348
|
+
reports,
|
|
349
|
+
)
|
|
350
|
+
if stage_id == "D22":
|
|
351
|
+
metrics, files = _step_metrics(states, steps)
|
|
352
|
+
ok, reason, shown, shown_reports = _machine_gate(
|
|
353
|
+
stage_id, "STA", steps, metrics, files, run_dir
|
|
354
|
+
)
|
|
355
|
+
return ok, reason, steps, shown, shown_reports
|
|
356
|
+
if stage_id == "D24":
|
|
357
|
+
viewed = _step_views(states, steps, (".gds", ".gds.gz"))
|
|
358
|
+
gds = [Path(p) for p in viewed]
|
|
359
|
+
if not gds:
|
|
360
|
+
gds = [
|
|
361
|
+
p
|
|
362
|
+
for p in evidence
|
|
363
|
+
if p.name.lower().endswith((".gds", ".gds.gz"))
|
|
364
|
+
]
|
|
365
|
+
if not gds:
|
|
366
|
+
return False, f"no GDS evidence in {run_dir}", steps, evidence, reports
|
|
367
|
+
sized = [p for p in gds if p.is_file() and p.stat().st_size > 0]
|
|
368
|
+
if not sized:
|
|
369
|
+
return False, "GDS artifact is empty", steps, evidence, [str(p) for p in gds]
|
|
370
|
+
return True, f"GDS {sized[0].name}", steps, evidence, [str(p) for p in sized]
|
|
371
|
+
if stage_id == "D25":
|
|
372
|
+
metrics, files = _step_metrics(states, steps)
|
|
373
|
+
_, _, total = drc_verdict(metrics)
|
|
374
|
+
if total is None:
|
|
375
|
+
signoff_total, signoff_path = signoff_drc_total(native.get("run_dir"))
|
|
376
|
+
if signoff_total is not None:
|
|
377
|
+
if signoff_total > 0:
|
|
378
|
+
return (
|
|
379
|
+
False,
|
|
380
|
+
f"{signoff_total} DRC violations (drc.klayout.json)",
|
|
381
|
+
steps,
|
|
382
|
+
[signoff_path],
|
|
383
|
+
[signoff_path],
|
|
384
|
+
)
|
|
385
|
+
return (
|
|
386
|
+
True,
|
|
387
|
+
"DRC total 0 (drc.klayout.json)",
|
|
388
|
+
steps,
|
|
389
|
+
[signoff_path],
|
|
390
|
+
[signoff_path],
|
|
391
|
+
)
|
|
392
|
+
ok, reason, shown, shown_reports = _machine_gate(
|
|
393
|
+
stage_id, "DRC", steps, metrics, files, run_dir
|
|
394
|
+
)
|
|
395
|
+
return ok, reason, steps, shown, shown_reports
|
|
396
|
+
if stage_id == "D26":
|
|
397
|
+
metrics, files = _step_metrics(states, steps)
|
|
398
|
+
ok, reason, shown, shown_reports = _machine_gate(
|
|
399
|
+
stage_id, "LVS", steps, metrics, files, run_dir
|
|
400
|
+
)
|
|
401
|
+
lvs_report = signoff_lvs_report(native.get("run_dir"))
|
|
402
|
+
if lvs_report is not None and lvs_report not in shown_reports:
|
|
403
|
+
shown_reports = [*shown_reports, lvs_report]
|
|
404
|
+
return ok, reason, steps, shown, shown_reports
|
|
405
|
+
if stage_id == "D27":
|
|
406
|
+
metrics, files = _step_metrics(states, steps)
|
|
407
|
+
ok, reason, shown, shown_reports = _machine_gate(
|
|
408
|
+
stage_id, "equivalence", steps, metrics, files, run_dir
|
|
409
|
+
)
|
|
410
|
+
return ok, reason, steps, shown, shown_reports
|
|
411
|
+
_, files = _step_metrics(states, steps)
|
|
412
|
+
if not files:
|
|
413
|
+
if not steps:
|
|
414
|
+
return (
|
|
415
|
+
False,
|
|
416
|
+
f"no {stage['label'].lower()} machine-file metrics "
|
|
417
|
+
f"(state_out.json/metrics.csv) in {run_dir}",
|
|
418
|
+
steps,
|
|
419
|
+
[],
|
|
420
|
+
[],
|
|
421
|
+
)
|
|
422
|
+
return (
|
|
423
|
+
False,
|
|
424
|
+
f"no {stage['label'].lower()} machine-file metrics "
|
|
425
|
+
f"(state_out.json/metrics.csv) in {', '.join(steps)}",
|
|
426
|
+
steps,
|
|
427
|
+
[],
|
|
428
|
+
[],
|
|
429
|
+
)
|
|
430
|
+
return (
|
|
431
|
+
True,
|
|
432
|
+
f"{stage['label'].lower()} complete ({len(files)} state files)",
|
|
433
|
+
steps,
|
|
434
|
+
evidence,
|
|
435
|
+
reports,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _evaluate_stages(native):
|
|
440
|
+
"""Judge every D11-D27 gate, applying force-bypass and exit rules.
|
|
441
|
+
|
|
442
|
+
A failed run marks the first failing gate failed and evidence-free
|
|
443
|
+
downstream gates not_run (no violation — they never ran); a clean
|
|
444
|
+
exit turns every missing gate into a failure. Forced failures
|
|
445
|
+
become warnings; force on a passing gate is inert. D27 additionally
|
|
446
|
+
fails as blocked while any earlier gate stays failed.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
native: Native output with exit_code, run scan inputs, the
|
|
450
|
+
forced list, and flow context.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
(stages, violations) with per-stage status plus evidence and
|
|
454
|
+
gate/forced violations.
|
|
455
|
+
"""
|
|
456
|
+
scan = _scan_run(native.get("run_dir"))
|
|
457
|
+
states = read_run_states(native.get("run_dir"))
|
|
458
|
+
forced = set(native.get("forced") or [])
|
|
459
|
+
exit_code = native.get("exit_code")
|
|
460
|
+
failed_run = exit_code is not None and exit_code != 0
|
|
461
|
+
judged = []
|
|
462
|
+
for stage in _STAGES:
|
|
463
|
+
ok, reason, steps, evidence, reports = _stage_gate(
|
|
464
|
+
stage, scan, native, states
|
|
465
|
+
)
|
|
466
|
+
judged.append(
|
|
467
|
+
{
|
|
468
|
+
"id": stage["id"],
|
|
469
|
+
"label": stage["label"],
|
|
470
|
+
"ok": ok,
|
|
471
|
+
"reason": reason,
|
|
472
|
+
"steps": steps,
|
|
473
|
+
"evidence": [str(p) for p in evidence],
|
|
474
|
+
"reports": [str(r) for r in reports],
|
|
475
|
+
}
|
|
476
|
+
)
|
|
477
|
+
blocked = [j["id"] for j in judged[:-1] if not j["ok"] and j["id"] not in forced]
|
|
478
|
+
if blocked:
|
|
479
|
+
final = judged[-1]
|
|
480
|
+
final["ok"] = False
|
|
481
|
+
final["reason"] = f"blocked by {', '.join(blocked)}"
|
|
482
|
+
if not final["reports"]:
|
|
483
|
+
final["reports"] = [native.get("run_dir") or native.get("design_dir", "")]
|
|
484
|
+
first_fail = next((i for i, j in enumerate(judged) if not j["ok"]), None)
|
|
485
|
+
stages = []
|
|
486
|
+
violations = []
|
|
487
|
+
for index, entry in enumerate(judged):
|
|
488
|
+
status = "pass" if entry["ok"] else "fail"
|
|
489
|
+
if failed_run and first_fail is not None and index > first_fail:
|
|
490
|
+
if not entry["steps"] and not entry["evidence"]:
|
|
491
|
+
status = "not_run"
|
|
492
|
+
if status == "fail" and entry["id"] in forced:
|
|
493
|
+
status = "forced"
|
|
494
|
+
stages.append(
|
|
495
|
+
{
|
|
496
|
+
"id": entry["id"],
|
|
497
|
+
"label": entry["label"],
|
|
498
|
+
"status": status,
|
|
499
|
+
"steps": entry["steps"],
|
|
500
|
+
"evidence": entry["evidence"],
|
|
501
|
+
"reports": entry["reports"],
|
|
502
|
+
"reason": entry["reason"],
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
if status == "fail":
|
|
506
|
+
report = entry["reports"][0] if entry["reports"] else native.get("run_dir")
|
|
507
|
+
message = f"{entry['id']} {entry['label']} failed: {entry['reason']}"
|
|
508
|
+
if report:
|
|
509
|
+
message += f" (see {report})"
|
|
510
|
+
violation = {
|
|
511
|
+
"rule": "digital_flow_gate",
|
|
512
|
+
"severity": "error",
|
|
513
|
+
"message": message,
|
|
514
|
+
}
|
|
515
|
+
if report:
|
|
516
|
+
violation["file"] = report
|
|
517
|
+
violations.append(violation)
|
|
518
|
+
elif status == "forced":
|
|
519
|
+
report = entry["reports"][0] if entry["reports"] else native.get("run_dir")
|
|
520
|
+
message = (
|
|
521
|
+
f"{entry['id']} {entry['label']} gate bypassed "
|
|
522
|
+
f"via force: {entry['reason']}"
|
|
523
|
+
)
|
|
524
|
+
if report:
|
|
525
|
+
message += f" (see {report})"
|
|
526
|
+
violation = {
|
|
527
|
+
"rule": "digital_flow_forced",
|
|
528
|
+
"severity": "warning",
|
|
529
|
+
"message": message,
|
|
530
|
+
}
|
|
531
|
+
if report:
|
|
532
|
+
violation["file"] = report
|
|
533
|
+
violations.append(violation)
|
|
534
|
+
if failed_run and not [v for v in violations if v["severity"] == "error"]:
|
|
535
|
+
violations.append(
|
|
536
|
+
{
|
|
537
|
+
"rule": "digital_flow_error",
|
|
538
|
+
"severity": "error",
|
|
539
|
+
"message": f"librelane exited {exit_code} with no failing gate",
|
|
540
|
+
}
|
|
541
|
+
)
|
|
542
|
+
return stages, violations
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class LibrelaneFlowAdapter(ToolAdapter):
|
|
546
|
+
"""run_digital_flow via LibreLane: RTL to verified GDS plus evidence.
|
|
547
|
+
|
|
548
|
+
Serves the run_digital_flow operation with deck.tool `librelane`
|
|
549
|
+
(the only tool). Stages the project RTL into a LibreLane design
|
|
550
|
+
dir, runs the Classic flow as a subprocess under the job timeout,
|
|
551
|
+
and judges the D11-D27 stage gates from the run's machine files
|
|
552
|
+
(state_out.json metrics plus state views, metrics.csv, signoff
|
|
553
|
+
JSON reports): report text never decides, failures name the stage
|
|
554
|
+
plus the report, and the job `force` list bypasses named gates as
|
|
555
|
+
warnings.
|
|
556
|
+
|
|
557
|
+
Job keys:
|
|
558
|
+
sources (required): Verilog source paths staged to design/.
|
|
559
|
+
top (required): Top module name (plain identifier).
|
|
560
|
+
clock_port: Clock port name (default clk; None omits it).
|
|
561
|
+
clock_period: Clock period in ns (default 10.0).
|
|
562
|
+
pdk: PDK name for --pdk (default sky130A).
|
|
563
|
+
pdk_root: PDK root dir for --pdk-root; omitted when None.
|
|
564
|
+
manual_pdk: Pass --manual-pdk (use PDK root as-is, no ciel
|
|
565
|
+
download; required for read-only provisioned PDKs).
|
|
566
|
+
scl: Standard-cell library for --scl; omitted when None.
|
|
567
|
+
flow: Flow name; only Classic is supported.
|
|
568
|
+
tag: Run tag for runs/<tag>/ (default engine).
|
|
569
|
+
force: Stage ids (D11-D27) whose gate failures bypass.
|
|
570
|
+
config: Extra config mapping merged into the flow config.
|
|
571
|
+
timeout_s: Flow budget in seconds (default 3600).
|
|
572
|
+
tool_dir: Directory holding librelane; PATH otherwise.
|
|
573
|
+
deck: Optional mapping; deck.tool must be librelane here.
|
|
574
|
+
workdir: Run directory (default ".").
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
runner: Optional isolated runner; direct subprocess when None.
|
|
578
|
+
|
|
579
|
+
Caution: Overwrites design/, the flow config, and digital_flow.log
|
|
580
|
+
in the workdir; long runs die by timeout_s with partial
|
|
581
|
+
evidence, never a hang — and a failed run never parses as
|
|
582
|
+
clean.
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
def __init__(self, runner=None):
|
|
586
|
+
self._runner = runner
|
|
587
|
+
|
|
588
|
+
def capabilities(self, target):
|
|
589
|
+
"""Report the served operation, tool, flow, and stage gates.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
target: Target the capabilities are reported for
|
|
593
|
+
(accepted, unused — one flow run per job).
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Capability mapping with operations, tool, flow, stages,
|
|
597
|
+
and force-bypass support.
|
|
598
|
+
"""
|
|
599
|
+
return {
|
|
600
|
+
"operations": [OPERATION],
|
|
601
|
+
"tool": _TOOL,
|
|
602
|
+
"flow": _FLOW,
|
|
603
|
+
"stages": list(_STAGE_IDS),
|
|
604
|
+
"force": True,
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
def preflight(self, job):
|
|
608
|
+
"""Check operation, deck, flow inputs, sources, and librelane.
|
|
609
|
+
|
|
610
|
+
Cheap deterministic checks first, tool resolution and its
|
|
611
|
+
version probe last; the resolved binary must report the
|
|
612
|
+
pinned 3.0.17 banner.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
job: Solver job with operation run_digital_flow plus
|
|
616
|
+
sources, top, clock, PDK, tag, force, and config
|
|
617
|
+
inputs.
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
Checked mapping with the resolved librelane path and
|
|
621
|
+
version plus the staged flow inputs.
|
|
622
|
+
|
|
623
|
+
Raises:
|
|
624
|
+
AdapterError: unsupported_operation for another
|
|
625
|
+
operation or a foreign deck.tool, invalid_command
|
|
626
|
+
for bad flow inputs, invalid_reference for missing
|
|
627
|
+
sources or pdk_root, missing_dependency for an
|
|
628
|
+
unresolvable or unpinned librelane.
|
|
629
|
+
"""
|
|
630
|
+
if job.get("operation") != OPERATION:
|
|
631
|
+
raise AdapterError(
|
|
632
|
+
"unsupported_operation",
|
|
633
|
+
f"LibreLane flow handles {OPERATION}, got {job.get('operation')}.",
|
|
634
|
+
)
|
|
635
|
+
deck = job.get("deck")
|
|
636
|
+
if deck is not None:
|
|
637
|
+
if not isinstance(deck, dict):
|
|
638
|
+
raise AdapterError("invalid_command", f"Bad deck: {deck!r}.")
|
|
639
|
+
if deck.get("tool", _TOOL) != _TOOL:
|
|
640
|
+
raise AdapterError(
|
|
641
|
+
"unsupported_operation",
|
|
642
|
+
f"LibreLane flow handles deck.tool {_TOOL}, got {deck.get('tool')}.",
|
|
643
|
+
)
|
|
644
|
+
top = job.get("top")
|
|
645
|
+
if not isinstance(top, str) or not _TOP_RE.fullmatch(top):
|
|
646
|
+
raise AdapterError("invalid_command", f"Bad top: {top!r}.")
|
|
647
|
+
timeout = job.get("timeout_s", _TIMEOUT_S)
|
|
648
|
+
if (
|
|
649
|
+
not isinstance(timeout, (int, float))
|
|
650
|
+
or isinstance(timeout, bool)
|
|
651
|
+
or not timeout > 0
|
|
652
|
+
):
|
|
653
|
+
raise AdapterError("invalid_command", f"Bad timeout_s: {timeout!r}.")
|
|
654
|
+
flow = job.get("flow", _FLOW)
|
|
655
|
+
if flow != _FLOW:
|
|
656
|
+
raise AdapterError(
|
|
657
|
+
"invalid_command", f"Bad flow: {flow!r} (only {_FLOW})."
|
|
658
|
+
)
|
|
659
|
+
tag = job.get("tag", "engine")
|
|
660
|
+
if not isinstance(tag, str) or not _TAG_RE.fullmatch(tag):
|
|
661
|
+
raise AdapterError("invalid_command", f"Bad tag: {tag!r}.")
|
|
662
|
+
pdk = job.get("pdk", "sky130A")
|
|
663
|
+
if not isinstance(pdk, str) or not _PDK_RE.fullmatch(pdk):
|
|
664
|
+
raise AdapterError("invalid_command", f"Bad pdk: {pdk!r}.")
|
|
665
|
+
period = job.get("clock_period", 10.0)
|
|
666
|
+
if (
|
|
667
|
+
not isinstance(period, (int, float))
|
|
668
|
+
or isinstance(period, bool)
|
|
669
|
+
or not period > 0
|
|
670
|
+
):
|
|
671
|
+
raise AdapterError("invalid_command", f"Bad clock_period: {period!r}.")
|
|
672
|
+
port = job.get("clock_port", "clk")
|
|
673
|
+
if port is not None and (
|
|
674
|
+
not isinstance(port, str) or not _TOP_RE.fullmatch(port)
|
|
675
|
+
):
|
|
676
|
+
raise AdapterError("invalid_command", f"Bad clock_port: {port!r}.")
|
|
677
|
+
sources = job.get("sources") or []
|
|
678
|
+
missing = [s for s in sources if not Path(s).is_file()]
|
|
679
|
+
if not sources or missing:
|
|
680
|
+
raise AdapterError(
|
|
681
|
+
"invalid_reference", f"Sources not found: {missing or sources}."
|
|
682
|
+
)
|
|
683
|
+
basenames = [Path(s).name for s in sources]
|
|
684
|
+
dupes = sorted({name for name in basenames if basenames.count(name) > 1})
|
|
685
|
+
if dupes:
|
|
686
|
+
raise AdapterError(
|
|
687
|
+
"invalid_command",
|
|
688
|
+
f"Duplicate source basenames collide in design/src/: {dupes}.",
|
|
689
|
+
)
|
|
690
|
+
force = job.get("force", [])
|
|
691
|
+
if (
|
|
692
|
+
not isinstance(force, list)
|
|
693
|
+
or any(not isinstance(f, str) or f not in _STAGE_IDS for f in force)
|
|
694
|
+
):
|
|
695
|
+
raise AdapterError("invalid_command", f"Bad force: {force!r}.")
|
|
696
|
+
extra = job.get("config", {})
|
|
697
|
+
if not isinstance(extra, dict) or any(not isinstance(k, str) for k in extra):
|
|
698
|
+
raise AdapterError("invalid_command", f"Bad config: {extra!r}.")
|
|
699
|
+
try:
|
|
700
|
+
json.dumps(extra)
|
|
701
|
+
except (TypeError, ValueError) as exc:
|
|
702
|
+
raise AdapterError("invalid_command", f"Bad config: {exc}.") from exc
|
|
703
|
+
pdk_root = job.get("pdk_root")
|
|
704
|
+
if pdk_root is not None and not Path(pdk_root).is_dir():
|
|
705
|
+
raise AdapterError("invalid_reference", f"PDK root not found: {pdk_root}.")
|
|
706
|
+
scl = job.get("scl")
|
|
707
|
+
if scl is not None and (not isinstance(scl, str) or not scl.strip()):
|
|
708
|
+
raise AdapterError("invalid_command", f"Bad scl: {scl!r}.")
|
|
709
|
+
manual_pdk = job.get("manual_pdk", False)
|
|
710
|
+
if not isinstance(manual_pdk, bool):
|
|
711
|
+
raise AdapterError("invalid_command", f"Bad manual_pdk: {manual_pdk!r}.")
|
|
712
|
+
librelane = self._resolve(job)
|
|
713
|
+
checked = {
|
|
714
|
+
"librelane": librelane,
|
|
715
|
+
"librelane_version": self._version(librelane),
|
|
716
|
+
"sources": list(sources),
|
|
717
|
+
"top": top,
|
|
718
|
+
"clock_port": port,
|
|
719
|
+
"clock_period": period,
|
|
720
|
+
"pdk": pdk,
|
|
721
|
+
"pdk_root": pdk_root,
|
|
722
|
+
"manual_pdk": manual_pdk,
|
|
723
|
+
"scl": scl,
|
|
724
|
+
"flow": flow,
|
|
725
|
+
"tag": tag,
|
|
726
|
+
"force": list(force),
|
|
727
|
+
"config": dict(extra),
|
|
728
|
+
"timeout_s": timeout,
|
|
729
|
+
}
|
|
730
|
+
if self._runner is not None:
|
|
731
|
+
try:
|
|
732
|
+
self._runner.preflight()
|
|
733
|
+
except ExecutionError as exc:
|
|
734
|
+
raise AdapterError(exc.code, str(exc)) from exc
|
|
735
|
+
return checked
|
|
736
|
+
|
|
737
|
+
def execute(self, job):
|
|
738
|
+
"""Stage the design, run the Classic flow bounded and cancellable.
|
|
739
|
+
|
|
740
|
+
The flow runs in its own process group under timeout_s; expiry
|
|
741
|
+
SIGKILLs the whole tree and reports a timeout native block
|
|
742
|
+
with partial evidence instead of hanging the engine.
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
job: Solver job with operation run_digital_flow plus
|
|
746
|
+
sources, top, clock, PDK, tag, force, and config
|
|
747
|
+
inputs.
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
Native output with exit_code, bounded streams, the full
|
|
751
|
+
log path, run/config dirs, the forced list, and the
|
|
752
|
+
librelane version.
|
|
753
|
+
|
|
754
|
+
Raises:
|
|
755
|
+
AdapterError: invalid_reference for a missing workdir or
|
|
756
|
+
an unstageable design, plus the preflight codes
|
|
757
|
+
(execute re-checks first).
|
|
758
|
+
"""
|
|
759
|
+
checked = self.preflight(job)
|
|
760
|
+
workdir = Path(job.get("workdir", "."))
|
|
761
|
+
if not workdir.is_dir():
|
|
762
|
+
raise AdapterError(
|
|
763
|
+
"invalid_reference", f"Workdir not found: {workdir}."
|
|
764
|
+
)
|
|
765
|
+
design = workdir / _DESIGN_DIR
|
|
766
|
+
try:
|
|
767
|
+
src_dir = design / _SRC_DIR
|
|
768
|
+
src_dir.mkdir(parents=True, exist_ok=True)
|
|
769
|
+
staged = []
|
|
770
|
+
for source in checked["sources"]:
|
|
771
|
+
dest = src_dir / Path(source).name
|
|
772
|
+
shutil.copy(source, dest)
|
|
773
|
+
staged.append(str(dest))
|
|
774
|
+
config = {
|
|
775
|
+
"DESIGN_NAME": checked["top"],
|
|
776
|
+
"VERILOG_FILES": staged,
|
|
777
|
+
"CLOCK_PERIOD": checked["clock_period"],
|
|
778
|
+
"PDK": checked["pdk"],
|
|
779
|
+
}
|
|
780
|
+
if checked["clock_port"] is not None:
|
|
781
|
+
config["CLOCK_PORT"] = checked["clock_port"]
|
|
782
|
+
config.update(checked["config"])
|
|
783
|
+
config_path = design / _CONFIG_NAME
|
|
784
|
+
config_path.write_text(json.dumps(config, indent=2) + "\n")
|
|
785
|
+
except OSError as exc:
|
|
786
|
+
raise AdapterError(
|
|
787
|
+
"invalid_reference", f"Cannot stage digital-flow design: {exc}."
|
|
788
|
+
) from exc
|
|
789
|
+
argv = [
|
|
790
|
+
checked["librelane"],
|
|
791
|
+
str(config_path),
|
|
792
|
+
# 3.x renamed --tag to --run-tag (pinned 3.0.17 rejects
|
|
793
|
+
# --tag with "No such option").
|
|
794
|
+
"--run-tag",
|
|
795
|
+
checked["tag"],
|
|
796
|
+
"--pdk",
|
|
797
|
+
checked["pdk"],
|
|
798
|
+
"--overwrite",
|
|
799
|
+
]
|
|
800
|
+
if checked["pdk_root"] is not None:
|
|
801
|
+
argv += ["--pdk-root", checked["pdk_root"]]
|
|
802
|
+
if checked.get("manual_pdk"):
|
|
803
|
+
# --manual-pdk: use the PDK root as-is (proven recipe in
|
|
804
|
+
# waveform-generator). No ciel download, no writes into the
|
|
805
|
+
# PDK tree, so read-only provisioned PDKs work.
|
|
806
|
+
argv += ["--manual-pdk"]
|
|
807
|
+
if checked["scl"] is not None:
|
|
808
|
+
argv += ["--scl", checked["scl"]]
|
|
809
|
+
binds = tool_roots([checked["librelane"]])
|
|
810
|
+
if checked["pdk_root"] is not None:
|
|
811
|
+
binds = list(binds) + [(checked["pdk_root"], checked["pdk_root"])]
|
|
812
|
+
timed_out, proc = self._run(argv, str(workdir), checked["timeout_s"], binds)
|
|
813
|
+
stdout = proc.stdout if proc is not None else ""
|
|
814
|
+
stderr = proc.stderr if proc is not None else ""
|
|
815
|
+
(workdir / _LOG_NAME).write_text(
|
|
816
|
+
f"$ {' '.join(argv)}\n{stdout}\n{stderr}", encoding="utf-8"
|
|
817
|
+
)
|
|
818
|
+
run_dir = design / _RUNS_DIR / checked["tag"]
|
|
819
|
+
return {
|
|
820
|
+
"exit_code": proc.returncode if proc is not None else None,
|
|
821
|
+
"stdout": _tail_kept(stdout or ""),
|
|
822
|
+
"stderr": _tail_kept(stderr or ""),
|
|
823
|
+
"log": _tail_kept(f"{stdout or ''}\n{stderr or ''}"),
|
|
824
|
+
"log_file": str(workdir / _LOG_NAME),
|
|
825
|
+
"timed_out": timed_out,
|
|
826
|
+
"workdir": str(workdir),
|
|
827
|
+
"design_dir": str(design),
|
|
828
|
+
"run_dir": str(run_dir) if run_dir.is_dir() else None,
|
|
829
|
+
"config": str(config_path),
|
|
830
|
+
"top": checked["top"],
|
|
831
|
+
"tag": checked["tag"],
|
|
832
|
+
"pdk": checked["pdk"],
|
|
833
|
+
"flow": checked["flow"],
|
|
834
|
+
"forced": list(checked["force"]),
|
|
835
|
+
"librelane_version": checked["librelane_version"],
|
|
836
|
+
"timeout_s": checked["timeout_s"],
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
def parse(self, native_output):
|
|
840
|
+
"""Judge the D11-D27 gates from the run's machine files.
|
|
841
|
+
|
|
842
|
+
Timeouts report digital_flow_timeout plus the partial gates;
|
|
843
|
+
failed runs fail the first gate and mark evidence-free
|
|
844
|
+
downstream gates not_run; clean exits fail every stateless
|
|
845
|
+
stage. Forced failures warn instead of erroring, and a flow
|
|
846
|
+
failure always names its stage plus report.
|
|
847
|
+
|
|
848
|
+
Args:
|
|
849
|
+
native_output: Native output from execute with exit
|
|
850
|
+
state, run/config dirs, the forced list, and flow
|
|
851
|
+
context.
|
|
852
|
+
|
|
853
|
+
Returns:
|
|
854
|
+
{total, violations[], measurements{}} with the per-stage
|
|
855
|
+
table plus gds, netlist, spef, drc_total, lvs_match,
|
|
856
|
+
version, tag, run_dir, and timed_out evidence.
|
|
857
|
+
|
|
858
|
+
Raises:
|
|
859
|
+
AdapterError: runner_unavailable when the output has no
|
|
860
|
+
exit code and no timeout — unparseable never parses
|
|
861
|
+
as clean.
|
|
862
|
+
"""
|
|
863
|
+
exit_code = native_output.get("exit_code")
|
|
864
|
+
timed_out = bool(native_output.get("timed_out"))
|
|
865
|
+
if exit_code is None and not timed_out:
|
|
866
|
+
raise AdapterError(
|
|
867
|
+
"runner_unavailable", "Native output has no exit code."
|
|
868
|
+
)
|
|
869
|
+
stages, violations = _evaluate_stages(native_output)
|
|
870
|
+
by_id = {stage["id"]: stage for stage in stages}
|
|
871
|
+
states = read_run_states(native_output.get("run_dir"))
|
|
872
|
+
gds = next(
|
|
873
|
+
(
|
|
874
|
+
path
|
|
875
|
+
for path in _step_views(
|
|
876
|
+
states, by_id.get("D24", {}).get("steps", []), (".gds", ".gds.gz")
|
|
877
|
+
)
|
|
878
|
+
+ [
|
|
879
|
+
path
|
|
880
|
+
for path in by_id.get("D24", {}).get("reports", [])
|
|
881
|
+
if path.lower().endswith((".gds", ".gds.gz"))
|
|
882
|
+
]
|
|
883
|
+
),
|
|
884
|
+
None,
|
|
885
|
+
)
|
|
886
|
+
netlist = next(
|
|
887
|
+
(
|
|
888
|
+
path
|
|
889
|
+
for path in _step_views(
|
|
890
|
+
states, by_id.get("D12", {}).get("steps", []), (".v",)
|
|
891
|
+
)
|
|
892
|
+
+ [
|
|
893
|
+
path
|
|
894
|
+
for path in by_id.get("D12", {}).get("evidence", [])
|
|
895
|
+
if path.lower().endswith(".v")
|
|
896
|
+
]
|
|
897
|
+
),
|
|
898
|
+
None,
|
|
899
|
+
)
|
|
900
|
+
spef = next(
|
|
901
|
+
(
|
|
902
|
+
path
|
|
903
|
+
for path in _step_views(
|
|
904
|
+
states, by_id.get("D21", {}).get("steps", []), (".spef",)
|
|
905
|
+
)
|
|
906
|
+
+ [
|
|
907
|
+
path
|
|
908
|
+
for path in by_id.get("D21", {}).get("evidence", [])
|
|
909
|
+
if path.lower().endswith(".spef")
|
|
910
|
+
]
|
|
911
|
+
),
|
|
912
|
+
None,
|
|
913
|
+
)
|
|
914
|
+
drc_metrics, _ = _step_metrics(
|
|
915
|
+
states, by_id.get("D25", {}).get("steps", [])
|
|
916
|
+
)
|
|
917
|
+
_, _, drc_total = drc_verdict(drc_metrics)
|
|
918
|
+
if drc_total is None:
|
|
919
|
+
drc_total, _ = signoff_drc_total(native_output.get("run_dir"))
|
|
920
|
+
lvs_metrics, _ = _step_metrics(
|
|
921
|
+
states, by_id.get("D26", {}).get("steps", [])
|
|
922
|
+
)
|
|
923
|
+
_, _, lvs_match = lvs_verdict(lvs_metrics)
|
|
924
|
+
state_files = sorted(
|
|
925
|
+
{
|
|
926
|
+
path
|
|
927
|
+
for name in (
|
|
928
|
+
by_id.get("D22", {}).get("steps", [])
|
|
929
|
+
+ by_id.get("D25", {}).get("steps", [])
|
|
930
|
+
+ by_id.get("D26", {}).get("steps", [])
|
|
931
|
+
+ by_id.get("D27", {}).get("steps", [])
|
|
932
|
+
)
|
|
933
|
+
for path in states.get(name, {}).get("files", [])
|
|
934
|
+
}
|
|
935
|
+
)
|
|
936
|
+
if timed_out:
|
|
937
|
+
timeout_s = native_output.get("timeout_s")
|
|
938
|
+
violations = [
|
|
939
|
+
{
|
|
940
|
+
"rule": "digital_flow_timeout",
|
|
941
|
+
"severity": "error",
|
|
942
|
+
"message": f"librelane timed out after {timeout_s}s.",
|
|
943
|
+
}
|
|
944
|
+
] + violations
|
|
945
|
+
measurements = {
|
|
946
|
+
"flow": native_output.get("flow"),
|
|
947
|
+
"tag": native_output.get("tag"),
|
|
948
|
+
"top": native_output.get("top"),
|
|
949
|
+
"pdk": native_output.get("pdk"),
|
|
950
|
+
"librelane_version": native_output.get("librelane_version"),
|
|
951
|
+
"timeout_s": native_output.get("timeout_s"),
|
|
952
|
+
"timed_out": timed_out,
|
|
953
|
+
"exit_code": exit_code,
|
|
954
|
+
"forced": list(native_output.get("forced") or []),
|
|
955
|
+
"run_dir": native_output.get("run_dir"),
|
|
956
|
+
"config": native_output.get("config"),
|
|
957
|
+
"stages": stages,
|
|
958
|
+
"gds": gds,
|
|
959
|
+
"netlist": netlist,
|
|
960
|
+
"spef": spef,
|
|
961
|
+
"drc_total": drc_total,
|
|
962
|
+
"lvs_match": lvs_match,
|
|
963
|
+
"state_files": state_files,
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
"total": len(violations),
|
|
967
|
+
"violations": violations,
|
|
968
|
+
"measurements": measurements,
|
|
969
|
+
"stages": stages,
|
|
970
|
+
"gds": gds,
|
|
971
|
+
"netlist": netlist,
|
|
972
|
+
"spef": spef,
|
|
973
|
+
"drc_total": drc_total,
|
|
974
|
+
"lvs_match": lvs_match,
|
|
975
|
+
"librelane_version": native_output.get("librelane_version"),
|
|
976
|
+
"tag": native_output.get("tag"),
|
|
977
|
+
"run_dir": native_output.get("run_dir"),
|
|
978
|
+
"timed_out": timed_out,
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
@staticmethod
|
|
982
|
+
def _resolve(job):
|
|
983
|
+
tool_dir = job.get("tool_dir")
|
|
984
|
+
if tool_dir:
|
|
985
|
+
candidate = Path(tool_dir) / _TOOL
|
|
986
|
+
if candidate.is_file():
|
|
987
|
+
return str(candidate)
|
|
988
|
+
raise AdapterError(
|
|
989
|
+
"missing_dependency",
|
|
990
|
+
f"{_TOOL} not found in {tool_dir} — {_MISSING_REMEDY}.",
|
|
991
|
+
)
|
|
992
|
+
found = shutil.which(_TOOL)
|
|
993
|
+
if found is None:
|
|
994
|
+
raise AdapterError(
|
|
995
|
+
"missing_dependency",
|
|
996
|
+
f"{_TOOL} not on PATH — {_MISSING_REMEDY}.",
|
|
997
|
+
)
|
|
998
|
+
return found
|
|
999
|
+
|
|
1000
|
+
@staticmethod
|
|
1001
|
+
def _version(librelane):
|
|
1002
|
+
"""Assert the binary answers with the pinned 3.0.17 banner.
|
|
1003
|
+
|
|
1004
|
+
Args:
|
|
1005
|
+
librelane: Resolved librelane binary path.
|
|
1006
|
+
|
|
1007
|
+
Returns:
|
|
1008
|
+
Full `librelane --version` banner line (kept as evidence).
|
|
1009
|
+
|
|
1010
|
+
Raises:
|
|
1011
|
+
AdapterError: missing_dependency when the probe fails,
|
|
1012
|
+
times out, or answers no pinned banner.
|
|
1013
|
+
"""
|
|
1014
|
+
try:
|
|
1015
|
+
proc = subprocess.run(
|
|
1016
|
+
[librelane, "--version"],
|
|
1017
|
+
capture_output=True,
|
|
1018
|
+
text=True,
|
|
1019
|
+
timeout=_VERSION_TIMEOUT_S,
|
|
1020
|
+
)
|
|
1021
|
+
except subprocess.TimeoutExpired as exc:
|
|
1022
|
+
raise AdapterError(
|
|
1023
|
+
"missing_dependency",
|
|
1024
|
+
f"librelane version probe timed out at {librelane} — "
|
|
1025
|
+
f"{_MISSING_REMEDY}.",
|
|
1026
|
+
) from exc
|
|
1027
|
+
except OSError as exc:
|
|
1028
|
+
raise AdapterError(
|
|
1029
|
+
"missing_dependency",
|
|
1030
|
+
f"librelane version probe failed at {librelane}: {exc} — "
|
|
1031
|
+
f"{_MISSING_REMEDY}.",
|
|
1032
|
+
) from exc
|
|
1033
|
+
banner = ((proc.stdout or "") + "\n" + (proc.stderr or "")).strip().splitlines()
|
|
1034
|
+
versioned = next((line for line in banner if _PIN in line), None)
|
|
1035
|
+
if proc.returncode != 0 or versioned is None:
|
|
1036
|
+
got = banner[0] if banner else "empty banner"
|
|
1037
|
+
raise AdapterError(
|
|
1038
|
+
"missing_dependency",
|
|
1039
|
+
f"librelane at {librelane} reported {got!r}, want pin {_PIN} — "
|
|
1040
|
+
f"{_MISSING_REMEDY}.",
|
|
1041
|
+
)
|
|
1042
|
+
return versioned.strip()
|
|
1043
|
+
|
|
1044
|
+
def _run(self, argv, workdir, timeout_s, extra_binds=()):
|
|
1045
|
+
"""(timed_out, proc) for one flow run; expiry kills the tree."""
|
|
1046
|
+
if self._runner is not None:
|
|
1047
|
+
try:
|
|
1048
|
+
proc = self._runner.run(argv, workdir, timeout_s, extra_binds)
|
|
1049
|
+
except ExecutionError as exc:
|
|
1050
|
+
if exc.code == "runner_unavailable" and "timed out after" in str(exc):
|
|
1051
|
+
return True, None
|
|
1052
|
+
raise AdapterError(exc.code, str(exc)) from exc
|
|
1053
|
+
return False, proc
|
|
1054
|
+
# Tee: stream the flow to the terminal live while keeping
|
|
1055
|
+
# bounded tails for verdicts (never swallow long builds).
|
|
1056
|
+
import collections
|
|
1057
|
+
import threading
|
|
1058
|
+
|
|
1059
|
+
proc = subprocess.Popen(
|
|
1060
|
+
argv,
|
|
1061
|
+
stdout=subprocess.PIPE,
|
|
1062
|
+
stderr=subprocess.PIPE,
|
|
1063
|
+
cwd=workdir,
|
|
1064
|
+
start_new_session=True,
|
|
1065
|
+
text=True,
|
|
1066
|
+
encoding="utf-8",
|
|
1067
|
+
errors="replace",
|
|
1068
|
+
bufsize=1,
|
|
1069
|
+
)
|
|
1070
|
+
tails = {
|
|
1071
|
+
"out": collections.deque(maxlen=500),
|
|
1072
|
+
"err": collections.deque(maxlen=500),
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
def _pump(stream, key, sink):
|
|
1076
|
+
try:
|
|
1077
|
+
for line in iter(stream.readline, ""):
|
|
1078
|
+
sink.write(line)
|
|
1079
|
+
sink.flush()
|
|
1080
|
+
tails[key].append(line)
|
|
1081
|
+
except (OSError, ValueError):
|
|
1082
|
+
pass
|
|
1083
|
+
finally:
|
|
1084
|
+
try:
|
|
1085
|
+
stream.close()
|
|
1086
|
+
except (OSError, ValueError):
|
|
1087
|
+
pass
|
|
1088
|
+
|
|
1089
|
+
threads = [
|
|
1090
|
+
threading.Thread(
|
|
1091
|
+
target=_pump, args=(proc.stdout, "out", sys.stdout), daemon=True
|
|
1092
|
+
),
|
|
1093
|
+
threading.Thread(
|
|
1094
|
+
target=_pump, args=(proc.stderr, "err", sys.stderr), daemon=True
|
|
1095
|
+
),
|
|
1096
|
+
]
|
|
1097
|
+
for thread in threads:
|
|
1098
|
+
thread.start()
|
|
1099
|
+
try:
|
|
1100
|
+
proc.wait(timeout=timeout_s)
|
|
1101
|
+
except subprocess.TimeoutExpired:
|
|
1102
|
+
try:
|
|
1103
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
1104
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
1105
|
+
pass
|
|
1106
|
+
try:
|
|
1107
|
+
proc.kill()
|
|
1108
|
+
except (ProcessLookupError, OSError):
|
|
1109
|
+
pass
|
|
1110
|
+
proc.wait()
|
|
1111
|
+
for thread in threads:
|
|
1112
|
+
thread.join(timeout=5)
|
|
1113
|
+
return True, None
|
|
1114
|
+
for thread in threads:
|
|
1115
|
+
thread.join(timeout=30)
|
|
1116
|
+
return False, subprocess.CompletedProcess(
|
|
1117
|
+
proc.args,
|
|
1118
|
+
proc.returncode,
|
|
1119
|
+
"".join(tails["out"]),
|
|
1120
|
+
"".join(tails["err"]),
|
|
1121
|
+
)
|