diff --git a/MODULE.bazel b/MODULE.bazel
index f828679d3f..a36173378f 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -101,7 +101,7 @@ bazel_dep(name = "bazel-orfs", version = "0.0.1")
bazel_dep(name = "bazel-orfs-verilog", dev_dependency = True)
-BAZEL_ORFS_COMMIT = "6ebadeb4be5c9ada103081c9a5e668c014126616"
+BAZEL_ORFS_COMMIT = "3342596c1f7017b9fda5b4a55a25a54ff2aaedf8"
BAZEL_ORFS_REMOTE = "https://github.com/The-OpenROAD-Project/bazel-orfs.git"
diff --git a/flow/designs/BUILD b/flow/designs/BUILD
index 0fe28bc544..c11384ea63 100644
--- a/flow/designs/BUILD
+++ b/flow/designs/BUILD
@@ -1 +1,12 @@
exports_files(["BUILD"])
+
+# Local TL;DR of https://dashboard.precisioninno.com/: a GitHub-markdown
+# status table over the forced-engine OpenROAD-SYN QoR tests already
+# built in this workspace (see flow/designs/design.bzl). Reads
+# bazel-testlogs only — builds nothing, instant.
+#
+# bazelisk run //flow/designs:syn-dashboard
+alias(
+ name = "syn-dashboard",
+ actual = "//flow/util:synDashboard",
+)
diff --git a/flow/designs/README.md b/flow/designs/README.md
new file mode 100644
index 0000000000..84cde748d3
--- /dev/null
+++ b/flow/designs/README.md
@@ -0,0 +1,72 @@
+# Fast local OpenROAD-SYN testing loop
+
+The maintainers' QoR signal comes from Jenkins runs published at
+. The targets described here are
+its local counterpart: the same designs, gated by the same
+`rules-base.json` files with the same `checkMetadata.py`, but built
+locally through Bazel so the loop can be sharpened onto exactly the
+design, engine, and stage being worked on — fast enough for an agent
+(e.g. Claude) to iterate against.
+
+Every design gets manual forced-engine flow variants (see
+`design.bzl`), regardless of the engine its config.mk selects:
+
+- `_syn_*` pins `SYNTH_USE_SYN=1` (OpenROAD-SYN), so any design
+ can be tested under SYN before its config.mk officially switches.
+- `_yosys_*` pins the yosys engine, so the baseline stays
+ reproducible after a design switches (asap7/gcd today).
+
+Designs with a `rules-base.json` get two QoR tiers per variant:
+
+- `__synth_test` — the fast tier: synthesis only,
+ checked against the `synth__`/`constraints__` subset of the rules.
+ Minutes. QoR-only — without LEC it cannot prove correctness, so use
+ it to iterate, not to conclude.
+- `__test` — the full flow against all rules: the real,
+ dashboard-equivalent gate. It reuses the fast tier's synthesis
+ action, so escalating from fast to full never repeats synthesis.
+
+## The loop
+
+Build/test whatever is relevant to what you are changing — one design,
+one stage, one tier, or a whole suite:
+
+```sh
+# fastest signal for one design
+bazelisk test //flow/designs/asap7/jpeg:jpeg_encoder_syn_synth_test
+
+# the real gate for that design
+bazelisk test //flow/designs/asap7/jpeg:jpeg_encoder_syn_test
+
+# a single stage, for triage
+bazelisk build //flow/designs/asap7/jpeg:jpeg_encoder_syn_route
+
+# yosys A/B on an officially-SYN design
+bazelisk test //flow/designs/asap7/gcd:gcd_yosys_test
+
+# suite-wide status (asap7): fast tier, then the full flows
+bazelisk test --keep_going //flow/designs/asap7:syn_synth_test
+bazelisk test --keep_going //flow/designs/asap7:syn_test
+```
+
+Then display what you have — deliberately a separate concern from
+deciding what to test or build:
+
+```sh
+bazelisk run //flow/designs:syn-dashboard
+```
+
+It prints a GitHub-markdown TL;DR table of every forced-engine test
+result already in `bazel-testlogs` — pass/fail per tier and engine,
+failing rule fields, result age — without building anything. Paste it
+into a PR or issue as-is. Because the display reads whatever exists,
+the loop stays fast: iterate on the one target you care about, and the
+dashboard aggregates across everything you (or earlier sessions) have
+built so far.
+
+The suites include designs that fail OpenROAD-SYN today on purpose —
+they are a status signal, not a green-only gate. A build failure (e.g.
+synthesis dies) records no test result at all, so run the suites with
+`--keep_going` and check their output for `FAILED TO BUILD`; designs
+too slow for the suites are listed in `SYN_SLOW_DESIGNS`
+(`asap7/BUILD`) and reproduced individually.
diff --git a/flow/designs/asap7/BUILD b/flow/designs/asap7/BUILD
new file mode 100644
index 0000000000..20005b4141
--- /dev/null
+++ b/flow/designs/asap7/BUILD
@@ -0,0 +1,111 @@
+# OpenROAD-SYN test suites for asap7 — the local counterpart of the
+# Jenkins QoR signal (https://dashboard.precisioninno.com/).
+#
+# Every design gets manual forced-engine flow variants from the
+# config.mk DSL (see flow/designs/design.bzl): _syn_* pins
+# SYNTH_USE_SYN=1 and _yosys_* pins the yosys engine, regardless
+# of the config.mk default. Designs with a rules-base.json get two QoR
+# gates per variant, both checking the design's regular rules file with
+# the regular checkMetadata.py — congruent with what Jenkins measures:
+#
+# bazelisk test --keep_going //flow/designs/asap7:syn_synth_test
+# Fast tier: synthesis only, checks the synth__/constraints__ subset
+# of rules-base.json. Minutes-scale; QoR-only (no LEC), so it cannot
+# prove correctness — use it to iterate, not to conclude.
+#
+# bazelisk test --keep_going //flow/designs/asap7:syn_test
+# Full flow, all rules-base.json gates. The real signal; shares its
+# synthesis action with the fast tier.
+#
+# Sharpening the loop on one design or stage:
+#
+# bazelisk test //flow/designs/asap7/jpeg:jpeg_encoder_syn_synth_test
+# bazelisk test //flow/designs/asap7/jpeg:jpeg_encoder_syn_test
+# bazelisk build //flow/designs/asap7/jpeg:jpeg_encoder_syn_route
+# bazelisk test //flow/designs/asap7/gcd:gcd_yosys_test # yosys A/B
+#
+# Both suites deliberately include designs that fail OpenROAD-SYN today
+# — they are a status signal, not a green-only gate. A design can fail
+# by not synthesizing at all, or by missing rules-base.json gates that
+# its regular flow meets.
+
+# Design directory -> DESIGN_NAME (bazel target prefix), ordered
+# fastest-first so --keep_going runs surface quick failures early.
+# Known failures / quirks are listed last. "minimal" is excluded: it
+# has no VERILOG_FILES and thus no generated design targets.
+#
+# Known OpenROAD-SYN failures today:
+# - mock-alu: SDC references yosys-style register names (*io_out_REG*)
+# absent from the SYN netlist.
+# - riscv32i: slang rejects procedural assignment to nets (dmem.v).
+# - mock-cpu: slang rejects endianness-mismatched part-select
+# (wptr_full.v).
+# - aes-block: hierarchical BLOCKS; synthesizes, but SYN area blows past
+# the yosys-derived rules-base.json gate.
+SYN_TEST_DESIGNS = {
+ "gcd": "gcd", # smoketest, fast
+ "uart": "uart",
+ "mock-alu": "MockAlu",
+ "riscv32i": "riscv_top",
+ "aes": "aes_cipher_top",
+ "aes_lvt": "aes_cipher_top",
+ "aes-mbff": "aes_cipher_top",
+ "jpeg": "jpeg_encoder",
+ "jpeg_lvt": "jpeg_encoder",
+ "ethmac": "ethmac",
+ "ethmac_lvt": "ethmac",
+ "mock-cpu": "mock_cpu",
+ "gcd-ccs": "gcd", # CCS liberty
+ "aes-block": "aes_cipher_top", # hierarchical BLOCKS
+}
+
+# Designs whose OpenROAD-SYN run is too slow (or does not terminate) for
+# the suites. Their generated per-design targets stay tags=["manual"]
+# like all forced-engine targets, but they are deliberately left out of
+# the suites so those remain runnable; reproduce individually, e.g.
+# bazelisk build //flow/designs/asap7/ibex:ibex_core_syn_synth
+#
+# - ibex: sv_elaborate/synthesize/repair_design complete in seconds, but
+# report_metrics ("Report metrics stage 1, synth...") spins in STA for
+# hours (observed >2h on 48 threads with no progress).
+# - cva6: synthesize itself takes ~73s, but repair_design -pre_placement
+# was still running after 10+ minutes when the status sweep was cut
+# off; not yet characterized to completion.
+# - swerv_wrapper: largest design in the suite; not yet run to
+# completion under OpenROAD-SYN in this environment.
+# - riscv32i-mock-sram: hierarchical BLOCKS (fakeram) — the syn variant
+# first needs the block's full PnR flow for its abstract; not yet run
+# to completion.
+SYN_SLOW_DESIGNS = {
+ "cva6": "cva6",
+ "ibex": "ibex_core",
+ "riscv32i-mock-sram": "riscv_top",
+ "swerv_wrapper": "swerv_wrapper",
+}
+
+# Explicitly listed manual tests DO run through a test_suite (the manual
+# tag only exempts them from wildcard expansion); the suites themselves
+# are manual so `bazelisk test //flow/...` does not pull them in either.
+test_suite(
+ name = "syn_test",
+ tags = ["manual"],
+ tests = [
+ "//flow/designs/asap7/{pkg}:{name}_syn_test".format(
+ name = name,
+ pkg = pkg,
+ )
+ for pkg, name in SYN_TEST_DESIGNS.items()
+ ],
+)
+
+test_suite(
+ name = "syn_synth_test",
+ tags = ["manual"],
+ tests = [
+ "//flow/designs/asap7/{pkg}:{name}_syn_synth_test".format(
+ name = name,
+ pkg = pkg,
+ )
+ for pkg, name in SYN_TEST_DESIGNS.items()
+ ],
+)
diff --git a/flow/designs/asap7/gcd/BUILD b/flow/designs/asap7/gcd/BUILD
index b8c3ddc518..9b63695b82 100644
--- a/flow/designs/asap7/gcd/BUILD
+++ b/flow/designs/asap7/gcd/BUILD
@@ -3,13 +3,13 @@ load("@orfs_designs//:designs.bzl", "DESIGNS")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("//flow/designs:design.bzl", "design")
-# SYNTH_USE_SYN is make-only for now: bazel-orfs's synth stage always
-# runs the yosys flow and neither stages the Verilog sources nor sets
-# VERILOG_FILES for the OpenROAD synthesis step, so the built-in
-# synthesizer opt-in must not reach the bazel arguments.
+# gcd's regular flow follows config.mk onto OpenROAD-SYN
+# (SYNTH_USE_SYN=1). The single-process comparison below deliberately
+# exercises the yosys netlist path (single_flow.tcl seeds from
+# 1_2_yosys.v), so it consumes the forced-yosys gcd_yosys_* variant the
+# config.mk DSL emits for every design (see flow/designs/design.bzl).
design(
config = "config.mk",
- local_arguments = ["SYNTH_USE_SYN"],
)
# Stage-boundary .odb/.sdc stems shared by the normal per-stage flow
@@ -56,7 +56,7 @@ SINGLE_FLOW_EXTRA_OUTS = [
# this package.
orfs_run(
name = "gcd_single_flow",
- src = ":gcd_synth",
+ src = ":gcd_yosys_synth",
outs = [
"results/asap7/gcd/single/{}.{}".format(stem, ext)
for stem in SINGLE_FLOW_STAGES.values()
@@ -79,11 +79,13 @@ orfs_run(
variant = "single",
)
-# Normal-flow stage outputs, one filegroup per compared file.
+# Normal-flow stage outputs, one filegroup per compared file. Taken from
+# the yosys-engine variant above so both sides of the byte-compare start
+# from the same yosys netlist.
[
filegroup(
name = "gcd_base_{}_{}".format(stage, ext),
- srcs = [":gcd_" + stage],
+ srcs = [":gcd_yosys_" + stage],
output_group = "{}.{}".format(stem, ext),
)
for stage, stem in SINGLE_FLOW_STAGES.items()
diff --git a/flow/designs/design.bzl b/flow/designs/design.bzl
index b4efc0b1d0..c317e34b9a 100644
--- a/flow/designs/design.bzl
+++ b/flow/designs/design.bzl
@@ -1,5 +1,6 @@
"""BUILD boilerplate for flow/designs/."""
+load("@bazel-orfs//:openroad.bzl", "orfs_flow")
load("@orfs_designs//:designs.bzl", "orfs_design")
# Per filegroup target: extensions included in the filegroup.
@@ -51,6 +52,65 @@ def _export_design_files():
visibility = ["//visibility:private"],
)
+def _engine_variant_targets(
+ name,
+ platform,
+ verilog_files,
+ arguments,
+ user_arguments,
+ sources,
+ user_sources,
+ macros,
+ stage_data,
+ tags): # buildifier: disable=unused-variable
+ """Forced-engine flow variants for one design (orfs_design extra hook).
+
+ Emits, always tagged "manual" (even for CI designs, so only explicit
+ invocations such as //flow/designs/asap7:syn_test run them), two full
+ flows that pin the synthesis engine regardless of the config.mk
+ default:
+
+ - variant "syn": SYNTH_USE_SYN=1 (OpenROAD-SYN) — lets any design be
+ tested under OpenROAD-SYN before its config.mk officially switches.
+ - variant "yosys": SYNTH_USE_SYN=0 — keeps the yosys baseline
+ reproducible after a design officially switches (e.g. asap7/gcd).
+
+ When the design has a rules-base.json each variant gets the same QoR
+ gates as the regular flow, congruent with the Jenkins/dashboard
+ signal because they check the same rules file with the same script:
+
+ - __test: the full flow gated on all of rules-base.json
+ (the real, dashboard-equivalent gate).
+ - __synth_test: the fast tier — synthesis plus a check
+ of only the synth__/constraints__ subset of rules-base.json.
+ QoR-only (no LEC), but a minutes-scale iteration signal that shares
+ its synth action with the full-flow variant.
+
+ The generated __update targets must NOT be run: they
+ would overwrite the design's rules-base.json with the forced engine's
+ numbers.
+ """
+ for engine, use_syn in [("syn", "1"), ("yosys", "0")]:
+ orfs_flow(
+ name = name,
+ verilog_files = verilog_files,
+ pdk = "//flow:" + platform,
+ arguments = arguments | {"SYNTH_USE_SYN": use_syn},
+ user_arguments = user_arguments,
+ # sources carries the auto-detected rules-base.json as
+ # RULES_JSON, which gates the variant's tests.
+ sources = sources,
+ user_sources = user_sources,
+ macros = macros,
+ stage_data = stage_data,
+ variant = engine,
+ tags = ["manual"],
+ test_kwargs = {"tags": ["manual"]},
+ # Sibling packages consume stage outputs (e.g. asap7/gcd's
+ # single-process comparison seeds from the yosys netlist).
+ visibility = ["//visibility:public"],
+ )
+
def design(config = "config.mk", user_arguments = [], user_sources = [], local_arguments = []):
"""Standard BUILD body for flow/designs///.
@@ -75,6 +135,7 @@ def design(config = "config.mk", user_arguments = [], user_sources = [], local_a
user_sources = user_sources,
local_arguments = local_arguments,
blender = True,
+ extra = _engine_variant_targets,
)
def files(group, extra_srcs = None):
diff --git a/flow/scripts/synth_syn.tcl b/flow/scripts/synth_syn.tcl
index efe1f7779b..bd6eaf0820 100644
--- a/flow/scripts/synth_syn.tcl
+++ b/flow/scripts/synth_syn.tcl
@@ -75,3 +75,12 @@ orfs_write_db $::env(RESULTS_DIR)/1_synth.odb
# out by OpenSTA that has no dependencies. Sole writer of
# 1_synth.sdc.
orfs_write_sdc $::env(RESULTS_DIR)/1_synth.sdc
+
+# Gate-level netlist for LEC (write_lec_verilog strips physical-only
+# masters, matching the CTS-stage LEC convention) and any other netlist
+# consumer. The Bazel synthesis action declares this file as an output,
+# so it must be written unconditionally. write_lec_verilog takes a bare
+# filename and prefixes $::env(RESULTS_DIR) itself, like the cts.tcl
+# call sites.
+source $::env(SCRIPTS_DIR)/lec_check.tcl
+write_lec_verilog 1_synth.v
diff --git a/flow/util/BUILD b/flow/util/BUILD
index cc21b559c6..a8e1092dbb 100644
--- a/flow/util/BUILD
+++ b/flow/util/BUILD
@@ -1,4 +1,4 @@
-load("@rules_python//python:defs.bzl", "py_library", "py_test")
+load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
exports_files(["open_plots.sh"])
@@ -33,6 +33,14 @@ filegroup(
visibility = ["//visibility:public"],
)
+# TL;DR markdown dashboard over locally built OpenROAD-SYN QoR tests;
+# run via the //flow/designs:syn-dashboard alias.
+py_binary(
+ name = "synDashboard",
+ srcs = ["synDashboard.py"],
+ visibility = ["//flow/designs:__pkg__"],
+)
+
py_library(
name = "genMetrics_lib",
srcs = ["genMetrics.py"],
diff --git a/flow/util/checkMetadata.py b/flow/util/checkMetadata.py
index 1a525d88a8..71c25425df 100755
--- a/flow/util/checkMetadata.py
+++ b/flow/util/checkMetadata.py
@@ -33,6 +33,14 @@
)
parser.add_argument("--metadata", "-m", required=True, help="The metadata file")
parser.add_argument("--rules", "-r", required=True, nargs="+", help="The rules file")
+parser.add_argument(
+ "--only-prefix",
+ nargs="+",
+ default=None,
+ help="Check only rules whose field starts with one of these prefixes. "
+ "Lets a partial run (e.g. synthesis-only) be gated by a full-flow "
+ "rules file without tripping the missing-field error.",
+)
args = parser.parse_args()
with open(args.metadata) as metadataFile:
@@ -46,6 +54,13 @@
else:
print(f"[WARN] File {filePath} not found")
+if args.only_prefix:
+ rules = {
+ field: rule
+ for field, rule in rules.items()
+ if any(field.startswith(prefix) for prefix in args.only_prefix)
+ }
+
if len(rules) == 0:
print("No rules")
sys.exit(1)
diff --git a/flow/util/genMetrics.py b/flow/util/genMetrics.py
index d845a4e065..9f20e89b16 100755
--- a/flow/util/genMetrics.py
+++ b/flow/util/genMetrics.py
@@ -192,9 +192,12 @@ def git_head_commit(git_exe, folder):
def file_sha1(path):
- """SHA-1 of `path`, or "N/A" if absent. Read in chunks so large
+ """SHA-1 of `path`, or "N/A" if absent or empty. Empty counts as
+ absent so Bazel's touched placeholder files (e.g. the canonicalize
+ RTLIL under SYNTH_USE_SYN or SYNTH_NETLIST_FILES) hash the same as
+ a make run that never wrote the file. Read in chunks so large
netlists don't blow the heap."""
- if not os.path.isfile(path):
+ if not os.path.isfile(path) or os.path.getsize(path) == 0:
return "N/A"
hasher = hashlib.sha1()
with open(path, "rb") as f:
@@ -258,7 +261,13 @@ def extract_metrics(
# Clocks
# =========================================================================
- clk_list = read_sdc(resultPath + "/2_floorplan.sdc")
+ # Prefer the floorplan SDC; on a synthesis-only tree (e.g. the "syn"
+ # flow variant stops after 1_synth) fall back to the canonicalized
+ # synthesis SDC, which carries the same create_clock lines.
+ sdc_file = resultPath + "/2_floorplan.sdc"
+ if not os.path.isfile(sdc_file):
+ sdc_file = resultPath + "/1_synth.sdc"
+ clk_list = read_sdc(sdc_file)
metrics_dict["constraints__clocks__count"] = len(clk_list)
metrics_dict["constraints__clocks__details"] = clk_list
@@ -277,22 +286,26 @@ def extract_metrics(
# Global Route
# =========================================================================
merge_jsons(logPath, metrics_dict, "5_*.json")
- extractTagFromFile(
- "globalroute__timing__clock__slack",
- metrics_dict,
- "^\\[INFO FLW-....\\] Clock .* slack (\\S+)",
- logPath + "/5_1_grt.log",
- )
+ # Guarded so a synthesis-only tree doesn't pollute the metadata with
+ # "ERR" values for stages that never ran.
+ if os.path.isfile(logPath + "/5_1_grt.log"):
+ extractTagFromFile(
+ "globalroute__timing__clock__slack",
+ metrics_dict,
+ "^\\[INFO FLW-....\\] Clock .* slack (\\S+)",
+ logPath + "/5_1_grt.log",
+ )
# Finish
# =========================================================================
merge_jsons(logPath, metrics_dict, "6_*.json")
- extractTagFromFile(
- "finish__timing__wns_percent_delay",
- metrics_dict,
- baseRegEx.format("finish slack div critical path delay", "(\\S+)"),
- rptPath + "/6_finish.rpt",
- )
+ if os.path.isfile(rptPath + "/6_finish.rpt"):
+ extractTagFromFile(
+ "finish__timing__wns_percent_delay",
+ metrics_dict,
+ baseRegEx.format("finish slack div critical path delay", "(\\S+)"),
+ rptPath + "/6_finish.rpt",
+ )
extractGnuTime("finish", metrics_dict, logPath + "/6_report.log")
diff --git a/flow/util/synDashboard.py b/flow/util/synDashboard.py
new file mode 100644
index 0000000000..6204e8f4cb
--- /dev/null
+++ b/flow/util/synDashboard.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+
+"""TL;DR dashboard over locally built OpenROAD-SYN QoR test results.
+
+The local counterpart of https://dashboard.precisioninno.com/: scans
+bazel-testlogs for the forced-engine QoR tests emitted by
+flow/designs/design.bzl (_{syn,yosys}[_synth]_test) and prints a
+GitHub-flavored markdown status table, so the output can be pasted
+into a PR or issue as-is. Reads only what previous `bazelisk test`
+runs left behind — it never builds anything, so it is instant.
+
+Run it via:
+ bazelisk run //flow/designs:syn-dashboard
+"""
+
+import os
+import re
+import sys
+import time
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+# _{syn,yosys}[_synth]_test, as emitted by design.bzl's
+# forced-engine variants. "_synth" marks the fast synthesis-only tier.
+TARGET_RE = re.compile(r"^(?P.+)_(?Psyn|yosys)(?P_synth)?_test$")
+
+FAIL_RULE_RE = re.compile(r"^\[ERROR\] (\S+) fail test: (.*)$")
+MISSING_RULE_RE = re.compile(r"^\[ERROR\] Value not found for (\S+)\.$")
+
+# (engine, fast) -> table column, in display order.
+COLUMNS = [
+ ("syn", True, "SYN synth"),
+ ("syn", False, "SYN flow"),
+ ("yosys", True, "yosys synth"),
+ ("yosys", False, "yosys flow"),
+]
+
+
+def workspace_root():
+ """The bazel workspace root: BUILD_WORKSPACE_DIRECTORY under
+ `bazel run`, else the nearest ancestor holding MODULE.bazel."""
+ env = os.environ.get("BUILD_WORKSPACE_DIRECTORY")
+ if env:
+ return Path(env)
+ for path in [Path.cwd()] + list(Path.cwd().parents):
+ if (path / "MODULE.bazel").is_file():
+ return path
+ sys.exit("error: not inside a bazel workspace (no MODULE.bazel found)")
+
+
+def test_status(test_dir):
+ """(passed, mtime) for one bazel-testlogs test dir, or None if the
+ test never produced a result. The test.xml duration is just the
+ metadata-check action, not the flow build, so it is not reported."""
+ xml_file = test_dir / "test.xml"
+ if not xml_file.is_file():
+ return None
+ try:
+ root = ET.parse(xml_file).getroot()
+ failures = 0
+ for suite in root.iter("testsuite"):
+ failures += int(suite.get("failures") or 0) + int(suite.get("errors") or 0)
+ except (ET.ParseError, ValueError, TypeError):
+ # A corrupt test.xml is "no result", not a pass — swallowing the
+ # error inside the sum would render a green cell for it.
+ return None
+ return failures == 0, xml_file.stat().st_mtime
+
+
+def failing_rules(test_dir):
+ """Failed rule fields harvested from checkMetadata.py output in
+ test.log."""
+ log_file = test_dir / "test.log"
+ if not log_file.is_file():
+ return []
+ fields = []
+ for line in log_file.read_text(encoding="utf-8", errors="replace").splitlines():
+ match = FAIL_RULE_RE.match(line)
+ if match:
+ fields.append(match.group(1))
+ continue
+ match = MISSING_RULE_RE.match(line)
+ if match:
+ fields.append(match.group(1) + " (missing)")
+ return fields
+
+
+def age(mtime):
+ seconds = max(0, time.time() - mtime)
+ if seconds >= 86400:
+ return f"{seconds / 86400:.0f}d ago"
+ if seconds >= 3600:
+ return f"{seconds / 3600:.0f}h ago"
+ if seconds >= 60:
+ return f"{seconds / 60:.0f}m ago"
+ return "just now"
+
+
+def collect(root):
+ """{(platform, design_pkg): {(engine, fast): result}} from
+ bazel-testlogs, where result adds the failed-rule fields."""
+ designs = {}
+ testlogs = root / "bazel-testlogs" / "flow" / "designs"
+ if not testlogs.is_dir():
+ return designs
+ for xml_file in testlogs.glob("*/*/*/test.xml"):
+ target_dir = xml_file.parent
+ match = TARGET_RE.match(target_dir.name)
+ if not match:
+ continue
+ status = test_status(target_dir)
+ if status is None:
+ continue
+ passed, mtime = status
+ key = (target_dir.parent.parent.name, target_dir.parent.name)
+ designs.setdefault(key, {})[
+ (match.group("engine"), bool(match.group("fast")))
+ ] = {
+ "passed": passed,
+ "mtime": mtime,
+ "failing": [] if passed else failing_rules(target_dir),
+ }
+ return designs
+
+
+def format_rules(fields, limit=3):
+ if len(fields) > limit:
+ fields = fields[:limit] + [f"+{len(fields) - limit} more"]
+ return ", ".join(fields)
+
+
+def main():
+ designs = collect(workspace_root())
+ print("# OpenROAD-SYN local status")
+ print()
+ if not designs:
+ print("No forced-engine test results under bazel-testlogs yet. Build some:")
+ print()
+ print(" bazelisk test --keep_going //flow/designs/asap7:syn_synth_test")
+ print(" bazelisk test --keep_going //flow/designs/asap7:syn_test")
+ return
+ header = (
+ ["design"] + [title for _, _, title in COLUMNS] + ["failing rules", "last run"]
+ )
+ print("| " + " | ".join(header) + " |")
+ print("|" + "|".join(["---"] * len(header)) + "|")
+ for (platform, pkg), results in sorted(designs.items()):
+ cells = []
+ for engine, fast, _ in COLUMNS:
+ result = results.get((engine, fast))
+ if result is None:
+ cells.append("—")
+ else:
+ cells.append("✅" if result["passed"] else "❌")
+ # The most meaningful failure detail: the full SYN flow if it
+ # failed, else the fast SYN tier, else any failing variant.
+ failing = []
+ for key in [("syn", False), ("syn", True), ("yosys", False), ("yosys", True)]:
+ result = results.get(key)
+ if result and not result["passed"] and result["failing"]:
+ failing = result["failing"]
+ break
+ newest = max(r["mtime"] for r in results.values())
+ row = [f"{platform}/{pkg}"] + cells + [format_rules(failing), age(newest)]
+ print("| " + " | ".join(row) + " |")
+ print()
+ print("Each cell reflects its own last `bazelisk test` run; `last run` is the")
+ print("newest across the row. `—` = no local result: never run, or its build")
+ print("failed outright (e.g. synthesis died) — bazel records no test result")
+ print("for build failures, so run the suites with `--keep_going` and check")
+ print("their output for `FAILED TO BUILD`.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/flow/util/utils.mk b/flow/util/utils.mk
index 0dbeb1ea6e..abd1427484 100644
--- a/flow/util/utils.mk
+++ b/flow/util/utils.mk
@@ -3,6 +3,19 @@
.PHONY: metadata
metadata: finish metadata-generate metadata-check
+# Synthesis-only metadata: generate and check QoR after just the synth
+# stage, without running the full flow. The synthesis-stage subset of
+# the design's regular rules file gates the run, so no per-variant
+# rules file needs to be committed. Sequential sub-makes rather than
+# prerequisites: each step consumes the previous one's outputs, which
+# plain prerequisites would race under make -j.
+.PHONY: metadata-synth
+metadata-synth: export RULES_JSON = $(DESIGN_DIR)/rules-base.json
+metadata-synth:
+ $(MAKE) synth
+ $(MAKE) metadata-generate
+ $(MAKE) metadata-check-synth
+
.PHONY: metadata-generate
metadata-generate:
mkdir -p $(REPORTS_DIR)
@@ -25,6 +38,16 @@ metadata-check:
-r $(RULES_JSON) 2>&1 \
| tee $(abspath $(REPORTS_DIR)/metadata-check.log)
+# Check only the synthesis-stage subset of RULES_JSON, so a
+# synthesis-only run can be gated by the design's full-flow rules file.
+.PHONY: metadata-check-synth
+metadata-check-synth:
+ $(PYTHON_EXE) $(UTILS_DIR)/checkMetadata.py \
+ -m $(REPORTS_DIR)/metadata.json \
+ -r $(RULES_JSON) \
+ --only-prefix synth__ constraints__ 2>&1 \
+ | tee $(abspath $(REPORTS_DIR)/metadata-check.log)
+
.PHONY: clean_metadata
clean_metadata:
rm -f $(REPORTS_DIR)/design-dir.txt