From 54237ddc7d9bfd334c7e94536c4a04e75baf79d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erick=20Andr=C3=A9s=20Obreg=C3=B3n=20Fonseca?= Date: Thu, 10 Sep 2026 19:20:20 -0600 Subject: [PATCH] [Add] Containerize reproducible OpenROAD execution - Add selectable local and containerized ORFS execution with an inmutable image digest, invoking-user ownership, configurable platform and variant, and isolated module or profile work directories. - Add a versioned module-owned physical-evidence policy that validates nonempty DEF, GDS, ODB, SDC, and netlist deliverables plus configurable timing, electrical, and routing thresholds. - Record container identity and hashes for physical artifacts, design configuration, constraints, reports, and policy inputs while preserving PASS, FAIL, and SKIP through the canonical flow contract. - Integrate OpenROAD evidence with the release manifests and add a cached Nangate45 GitHub Actions fixture with image, artifacts, metric, and ownership assertions. - Qualify concurrent execution, cache restoration, missing or empty artifacts, image configuration, unavailable runtimes, and nonzero violations, document the non-signoff boundary, and advance the methodology version to 0.8.0. --- .github/workflows/flow-quality.yml | 76 ++++ README.md | 2 + VERSION | 2 +- ci/cache_openroad_image.sh | 42 +++ ci/check_flow_quality.sh | 10 + ci/openroad_evidence.py | 355 ++++++++++++++++++ ci/release_manifest.py | 5 +- config/tool-versions.env | 3 + config/tools.mk | 25 +- docs/README.md | 12 +- docs/configuration.md | 19 +- docs/flows.md | 20 +- docs/getting-started.md | 2 + docs/openroad.md | 159 ++++++++ docs/release-evidence.md | 8 + flows/openroad/run.sh | 158 +++++++- mk/module.mk | 4 +- .../openroad-evidence-policy-v1.schema.json | 67 ++++ tests/fixture-module/config/design.mk | 5 + .../config/openroad-evidence.json | 55 +++ tests/fixture-module/config/openroad.mk | 8 +- tests/fixture-module/constraints/openroad.sdc | 5 + tests/test_openroad_evidence.sh | 271 +++++++++++++ 23 files changed, 1283 insertions(+), 30 deletions(-) create mode 100755 ci/cache_openroad_image.sh create mode 100755 ci/openroad_evidence.py create mode 100644 docs/openroad.md create mode 100644 schemas/openroad-evidence-policy-v1.schema.json create mode 100644 tests/fixture-module/config/openroad-evidence.json create mode 100644 tests/fixture-module/constraints/openroad.sdc create mode 100755 tests/test_openroad_evidence.sh diff --git a/.github/workflows/flow-quality.yml b/.github/workflows/flow-quality.yml index 3d312b8..08fdc3e 100644 --- a/.github/workflows/flow-quality.yml +++ b/.github/workflows/flow-quality.yml @@ -135,3 +135,79 @@ jobs: - name: Run containerized release-manifest fixture run: FIXTURE_CONTEXT=container tests/test_release_manifest.sh + + openroad-container: + name: Containerized Nangate45 physical fixture + runs-on: ubuntu-24.04 + timeout-minutes: 45 + + steps: + - name: Check out mosaic-flow + uses: actions/checkout@v4 + + - name: Cache pinned OpenROAD image layers + uses: actions/cache@v4 + with: + path: ~/.cache/mosaic/openroad-images + key: openroad-image-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('config/tool-versions.env') }} + + - name: Prepare pinned OpenROAD image + run: ci/cache_openroad_image.sh + + - name: Run Nangate45 fixture + run: | + make -C tests/fixture-module \ + FLOW_ROOT="${GITHUB_WORKSPACE}" \ + OPENROAD_EXECUTION_MODE=container \ + open-physical + + - name: Validate physical evidence and ownership + run: | + python3 - <<'PY' + import json + import pathlib + + evidence = json.loads( + pathlib.Path("tests/fixture-module/reports/openroad/evidence.json").read_text() + ) + assert evidence["status"] == "PASS" + assert evidence["design"]["platform"] == "nangate45" + assert evidence["execution"]["mode"] == "container" + assert evidence["execution"]["image"]["reference"].endswith( + "@" + evidence["execution"]["image"]["digest"] + ) + assert {item["name"] for item in evidence["artifacts"]} == { + "final_def", "final_gds", "final_netlist", "final_odb", "final_sdc" + } + assert all(item["passed"] for item in evidence["metrics"]) + PY + if find tests/fixture-module/work/openroad -not -user "$(id -un)" -print -quit | grep -q .; then + echo "Containerized OpenROAD produced files owned by another user" >&2 + exit 1 + fi + + - name: Index physical evidence in release manifest + run: | + make -C tests/fixture-module \ + FLOW_ROOT="${GITHUB_WORKSPACE}" \ + DISABLED_FLOWS="verible_lint verible_format slang_elaboration verilator_lint yosys_synthesis symbiyosys_formal eqy_equivalence verilator_sim pyuvm_open_source coverage_qualification negative_qualification four_state_qualification static_intent vcs_sim pyuvm_commercial vc_lint vc_cdc sg_cdc sg_dft vc_lp synopsys_synthesis synopsys_primetime synopsys_primepower" \ + MODULE_REVISION="${GITHUB_SHA}" \ + METHODOLOGY_REVISION="${GITHUB_SHA}" \ + RELEASE_EXECUTION_CONTEXT=container \ + RELEASE_TECHNOLOGY=nangate45 \ + release-manifest release-manifest-validate + grep -Fq 'reports/openroad/evidence.json' \ + tests/fixture-module/reports/release_manifest/container/manifest.json + + - name: Upload physical fixture evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: mosaic-flow-openroad-nangate45 + path: | + tests/fixture-module/reports/openroad/ + tests/fixture-module/reports/release_manifest/container/ + tests/fixture-module/work/openroad/results/ + tests/fixture-module/work/openroad/reports/ + tests/fixture-module/work/openroad/logs/ + if-no-files-found: error diff --git a/README.md b/README.md index 565561c..d6656d2 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ methodology without copying flow scripts or changing module RTL. - Declarative HDL and formal coverage qualification with reviewed exclusions - Declarative negative-test and four-state qualification with explicit controls - Declarative, license-independent SDC and UPF intent validation +- Reproducible local or pinned-container ORFS implementation with declarative physical evidence - Flow selection, statuses, reports, and quality gates - Schema-validated release evidence with input hashes and tool identities - Pinned open-source tool installers and versions @@ -40,6 +41,7 @@ reference, and links to: - Every open-source and commercial flow - Results, quality gates, waivers, and release evidence - Portable SDC and UPF intent checks and their signoff boundary +- Containerized OpenROAD execution and physical evidence policy - Methodology development, qualification, and release procedures ## Consumer quick start diff --git a/VERSION b/VERSION index faef31a..a3df0a6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.0 +0.8.0 diff --git a/ci/cache_openroad_image.sh b/ci/cache_openroad_image.sh new file mode 100755 index 0000000..7ef0922 --- /dev/null +++ b/ci/cache_openroad_image.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +flow_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=../config/tool-versions.env +source "${flow_root}/config/tool-versions.env" + +runtime="${OPENROAD_CONTAINER_RUNTIME:-docker}" +image="${OPENROAD_ORFS_IMAGE:-${ORFS_IMAGE_REPOSITORY}@${ORFS_IMAGE_DIGEST}}" +cache_root="${OPENROAD_IMAGE_CACHE_ROOT:-${HOME}/.cache/mosaic/openroad-images}" +archive="${cache_root}/${ORFS_IMAGE_DIGEST#sha256:}.tar" + +if ! command -v "${runtime}" >/dev/null 2>&1; then + echo "OpenROAD container runtime is unavailable: ${runtime}" >&2 + exit 2 +fi +if [[ ! "${image}" =~ @sha256:[0-9a-f]{64}$ ]]; then + echo "OpenROAD image cache requires an immutable @sha256 reference: ${image}" >&2 + exit 2 +fi + +if ! "${runtime}" image inspect "${image}" >/dev/null 2>&1 && [[ -s "${archive}" ]]; then + echo "Loading pinned OpenROAD image from ${archive}" + "${runtime}" load --input "${archive}" +fi + +if ! "${runtime}" image inspect "${image}" >/dev/null 2>&1; then + echo "Pulling pinned OpenROAD image: ${image}" + "${runtime}" pull "${image}" +else + echo "Pinned OpenROAD image is available: ${image}" +fi + +if [[ ! -s "${archive}" ]]; then + mkdir -p "${cache_root}" + temporary_archive="${archive}.tmp.$$" + trap 'rm -f "${temporary_archive:-}"' EXIT + echo "Saving pinned OpenROAD image layers to ${archive}" + "${runtime}" save --output "${temporary_archive}" "${image}" + mv "${temporary_archive}" "${archive}" + trap - EXIT +fi diff --git a/ci/check_flow_quality.sh b/ci/check_flow_quality.sh index 4608052..135689a 100755 --- a/ci/check_flow_quality.sh +++ b/ci/check_flow_quality.sh @@ -22,10 +22,12 @@ python3 -m py_compile ci/release_manifest.py python3 -m py_compile ci/coverage_qualification.py python3 -m py_compile ci/qualification_campaign.py python3 -m py_compile ci/static_intent.py +python3 -m py_compile ci/openroad_evidence.py python3 -m json.tool schemas/release-evidence-v1.schema.json >/dev/null python3 -m json.tool schemas/coverage-policy-v1.schema.json >/dev/null python3 -m json.tool schemas/qualification-campaigns-v1.schema.json >/dev/null python3 -m json.tool schemas/static-intent-v1.schema.json >/dev/null +python3 -m json.tool schemas/openroad-evidence-policy-v1.schema.json >/dev/null if ! grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' VERSION; then echo "VERSION must contain a semantic version such as 1.2.3" >&2 @@ -46,6 +48,8 @@ required_version_keys=( SHELLCHECK_SHA256 ACTIONLINT_VERSION ACTIONLINT_SHA256 + ORFS_IMAGE_REPOSITORY + ORFS_IMAGE_DIGEST ) source config/tool-versions.env for version_key in "${required_version_keys[@]}"; do @@ -55,6 +59,11 @@ for version_key in "${required_version_keys[@]}"; do fi done +if [[ ! "${ORFS_IMAGE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "ORFS_IMAGE_DIGEST must be an immutable sha256 digest" >&2 + exit 1 +fi + for requirement in \ "pyuvm==${PYUVM_VERSION}" \ "cocotb==${COCOTB_VERSION}" \ @@ -85,4 +94,5 @@ tests/test_release_manifest.sh tests/test_coverage_qualification.sh tests/test_qualification_campaigns.sh tests/test_static_intent.sh +tests/test_openroad_evidence.sh echo "mosaic-flow static quality checks passed" diff --git a/ci/openroad_evidence.py b/ci/openroad_evidence.py new file mode 100755 index 0000000..eddf019 --- /dev/null +++ b/ci/openroad_evidence.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +"""Validate module-owned OpenROAD deliverables and violation thresholds.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import sys +from pathlib import Path +from typing import Any + + +POLICY_SCHEMA = "mosaic-openroad-evidence-policy-v1" +RESULT_SCHEMA = "mosaic-openroad-evidence-result-v1" +NAME = re.compile(r"^[a-z][a-z0-9_]*$") +IMAGE_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +IMAGE_REFERENCE = re.compile(r"^\S+@sha256:[0-9a-f]{64}$") +DIRECTORIES = {"results", "reports", "logs", "objects"} + + +class EvidenceError(ValueError): + """Report invalid policy or missing physical evidence.""" + + +def sha256(path: Path) -> str: + """Hash one nonempty regular file.""" + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def checked_file(path: Path, label: str) -> Path: + """Require a nonempty regular file and return its resolved path.""" + resolved = path.resolve() + if not resolved.is_file(): + raise EvidenceError(f"Missing {label}: {resolved}") + if resolved.stat().st_size == 0: + raise EvidenceError(f"Empty {label}: {resolved}") + return resolved + + +def relative_record(path: Path, module_root: Path) -> dict[str, Any]: + """Describe a file with a stable module-relative path when possible.""" + resolved = path.resolve() + try: + display_path = resolved.relative_to(module_root.resolve()).as_posix() + except ValueError: + display_path = str(resolved) + return { + "path": display_path, + "bytes": resolved.stat().st_size, + "sha256": sha256(resolved), + } + + +def policy_path( + root: Path, + entry: dict[str, Any], + platform: str, + design: str, + variant: str, +) -> Path: + """Resolve a policy path below its declared ORFS evidence directory.""" + relative = Path(entry["path"]) + if relative.is_absolute() or ".." in relative.parts: + raise EvidenceError(f"Policy path must be relative and contained: {relative}") + return root / entry["directory"] / platform / design / variant / relative + + +def validate_policy(policy: object) -> dict[str, Any]: + """Validate the policy subset used by the dependency-free implementation.""" + if not isinstance(policy, dict) or set(policy) != { + "schema", + "artifacts", + "metrics", + }: + raise EvidenceError("OpenROAD policy root has invalid fields") + if policy["schema"] != POLICY_SCHEMA: + raise EvidenceError(f"OpenROAD policy schema must be {POLICY_SCHEMA}") + + for section in ("artifacts", "metrics"): + if not isinstance(policy[section], list) or not policy[section]: + raise EvidenceError(f"OpenROAD policy {section} must be a nonempty array") + + names: set[str] = set() + for index, artifact in enumerate(policy["artifacts"]): + if not isinstance(artifact, dict) or set(artifact) != { + "name", + "directory", + "path", + }: + raise EvidenceError(f"Artifact {index} has invalid fields") + validate_entry(artifact, f"Artifact {index}") + if artifact["name"] in names: + raise EvidenceError(f"Duplicate evidence name: {artifact['name']}") + names.add(artifact["name"]) + + for index, metric in enumerate(policy["metrics"]): + if not isinstance(metric, dict): + raise EvidenceError(f"Metric {index} must be an object") + allowed = { + "name", + "directory", + "path", + "pattern", + "match", + "minimum", + "maximum", + } + if not {"name", "directory", "path", "pattern"}.issubset(metric) or not set( + metric + ).issubset(allowed): + raise EvidenceError(f"Metric {index} has invalid fields") + validate_entry(metric, f"Metric {index}") + if metric["name"] in names: + raise EvidenceError(f"Duplicate evidence name: {metric['name']}") + names.add(metric["name"]) + if "minimum" not in metric and "maximum" not in metric: + raise EvidenceError(f"Metric {metric['name']} has no threshold") + if metric.get("match", "only") not in {"only", "first", "last"}: + raise EvidenceError( + f"Metric {metric['name']} match must be only, first, or last" + ) + for threshold in ("minimum", "maximum"): + if threshold in metric and ( + isinstance(metric[threshold], bool) + or not isinstance(metric[threshold], int) + or metric[threshold] < 0 + ): + raise EvidenceError( + f"Metric {metric['name']} {threshold} must be a nonnegative integer" + ) + try: + expression = re.compile(metric["pattern"], re.MULTILINE) + except (TypeError, re.error) as error: + raise EvidenceError( + f"Metric {metric['name']} has an invalid pattern: {error}" + ) from error + if "value" not in expression.groupindex: + raise EvidenceError( + f"Metric {metric['name']} pattern must define named group 'value'" + ) + return policy + + +def validate_entry(entry: dict[str, Any], label: str) -> None: + """Validate fields shared by artifact and metric entries.""" + if not isinstance(entry.get("name"), str) or not NAME.fullmatch(entry["name"]): + raise EvidenceError(f"{label} has an invalid name") + if entry.get("directory") not in DIRECTORIES: + raise EvidenceError(f"{label} has an invalid directory") + path = entry.get("path") + if ( + not isinstance(path, str) + or not path + or Path(path).is_absolute() + or ".." in Path(path).parts + ): + raise EvidenceError(f"{label} has an invalid relative path") + + +def load_policy(path: Path) -> dict[str, Any]: + """Read and validate one JSON physical evidence policy.""" + try: + raw = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise EvidenceError(f"Cannot read OpenROAD policy {path}: {error}") from error + return validate_policy(raw) + + +def qualify(args: argparse.Namespace) -> dict[str, Any]: + """Evaluate artifacts and metrics and return normalized evidence.""" + module_root = args.module_root.resolve() + evidence_root = args.evidence_root.resolve() + policy_file = checked_file(args.policy, "OpenROAD evidence policy") + design_config = checked_file(args.design_config, "OpenROAD design configuration") + constraint = checked_file(args.constraint, "OpenROAD constraint") + policy = load_policy(policy_file) + failures: list[str] = [] + artifacts: list[dict[str, Any]] = [] + metrics: list[dict[str, Any]] = [] + + for declaration in policy["artifacts"]: + path = policy_path( + evidence_root, + declaration, + args.platform, + args.design, + args.variant, + ) + try: + record = relative_record( + checked_file(path, f"required artifact {declaration['name']}"), + module_root, + ) + artifacts.append({"name": declaration["name"], **record}) + except EvidenceError as error: + failures.append(str(error)) + + source_records: dict[Path, dict[str, Any]] = {} + for declaration in policy["metrics"]: + path = policy_path( + evidence_root, + declaration, + args.platform, + args.design, + args.variant, + ) + value: int | None = None + passed = False + try: + source = checked_file(path, f"metric source {declaration['name']}") + text = source.read_text(encoding="utf-8", errors="replace") + expression = re.compile(declaration["pattern"], re.MULTILINE) + matches = list(expression.finditer(text)) + selection = declaration.get("match", "only") + if not matches or (selection == "only" and len(matches) != 1): + raise EvidenceError( + f"Metric {declaration['name']} expected " + f"{'one' if selection == 'only' else 'at least one'} match " + f"in {source}, " + f"found {len(matches)}" + ) + selected_match = matches[-1] if selection == "last" else matches[0] + raw_value = selected_match.group("value") + if not re.fullmatch(r"[0-9]+", raw_value): + raise EvidenceError( + f"Metric {declaration['name']} value is not a nonnegative integer: " + f"{raw_value!r}" + ) + value = int(raw_value) + passed = ( + ("minimum" not in declaration or value >= declaration["minimum"]) + and ("maximum" not in declaration or value <= declaration["maximum"]) + ) + if not passed: + bounds = [] + if "minimum" in declaration: + bounds.append(f"minimum {declaration['minimum']}") + if "maximum" in declaration: + bounds.append(f"maximum {declaration['maximum']}") + failures.append( + f"Metric {declaration['name']} is {value}, expected " + f"{' and '.join(bounds)}" + ) + source_records.setdefault(source, relative_record(source, module_root)) + except (EvidenceError, OSError) as error: + failures.append(str(error)) + + metric: dict[str, Any] = { + "name": declaration["name"], + "value": value, + "passed": passed, + "match": declaration.get("match", "only"), + "source": source_records.get(path.resolve(), {"path": str(path.resolve())}), + } + for threshold in ("minimum", "maximum"): + if threshold in declaration: + metric[threshold] = declaration[threshold] + metrics.append(metric) + + image: dict[str, str] | None = None + if args.execution_mode == "container": + if not IMAGE_REFERENCE.fullmatch(args.image_reference) or not IMAGE_DIGEST.fullmatch( + args.image_digest + ): + failures.append( + "Container evidence requires an immutable image reference and digest" + ) + elif not args.image_reference.endswith(f"@{args.image_digest}"): + failures.append( + "Container image reference digest does not match the recorded digest" + ) + image = { + "reference": args.image_reference, + "digest": args.image_digest, + "id": args.image_id, + } + + return { + "schema": RESULT_SCHEMA, + "status": "PASS" if not failures else "FAIL", + "design": { + "name": args.design, + "platform": args.platform, + "variant": args.variant, + }, + "execution": { + "mode": args.execution_mode, + "runtime": args.runtime, + "runtime_version": args.runtime_version, + "orfs_revision": args.orfs_revision, + "image": image, + }, + "inputs": { + "policy": relative_record(policy_file, module_root), + "design_config": relative_record(design_config, module_root), + "constraint": relative_record(constraint, module_root), + }, + "artifacts": artifacts, + "metrics": metrics, + "failures": failures, + } + + +def parser() -> argparse.ArgumentParser: + """Construct the command-line parser.""" + result = argparse.ArgumentParser(description=__doc__) + result.add_argument("--module-root", type=Path, required=True) + result.add_argument("--policy", type=Path, required=True) + result.add_argument("--evidence-root", type=Path, required=True) + result.add_argument("--design-config", type=Path, required=True) + result.add_argument("--constraint", type=Path, required=True) + result.add_argument("--output", type=Path, required=True) + result.add_argument( + "--execution-mode", choices=("local", "container"), required=True + ) + result.add_argument("--design", required=True) + result.add_argument("--platform", required=True) + result.add_argument("--variant", required=True) + result.add_argument("--runtime", default="") + result.add_argument("--runtime-version", default="") + result.add_argument("--orfs-revision", default="") + result.add_argument("--image-reference", default="") + result.add_argument("--image-digest", default="") + result.add_argument("--image-id", default="") + return result + + +def main() -> int: + """Run qualification and always retain a result for policy failures.""" + args = parser().parse_args() + args.output.parent.mkdir(parents=True, exist_ok=True) + try: + evidence = qualify(args) + except EvidenceError as error: + print(f"OpenROAD evidence error: {error}", file=sys.stderr) + return 2 + args.output.write_text( + json.dumps(evidence, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + if evidence["status"] != "PASS": + for failure in evidence["failures"]: + print(f"OpenROAD evidence failure: {failure}", file=sys.stderr) + return 1 + print(f"OpenROAD physical evidence passed: {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ci/release_manifest.py b/ci/release_manifest.py index 5ebde36..dc23adc 100755 --- a/ci/release_manifest.py +++ b/ci/release_manifest.py @@ -688,7 +688,10 @@ def generate_manifest(module_root: Path, flow_root: Path, report_dir: Path) -> d collect_static_intent_inputs(collector, module_root, required_flows) gates = collect_supplemental_gates(module_root, report_dir) additional_evidence = [] - for declared_path in environment_list("RELEASE_ADDITIONAL_EVIDENCE"): + for declared_path in ( + environment_list("RELEASE_STANDARD_EVIDENCE") + + environment_list("RELEASE_ADDITIONAL_EVIDENCE") + ): path = Path(declared_path) if not path.is_absolute(): path = module_root / path diff --git a/config/tool-versions.env b/config/tool-versions.env index a481a44..95058c4 100644 --- a/config/tool-versions.env +++ b/config/tool-versions.env @@ -12,3 +12,6 @@ SHELLCHECK_VERSION=0.11.0 SHELLCHECK_SHA256=8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198 ACTIONLINT_VERSION=1.7.12 ACTIONLINT_SHA256=8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 +# OpenROAD Flow Scripts is selected by an immutable OCI digest. +ORFS_IMAGE_REPOSITORY=openroad/orfs +ORFS_IMAGE_DIGEST=sha256:d995618be9f2bcdfa5538b885123463070dfbf178bea1818716d4652fe0fa380 diff --git a/config/tools.mk b/config/tools.mk index a5935d6..17d30df 100644 --- a/config/tools.mk +++ b/config/tools.mk @@ -48,8 +48,31 @@ export VERIBLE_FORMAT_ARGS ?= # VERIBLE_FORMAT_PATHS: Whitespace-separated module-owned files or directories # checked by the formatter. Multi-module profiles should narrow the default paths. export VERIBLE_FORMAT_PATHS ?=rtl verif -# OPENROAD_CMD: OpenROAD executable used by the physical implementation adapter. +# OPENROAD_CMD: OpenROAD executable reserved for direct tool integrations. export OPENROAD_CMD ?=openroad +# OPENROAD_EXECUTION_MODE: ORFS backend. local uses OPENROAD_FLOW_ROOT and +# container uses the immutable OPENROAD_ORFS_IMAGE through the selected runtime. +export OPENROAD_EXECUTION_MODE ?=local +# OPENROAD_FLOW_ROOT: Qualified local OpenROAD-flow-scripts checkout. Required +# only when OPENROAD_EXECUTION_MODE is local. +export OPENROAD_FLOW_ROOT ?= +# OPENROAD_CONTAINER_RUNTIME: OCI-compatible command used for container mode. +export OPENROAD_CONTAINER_RUNTIME ?=docker +# OPENROAD_ORFS_IMAGE: Immutable ORFS image assembled from the pinned repository +# and digest in tool-versions.env. Overrides must retain the @sha256 form. +export OPENROAD_ORFS_IMAGE ?=$(ORFS_IMAGE_REPOSITORY)@$(ORFS_IMAGE_DIGEST) +# OPENROAD_PLATFORM: ORFS public or site-owned platform selected by the module. +export OPENROAD_PLATFORM ?=nangate45 +# OPENROAD_FLOW_VARIANT: Stable ORFS output namespace for this configuration. +export OPENROAD_FLOW_VARIANT ?=base +# OPENROAD_DESIGN_NAME: ORFS design nickname and output directory component. +export OPENROAD_DESIGN_NAME ?=$(DESIGN_TOP) +# OPENROAD_CONSTRAINT_FILE: Exact module-owned SDC used by ORFS and evidence. +export OPENROAD_CONSTRAINT_FILE ?=$(SYNTHESIS_CONSTRAINT_FILE) +# OPENROAD_EVIDENCE_POLICY: Module-owned artifact and metric acceptance policy. +export OPENROAD_EVIDENCE_POLICY ?=$(MODULE_ROOT)/config/openroad-evidence.json +# OPENROAD_EVIDENCE_TOOL: Shared policy validator and evidence normalizer. +export OPENROAD_EVIDENCE_TOOL ?=$(FLOW_ROOT)/ci/openroad_evidence.py # VC_LINT_BIN: VC SpyGlass executable used by the licensed lint adapter. export VC_LINT_BIN ?=vc_static_shell # VC_CDC_BIN: VC SpyGlass executable used by the licensed CDC adapter. diff --git a/docs/README.md b/docs/README.md index edde811..48acb3d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,13 +24,15 @@ maintainers, and contributors to the methodology itself. named coverpoints, reviewed exclusions, and formal reachability. 8. [Negative-test and four-state qualification](qualification-campaigns.md) defines declarative fault campaigns, controls, diagnostics, and evidence. -9. [Results and quality gates](results-and-quality-gates.md) defines statuses, +9. [Containerized OpenROAD](openroad.md) defines local and pinned-container + execution, physical evidence policy, isolation, and the signoff boundary. +10. [Results and quality gates](results-and-quality-gates.md) defines statuses, waivers, generated artifacts, and CI behavior. -10. [Portable SDC and UPF intent](static-intent.md) defines the declarative +11. [Portable SDC and UPF intent](static-intent.md) defines the declarative constraint and power-intent gate and its signoff boundary. -11. [Release evidence](release-evidence.md) defines the manifest schema, +12. [Release evidence](release-evidence.md) defines the manifest schema, acceptance policy, extension points, and release integration. -12. [Methodology development](development.md) explains how to change, test, +13. [Methodology development](development.md) explains how to change, test, qualify, version, and release this repository. ## Quick reference @@ -56,6 +58,7 @@ maintainers, and contributors to the methodology itself. | Qualify HDL and formal coverage | [Coverage qualification](coverage-qualification.md) | | Qualify known faults and X/Z controls | [Qualification campaigns](qualification-campaigns.md) | | Validate portable SDC and UPF intent | `make open-static-intent` | +| Run pinned containerized OpenROAD | `make OPENROAD_EXECUTION_MODE=container open-physical` | | Inspect machine-readable status | Module `reports//status.txt` | | Add a methodology flow | [Methodology development](development.md#adding-a-flow) | @@ -72,6 +75,7 @@ most important implementation entry points are: - `ci/parameter_profiles.py` for parameter translation and profile evidence - `ci/release_manifest.py` for release evidence generation and validation - `ci/static_intent.py` for non-executing SDC and UPF command capture +- `ci/openroad_evidence.py` for physical artifact and metric qualification - `ci/run_flow.sh` for execution eligibility and status recording - `ci/*_quality_gate.sh` for acceptance policy - `flows//` for tool adapters diff --git a/docs/configuration.md b/docs/configuration.md index 5ce0be8..aee1ba6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -208,6 +208,8 @@ Flow-specific inputs are required when their flow is enabled: | `FORMAL_COVER_CONFIG` | SymbiYosys cover reachability task | | `EQUIVALENCE_CONFIG` | EQY | | `OPENROAD_CONFIG` | OpenROAD Flow Scripts | +| `OPENROAD_CONSTRAINT_FILE` | OpenROAD Flow Scripts and physical evidence | +| `OPENROAD_EVIDENCE_POLICY` | OpenROAD physical evidence qualification | | `SYNTHESIS_CONSTRAINT_FILE` | Module convention for synthesis SDC | | `CDC_CONFIG` | VC CDC or SpyGlass CDC adapter | | `DFT_CONFIG` | SpyGlass DFT adapter | @@ -344,7 +346,16 @@ that path until the adapter is changed to consume the variable directly. | `TARGET_LIBRARY` | Site or project target libraries when used by setup Tcl | | `LINK_LIBRARY` | Site or project link libraries when used by setup Tcl | | `OPERATING_CONDITION` | Requested timing or power corner when used by setup Tcl | -| `OPENROAD_FLOW_ROOT` | Checkout root of OpenROAD Flow Scripts | +| `OPENROAD_EXECUTION_MODE` | `local` checkout or pinned `container` execution | +| `OPENROAD_FLOW_ROOT` | Checkout root of OpenROAD Flow Scripts in local mode | +| `OPENROAD_CONTAINER_RUNTIME` | OCI runtime command, `docker` by default | +| `OPENROAD_ORFS_IMAGE` | Immutable `image@sha256:` ORFS reference | +| `OPENROAD_PLATFORM` | Selected ORFS platform, `nangate45` by default | +| `OPENROAD_FLOW_VARIANT` | Stable ORFS output variant, `base` by default | +| `OPENROAD_DESIGN_NAME` | Design nickname, `DESIGN_TOP` by default | +| `OPENROAD_CONFIG` | Module-owned ORFS Make configuration | +| `OPENROAD_CONSTRAINT_FILE` | Exact module-owned SDC supplied to ORFS | +| `OPENROAD_EVIDENCE_POLICY` | Module-owned artifact and metric policy | The shared Tcl currently sources `TECH_SETUP_TCL` when it is nonempty. The other technology variables are exported for the site setup to consume. Their exact @@ -383,9 +394,9 @@ make synopsys-synth | `PRIMETIME_BIN` | `pt_shell` | PrimeTime | | `PRIMEPOWER_BIN` | `pt_shell` | PrimePower | -The current OpenROAD wrapper invokes the OpenROAD Flow Scripts Makefile. It -uses `OPENROAD_FLOW_ROOT` and `OPENROAD_CONFIG` rather than invoking -`OPENROAD_CMD` directly. +The OpenROAD wrapper invokes the ORFS Makefile locally or through the immutable +container pin. `OPENROAD_CMD` remains reserved for direct integrations. See +[Containerized OpenROAD](openroad.md) for execution and evidence details. `VERIBLE_FORMAT_ARGS` provides whitespace-separated formatter options such as `--indentation_spaces=4`. `VERIBLE_FORMAT_PATHS` selects the module-owned files diff --git a/docs/flows.md b/docs/flows.md index a04a23f..92f6001 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -312,18 +312,24 @@ for the manifest schema, case semantics, evidence, and migration guidance. - **ID:** `openroad` - **Target:** `make open-physical` - **Adapter:** [`flows/openroad/run.sh`](../flows/openroad/run.sh) -- **Inputs:** `OPENROAD_FLOW_ROOT`, `OPENROAD_CONFIG`, PDK and platform collateral -- **Reports:** `reports/openroad/run.log`, `status.txt` -- **Work products:** managed by the selected OpenROAD Flow Scripts checkout +- **Inputs:** `OPENROAD_CONFIG`, `OPENROAD_CONSTRAINT_FILE`, + `OPENROAD_EVIDENCE_POLICY`, platform and PDK collateral +- **Reports:** `reports/openroad/run.log`, `evidence.json`, `status.txt` +- **Work products:** `work/openroad/{results,reports,logs,objects}////` This optional adapter invokes OpenROAD Flow Scripts with the module-owned design configuration. It is not part of `make open-source` because it requires a selected PDK, compatible libraries, LEF data, and physical constraints. -Set `OPENROAD_FLOW_ROOT` to a qualified OpenROAD Flow Scripts checkout. The -module configuration must select its platform, top, source files, SDC, and -physical targets. The exact physical result hierarchy is owned by OpenROAD Flow -Scripts rather than copied into the module's `work/openroad/` directory. +Set `OPENROAD_EXECUTION_MODE=local` and `OPENROAD_FLOW_ROOT` for a qualified +checkout, or select `container` to use the methodology's immutable ORFS image. +The module configuration selects its platform, top, source files, SDC, physical +targets, expected artifacts, parsed metrics, and acceptance thresholds. The +adapter isolates ORFS outputs below the selected module and profile work root, +runs a container with the invoking UID and GID, and hashes accepted outputs. + +See [Containerized OpenROAD](openroad.md) for the complete configuration, +policy, evidence, CI, and non-signoff contract. More information: [OpenROAD Flow](https://openroad-flow-scripts.readthedocs.io/en/latest/mainREADME.html) and its [configuration tutorial](https://openroad-flow-scripts.readthedocs.io/en/latest/tutorials/FlowTutorial.html). diff --git a/docs/getting-started.md b/docs/getting-started.md index df4455a..b165c55 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -82,6 +82,8 @@ export FORMAL_CONFIG := $(FLOW_CONFIG_ROOT)/symbiyosys/formal.sby export FORMAL_COVER_CONFIG := $(FLOW_CONFIG_ROOT)/symbiyosys/formal_cover.sby export EQUIVALENCE_CONFIG := $(FLOW_CONFIG_ROOT)/eqy/equivalence.eqy export OPENROAD_CONFIG := $(FLOW_CONFIG_ROOT)/openroad/config.mk +export OPENROAD_CONSTRAINT_FILE := $(FLOW_CONFIG_ROOT)/openroad/timing.sdc +export OPENROAD_EVIDENCE_POLICY := $(FLOW_CONFIG_ROOT)/openroad/evidence.json export SYNTHESIS_CONSTRAINT_FILE := $(FLOW_CONFIG_ROOT)/synthesis/timing.sdc export CDC_CONFIG := $(FLOW_CONFIG_ROOT)/cdc/constraints.tcl export DFT_CONFIG := $(FLOW_CONFIG_ROOT)/sg_dft/constraints.tcl diff --git a/docs/openroad.md b/docs/openroad.md new file mode 100644 index 0000000..23a3a4e --- /dev/null +++ b/docs/openroad.md @@ -0,0 +1,159 @@ +# Containerized OpenROAD Flow Scripts + +## Purpose and boundary + +The `openroad` adapter runs module-owned RTL and constraints through OpenROAD +Flow Scripts (ORFS). It supports a qualified local checkout and a pinned OCI +container while applying the same declarative physical evidence policy. + +The public Nangate45 fixture demonstrates reproducible exploratory synthesis, +placement, clock-tree synthesis, routing, and output generation. A `PASS` result +is not foundry signoff. It does not replace proprietary PDK validation, +extraction, signal-integrity analysis, physical verification, or signoff timing +and power tools. + +## Module-owned inputs + +Declare the physical configuration in the module's `config/design.mk`: + +```make +export OPENROAD_CONFIG := $(CURDIR)/config/openroad.mk +export OPENROAD_CONSTRAINT_FILE := $(CURDIR)/constraints/timing.sdc +export OPENROAD_EVIDENCE_POLICY := $(CURDIR)/config/openroad-evidence.json +export OPENROAD_PLATFORM := nangate45 +export OPENROAD_FLOW_VARIANT := release +export OPENROAD_DESIGN_NAME := $(DESIGN_TOP) +``` + +Both `OPENROAD_CONFIG` and `OPENROAD_CONSTRAINT_FILE` must resolve below +`MODULE_ROOT`. The adapter mounts the module read-only in container mode and +passes the selected SDC to ORFS explicitly, so the file hashed in the evidence +is the file used by implementation. + +An ORFS configuration remains module-owned. For example: + +```make +export DESIGN_NAME = my_module +export PLATFORM = $(OPENROAD_PLATFORM) +export VERILOG_FILES = $(REPO_ROOT)/rtl/my_module.sv +export SDC_FILE = $(REPO_ROOT)/constraints/timing.sdc +export DIE_AREA = 0 0 60 60 +export CORE_AREA = 5 5 55 55 +``` + +## Physical evidence policy + +The policy uses the versioned +[`mosaic-openroad-evidence-policy-v1`](../schemas/openroad-evidence-policy-v1.schema.json) +contract. Artifact paths are relative to the selected ORFS result, report, log, +or object directory. Every required artifact must exist and be nonempty. + +```json +{ + "schema": "mosaic-openroad-evidence-policy-v1", + "artifacts": [ + {"name": "final_def", "directory": "results", "path": "6_final.def"}, + {"name": "final_gds", "directory": "results", "path": "6_final.gds"}, + {"name": "final_odb", "directory": "results", "path": "6_final.odb"}, + {"name": "final_sdc", "directory": "results", "path": "6_final.sdc"}, + {"name": "final_netlist", "directory": "results", "path": "6_final.v"} + ], + "metrics": [ + { + "name": "setup_violations", + "directory": "reports", + "path": "6_finish.rpt", + "pattern": "setup violation count[ \\t]+(?P[0-9]+)", + "maximum": 0 + }, + { + "name": "routing_violations", + "directory": "logs", + "path": "5_2_route.log", + "pattern": "Number of violations = (?P[0-9]+)", + "match": "last", + "maximum": 0 + } + ] +} +``` + +Each metric regular expression must contain exactly one named integer group +called `value`. `match` defaults to `only` and may select `first` or `last` when +a tool logs an iterative metric multiple times. A metric may declare `minimum`, +`maximum`, or both. Modules normally declare setup, hold, slew, fanout, +capacitance, and routing violation counts, but their report paths and thresholds +remain policy rather than shared script constants. + +## Container execution + +Container mode defaults to the immutable ORFS image digest in +`config/tool-versions.env`: + +```sh +make OPENROAD_EXECUTION_MODE=container open-physical +``` + +`OPENROAD_CONTAINER_RUNTIME` defaults to `docker` and may select a compatible +runtime. `OPENROAD_ORFS_IMAGE` may override the image only with an +`image@sha256:` reference. A missing runtime or floating image fails +before implementation. + +The container runs with the invoking UID and GID. `MODULE_ROOT` is mounted +read-only at `/workspace`, while the profile-qualified `WORK_DIR/openroad` is +mounted read-write at `/orfs-work`. ORFS places results, reports, logs, and +objects below that root using platform, design, and variant components. Module +selection and parameter-profile selection already qualify `WORK_DIR`, so +concurrent jobs do not share physical databases. + +CI can prepare the pinned image and retain its layers with: + +```sh +ci/cache_openroad_image.sh +``` + +The cache archive name is derived from the immutable image digest. Changing the +methodology pin therefore creates a new cache entry instead of silently reusing +a floating ORFS version. + +## Local execution + +Local mode preserves the checkout-based workflow: + +```sh +make \ + OPENROAD_EXECUTION_MODE=local \ + OPENROAD_FLOW_ROOT="$HOME/tools/OpenROAD-flow-scripts" \ + open-physical +``` + +The checkout must contain `flow/Makefile`. When it is a Git repository, its +resolved revision is recorded in evidence. Local ORFS dependencies and platform +collateral remain the developer or site administrator's responsibility. + +## Outputs and release evidence + +The adapter writes: + +```text +reports/openroad/ +|-- evidence.json +|-- run.log +`-- status.txt + +work/openroad/ +|-- logs//// +|-- objects//// +|-- reports//// +`-- results//// +``` + +`evidence.json` records execution mode, platform, design, variant, local ORFS +revision or container identity, runtime version, configuration and SDC hashes, +artifact sizes and hashes, parsed metrics, thresholds, and failures. Container +evidence retains both the configured immutable reference and the resolved local +image ID. + +When `openroad` is required, `make release-manifest` automatically hashes the +policy as an input and indexes `reports/openroad/evidence.json`. Missing physical +evidence then fails release-manifest generation. diff --git a/docs/release-evidence.md b/docs/release-evidence.md index 0dde43e..59f9e1b 100644 --- a/docs/release-evidence.md +++ b/docs/release-evidence.md @@ -137,6 +137,14 @@ When enabled, `static_intent` is recorded as an independent required canonical flow. Its evidence establishes portable structural intent only and does not replace STA or VC LP signoff evidence. +When `openroad` is enabled and not explicitly disabled for the release, the +generator hashes `OPENROAD_EVIDENCE_POLICY`, the selected ORFS configuration, +and the exact SDC. It also requires and indexes +`reports/openroad/evidence.json`. That compact record includes the immutable +container identity or local ORFS revision, parsed physical metrics, and hashes +for every required deliverable. A passing exploratory ORFS run is not foundry +signoff. + Register another tool version command with JSON: ```make diff --git a/flows/openroad/run.sh b/flows/openroad/run.sh index 0c769cf..1b35ecd 100755 --- a/flows/openroad/run.sh +++ b/flows/openroad/run.sh @@ -1,11 +1,155 @@ #!/usr/bin/env bash +set -euo pipefail + +# shellcheck source=../common/env.sh source "$(dirname "$0")/../common/env.sh" -if [[ -z "${OPENROAD_FLOW_ROOT:-}" ]]; then - echo "OPENROAD_FLOW_ROOT must point to an OpenROAD-flow-scripts checkout." >&2 - echo "Run this optional PDK-backed flow with OPENROAD_FLOW_ROOT and OPENROAD_PLATFORM set." >&2 - exit 2 -fi +flow_report_dir="${REPORT_DIR}/openroad" +orfs_work_home="${WORK_DIR}/openroad" +config_relative="" +constraint_relative="" +module_root_real="" +execution_mode="" + +module_relative_path() { + local candidate="$1" + local resolved + resolved="$(realpath -e "${candidate}")" + if [[ "${resolved}" != "${module_root_real}/"* ]]; then + echo "OpenROAD input must be owned by MODULE_ROOT: ${candidate}" >&2 + return 2 + fi + printf '%s\n' "${resolved#"${module_root_real}/"}" +} + +execution_runtime="" +runtime_version="" +orfs_revision="" +image_reference="" +image_digest="" +image_id="" + +prepare_openroad() { + local required_openroad_vars=( + OPENROAD_CONFIG + OPENROAD_CONSTRAINT_FILE + OPENROAD_EVIDENCE_POLICY + OPENROAD_EVIDENCE_TOOL + OPENROAD_PLATFORM + OPENROAD_FLOW_VARIANT + OPENROAD_DESIGN_NAME + ) + local variable_name + + for variable_name in "${required_openroad_vars[@]}"; do + if [[ -z "${!variable_name:-}" ]]; then + echo "Missing OpenROAD environment variable: ${variable_name}" >&2 + return 2 + fi + done + execution_mode="${OPENROAD_EXECUTION_MODE:-local}" + module_root_real="$(realpath -e "${MODULE_ROOT}")" || return $? + config_relative="$(module_relative_path "${OPENROAD_CONFIG}")" || return $? + constraint_relative="$(module_relative_path "${OPENROAD_CONSTRAINT_FILE}")" || return $? + module_relative_path "${OPENROAD_EVIDENCE_POLICY}" >/dev/null || return $? + mkdir -p "${flow_report_dir}" "${orfs_work_home}" +} + +run_orfs_local() { + if [[ -z "${OPENROAD_FLOW_ROOT:-}" || \ + ! -f "${OPENROAD_FLOW_ROOT}/flow/Makefile" ]]; then + echo "OPENROAD_FLOW_ROOT must identify an OpenROAD-flow-scripts checkout in local mode." >&2 + return 2 + fi + orfs_revision="$(git -C "${OPENROAD_FLOW_ROOT}" rev-parse HEAD 2>/dev/null || true)" + make -C "${OPENROAD_FLOW_ROOT}/flow" \ + DESIGN_CONFIG="${OPENROAD_CONFIG}" \ + DESIGN_NICKNAME="${OPENROAD_DESIGN_NAME}" \ + FLOW_VARIANT="${OPENROAD_FLOW_VARIANT}" \ + OPENROAD_PLATFORM="${OPENROAD_PLATFORM}" \ + PLATFORM="${OPENROAD_PLATFORM}" \ + REPO_ROOT="${module_root_real}" \ + SDC_FILE="${OPENROAD_CONSTRAINT_FILE}" \ + WORK_HOME="${orfs_work_home}" || return $? +} + +run_orfs_container() { + execution_runtime="${OPENROAD_CONTAINER_RUNTIME:-docker}" + image_reference="${OPENROAD_ORFS_IMAGE:-}" + if [[ ! "${image_reference}" =~ @(sha256:[0-9a-f]{64})$ ]]; then + echo "OPENROAD_ORFS_IMAGE must use an immutable @sha256 digest: ${image_reference}" >&2 + return 2 + fi + image_digest="${BASH_REMATCH[1]}" + if ! command -v "${execution_runtime}" >/dev/null 2>&1; then + echo "OpenROAD container runtime is unavailable: ${execution_runtime}" >&2 + return 2 + fi + runtime_version="$(${execution_runtime} --version | head -n 1)" || return $? + if ! "${execution_runtime}" image inspect "${image_reference}" >/dev/null 2>&1; then + "${execution_runtime}" pull "${image_reference}" || return $? + fi + image_id="$( + "${execution_runtime}" image inspect "${image_reference}" --format '{{.Id}}' + )" || return $? + + # Variables in the final command are intentionally expanded by the container. + # shellcheck disable=SC2016 + "${execution_runtime}" run --rm \ + --user "$(id -u):$(id -g)" \ + --env HOME=/tmp \ + --env MOSAIC_DESIGN_CONFIG="/workspace/${config_relative}" \ + --env MOSAIC_DESIGN_NAME="${OPENROAD_DESIGN_NAME}" \ + --env MOSAIC_FLOW_VARIANT="${OPENROAD_FLOW_VARIANT}" \ + --env MOSAIC_PLATFORM="${OPENROAD_PLATFORM}" \ + --env MOSAIC_SDC_FILE="/workspace/${constraint_relative}" \ + --mount "type=bind,src=${module_root_real},dst=/workspace,readonly" \ + --mount "type=bind,src=$(realpath -e "${orfs_work_home}"),dst=/orfs-work" \ + --entrypoint bash \ + "${image_reference}" \ + -lc 'source /OpenROAD-flow-scripts/env.sh && make -C /OpenROAD-flow-scripts/flow \ + DESIGN_CONFIG="${MOSAIC_DESIGN_CONFIG}" \ + DESIGN_NICKNAME="${MOSAIC_DESIGN_NAME}" \ + FLOW_VARIANT="${MOSAIC_FLOW_VARIANT}" \ + OPENROAD_PLATFORM="${MOSAIC_PLATFORM}" \ + PLATFORM="${MOSAIC_PLATFORM}" \ + REPO_ROOT=/workspace \ + SDC_FILE="${MOSAIC_SDC_FILE}" \ + WORK_HOME=/orfs-work' || return $? +} + +qualify_openroad() { + prepare_openroad || return $? + case "${execution_mode}" in + local) + run_orfs_local + ;; + container) + run_orfs_container + ;; + *) + echo "OPENROAD_EXECUTION_MODE must be local or container, found: ${execution_mode}" >&2 + return 2 + ;; + esac + + "${OPENROAD_EVIDENCE_TOOL}" \ + --module-root "${MODULE_ROOT}" \ + --policy "${OPENROAD_EVIDENCE_POLICY}" \ + --evidence-root "${orfs_work_home}" \ + --design-config "${OPENROAD_CONFIG}" \ + --constraint "${OPENROAD_CONSTRAINT_FILE}" \ + --output "${flow_report_dir}/evidence.json" \ + --execution-mode "${execution_mode}" \ + --design "${OPENROAD_DESIGN_NAME}" \ + --platform "${OPENROAD_PLATFORM}" \ + --variant "${OPENROAD_FLOW_VARIANT}" \ + --runtime "${execution_runtime}" \ + --runtime-version "${runtime_version}" \ + --orfs-revision "${orfs_revision}" \ + --image-reference "${image_reference}" \ + --image-digest "${image_digest}" \ + --image-id "${image_id}" +} -run_and_record openroad make -C "${OPENROAD_FLOW_ROOT}/flow" \ - DESIGN_CONFIG="${OPENROAD_CONFIG}" +run_and_record openroad qualify_openroad diff --git a/mk/module.mk b/mk/module.mk index 773a28a..5d0a6f6 100644 --- a/mk/module.mk +++ b/mk/module.mk @@ -126,6 +126,7 @@ RELEASE_STANDARD_INPUTS := \ $(if $(filter enabled,$(FLOW_static_intent)),$(STATIC_INTENT_CONFIG)) \ $(EQUIVALENCE_CONFIG) \ $(OPENROAD_CONFIG) \ + $(if $(filter openroad,$(DISABLED_FLOWS)),,$(if $(filter enabled,$(FLOW_openroad)),$(OPENROAD_EVIDENCE_POLICY))) \ $(SYNTHESIS_CONSTRAINT_FILE) \ $(ASYNC_SYNTHESIS_CONSTRAINT_FILE) \ $(OPENROAD_CONSTRAINT_FILE) \ @@ -143,6 +144,7 @@ RELEASE_FILELISTS := $(sort $(strip \ $(COVERAGE_FILELIST) \ $(PYUVM_FILELIST))) RELEASE_INPUT_FILES := $(sort $(strip $(RELEASE_STANDARD_INPUTS) $(RELEASE_ADDITIONAL_INPUTS))) +RELEASE_STANDARD_EVIDENCE := $(if $(filter openroad,$(DISABLED_FLOWS)),,$(if $(filter enabled,$(FLOW_openroad)),$(REPORT_DIR)/openroad/evidence.json)) export RELEASE_MANIFEST_TOOL RELEASE_MODULE_NAME MODULE_REVISION METHODOLOGY_REVISION export RELEASE_EXECUTION_CONTEXT RELEASE_ALLOW_DIRTY RELEASE_TECHNOLOGY @@ -151,7 +153,7 @@ export RELEASE_TECHNOLOGY_METADATA_JSON RELEASE_METADATA_JSON export RELEASE_EXECUTION_METADATA_JSON RELEASE_ADDITIONAL_TOOLS_JSON export RELEASE_COVERAGE_EVIDENCE_JSON RELEASE_SUPPLEMENTAL_GATES export RELEASE_ADDITIONAL_INPUTS RELEASE_ADDITIONAL_EVIDENCE RELEASE_MANIFEST_DIR -export RELEASE_FILELISTS RELEASE_INPUT_FILES +export RELEASE_FILELISTS RELEASE_INPUT_FILES RELEASE_STANDARD_EVIDENCE OPEN_SOURCE_TARGETS := open-source open-style-lint open-format-check open-elaborate open-lint open-waiver-draft open-synth open-formal open-equivalence open-sim open-pyuvm open-coverage open-negative open-four-state open-static-intent open-quality-gate OPEN_FLOW_TARGETS := open-style-lint open-format-check open-elaborate open-lint open-synth open-formal open-equivalence open-sim open-pyuvm open-coverage open-negative open-four-state open-static-intent diff --git a/schemas/openroad-evidence-policy-v1.schema.json b/schemas/openroad-evidence-policy-v1.schema.json new file mode 100644 index 0000000..45c9858 --- /dev/null +++ b/schemas/openroad-evidence-policy-v1.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ecaslab.github.io/mosaic-flow/schemas/openroad-evidence-policy-v1.schema.json", + "title": "MOSAIC OpenROAD physical evidence policy", + "type": "object", + "additionalProperties": false, + "required": ["schema", "artifacts", "metrics"], + "properties": { + "schema": {"const": "mosaic-openroad-evidence-policy-v1"}, + "artifacts": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/artifact"} + }, + "metrics": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/metric"} + } + }, + "$defs": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + }, + "directory": { + "enum": ["results", "reports", "logs", "objects"] + }, + "relativePath": { + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?!.*(^|/)\\.\\.(/|$)).+$" + }, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": ["name", "directory", "path"], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "directory": {"$ref": "#/$defs/directory"}, + "path": {"$ref": "#/$defs/relativePath"} + } + }, + "metric": { + "type": "object", + "additionalProperties": false, + "required": ["name", "directory", "path", "pattern"], + "anyOf": [ + {"required": ["minimum"]}, + {"required": ["maximum"]} + ], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "directory": {"$ref": "#/$defs/directory"}, + "path": {"$ref": "#/$defs/relativePath"}, + "pattern": { + "type": "string", + "minLength": 1, + "description": "Python regular expression containing one named integer group called value." + }, + "match": {"enum": ["only", "first", "last"]}, + "minimum": {"type": "integer", "minimum": 0}, + "maximum": {"type": "integer", "minimum": 0} + } + } + } +} diff --git a/tests/fixture-module/config/design.mk b/tests/fixture-module/config/design.mk index 3d8271e..18a6266 100644 --- a/tests/fixture-module/config/design.mk +++ b/tests/fixture-module/config/design.mk @@ -31,6 +31,11 @@ export QUALIFICATION_CAMPAIGN_MANIFEST := $(CURDIR)/config/qualification-campaig export STATIC_INTENT_CONFIG := $(CURDIR)/config/static-intent.json export EQUIVALENCE_CONFIG := $(CURDIR)/config/equivalence.eqy export OPENROAD_CONFIG := $(CURDIR)/config/openroad.mk +export OPENROAD_CONSTRAINT_FILE := $(CURDIR)/constraints/openroad.sdc +export OPENROAD_EVIDENCE_POLICY := $(CURDIR)/config/openroad-evidence.json +export OPENROAD_PLATFORM := nangate45 +export OPENROAD_FLOW_VARIANT := fixture_release +export OPENROAD_DESIGN_NAME := $(DESIGN_TOP) export CONSTRAINT_DIR := $(CURDIR)/constraints export REPORT_DIR := $(CURDIR)/reports export WORK_DIR := $(CURDIR)/work diff --git a/tests/fixture-module/config/openroad-evidence.json b/tests/fixture-module/config/openroad-evidence.json new file mode 100644 index 0000000..49b8b21 --- /dev/null +++ b/tests/fixture-module/config/openroad-evidence.json @@ -0,0 +1,55 @@ +{ + "schema": "mosaic-openroad-evidence-policy-v1", + "artifacts": [ + {"name": "final_def", "directory": "results", "path": "6_final.def"}, + {"name": "final_gds", "directory": "results", "path": "6_final.gds"}, + {"name": "final_odb", "directory": "results", "path": "6_final.odb"}, + {"name": "final_sdc", "directory": "results", "path": "6_final.sdc"}, + {"name": "final_netlist", "directory": "results", "path": "6_final.v"} + ], + "metrics": [ + { + "name": "setup_violations", + "directory": "reports", + "path": "6_finish.rpt", + "pattern": "^setup violation count[ \\t]+(?P[0-9]+)$", + "maximum": 0 + }, + { + "name": "hold_violations", + "directory": "reports", + "path": "6_finish.rpt", + "pattern": "^hold violation count[ \\t]+(?P[0-9]+)$", + "maximum": 0 + }, + { + "name": "slew_violations", + "directory": "reports", + "path": "6_finish.rpt", + "pattern": "^max slew violation count[ \\t]+(?P[0-9]+)$", + "maximum": 0 + }, + { + "name": "fanout_violations", + "directory": "reports", + "path": "6_finish.rpt", + "pattern": "^max fanout violation count[ \\t]+(?P[0-9]+)$", + "maximum": 0 + }, + { + "name": "capacitance_violations", + "directory": "reports", + "path": "6_finish.rpt", + "pattern": "^max cap violation count[ \\t]+(?P[0-9]+)$", + "maximum": 0 + }, + { + "name": "routing_violations", + "directory": "logs", + "path": "5_2_route.log", + "pattern": "Number of violations = (?P[0-9]+)", + "match": "last", + "maximum": 0 + } + ] +} diff --git a/tests/fixture-module/config/openroad.mk b/tests/fixture-module/config/openroad.mk index 88f045a..a617f42 100644 --- a/tests/fixture-module/config/openroad.mk +++ b/tests/fixture-module/config/openroad.mk @@ -1,7 +1,7 @@ export DESIGN_NAME = flow_fixture export PLATFORM = $(OPENROAD_PLATFORM) export VERILOG_FILES = $(REPO_ROOT)/rtl/flow_fixture.sv -export SDC_FILE = $(REPO_ROOT)/constraints/timing.sdc -export CORE_UTILIZATION = 35 -export CORE_ASPECT_RATIO = 1 -export CORE_MARGIN = 2 +export SDC_FILE = $(REPO_ROOT)/constraints/openroad.sdc +export DIE_AREA = 0 0 60 60 +export CORE_AREA = 5 5 55 55 +export SKIP_CTS_REPAIR_TIMING = 1 diff --git a/tests/fixture-module/constraints/openroad.sdc b/tests/fixture-module/constraints/openroad.sdc new file mode 100644 index 0000000..9e1101a --- /dev/null +++ b/tests/fixture-module/constraints/openroad.sdc @@ -0,0 +1,5 @@ +create_clock -name core_clk -period 10.000 [get_ports clk_i] +set_clock_uncertainty 0.100 [get_clocks core_clk] +set_input_delay 1.000 -clock core_clk [get_ports {enable_i data_i[*]}] +set_output_delay 1.000 -clock core_clk [get_ports data_o[*]] +set_false_path -from [get_ports rst_ni] diff --git a/tests/test_openroad_evidence.sh b/tests/test_openroad_evidence.sh new file mode 100755 index 0000000..532198f --- /dev/null +++ b/tests/test_openroad_evidence.sh @@ -0,0 +1,271 @@ +#!/usr/bin/env bash +set -euo pipefail + +flow_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +fixture_root="$(mktemp -d)" +trap 'rm -rf "${fixture_root}"' EXIT + +module_root="${fixture_root}/module" +orfs_root="${fixture_root}/orfs" +mkdir -p \ + "${module_root}/config" \ + "${module_root}/constraints" \ + "${module_root}/filelists" \ + "${module_root}/rtl" \ + "${module_root}/tools" \ + "${orfs_root}/flow" +printf 'module dut(input logic clk_i); endmodule\n' >"${module_root}/rtl/dut.sv" +printf '%s\n' "${module_root}/rtl/dut.sv" >"${module_root}/filelists/rtl.f" +cp "${module_root}/filelists/rtl.f" "${module_root}/filelists/tb.f" +printf 'create_clock -period 10 [get_ports clk_i]\n' >"${module_root}/constraints/timing.sdc" +cat >"${module_root}/config/openroad.mk" <<'EOF' +export DESIGN_NAME = dut +export PLATFORM = $(OPENROAD_PLATFORM) +export VERILOG_FILES = $(REPO_ROOT)/rtl/dut.sv +export SDC_FILE = $(REPO_ROOT)/constraints/timing.sdc +EOF +cat >"${module_root}/config/openroad-evidence.json" <<'EOF' +{ + "schema": "mosaic-openroad-evidence-policy-v1", + "artifacts": [ + {"name": "final_def", "directory": "results", "path": "6_final.def"}, + {"name": "final_gds", "directory": "results", "path": "6_final.gds"}, + {"name": "final_odb", "directory": "results", "path": "6_final.odb"}, + {"name": "final_sdc", "directory": "results", "path": "6_final.sdc"}, + {"name": "final_netlist", "directory": "results", "path": "6_final.v"} + ], + "metrics": [ + {"name": "setup_violations", "directory": "reports", "path": "6_finish.rpt", "pattern": "setup violation count +(?P[0-9]+)", "maximum": 0}, + {"name": "hold_violations", "directory": "reports", "path": "6_finish.rpt", "pattern": "hold violation count +(?P[0-9]+)", "maximum": 0}, + {"name": "slew_violations", "directory": "reports", "path": "6_finish.rpt", "pattern": "slew violation count +(?P[0-9]+)", "maximum": 0}, + {"name": "fanout_violations", "directory": "reports", "path": "6_finish.rpt", "pattern": "fanout violation count +(?P[0-9]+)", "maximum": 0}, + {"name": "capacitance_violations", "directory": "reports", "path": "6_finish.rpt", "pattern": "capacitance violation count +(?P[0-9]+)", "maximum": 0}, + {"name": "routing_violations", "directory": "logs", "path": "5_2_route.log", "pattern": "Number of violations = (?P[0-9]+)", "maximum": 0} + ] +} +EOF + +cat >"${orfs_root}/flow/Makefile" <<'EOF' +.RECIPEPREFIX := > +.PHONY: all +all: +>mkdir -p "$(WORK_HOME)/results/$(PLATFORM)/$(DESIGN_NICKNAME)/$(FLOW_VARIANT)" +>mkdir -p "$(WORK_HOME)/reports/$(PLATFORM)/$(DESIGN_NICKNAME)/$(FLOW_VARIANT)" +>mkdir -p "$(WORK_HOME)/logs/$(PLATFORM)/$(DESIGN_NICKNAME)/$(FLOW_VARIANT)" +>mkdir -p "$(WORK_HOME)/objects/$(PLATFORM)/$(DESIGN_NICKNAME)/$(FLOW_VARIANT)" +>for artifact in 6_final.def 6_final.gds 6_final.odb 6_final.sdc 6_final.v; do printf 'fixture\n' >"$(WORK_HOME)/results/$(PLATFORM)/$(DESIGN_NICKNAME)/$(FLOW_VARIANT)/$${artifact}"; done +>printf 'setup violation count 0\nhold violation count 0\nslew violation count 0\nfanout violation count 0\ncapacitance violation count 0\n' >"$(WORK_HOME)/reports/$(PLATFORM)/$(DESIGN_NICKNAME)/$(FLOW_VARIANT)/6_finish.rpt" +>printf 'Number of violations = 0\n' >"$(WORK_HOME)/logs/$(PLATFORM)/$(DESIGN_NICKNAME)/$(FLOW_VARIANT)/5_2_route.log" +EOF + +common_environment=( + "MODULE_ROOT=${module_root}" + "FLOW_ROOT=${flow_root}" + "DESIGN_TOP=dut" + "TB_TOP=dut_tb" + "FORMAL_TOP=dut_formal" + "DUT_INSTANCE=dut_tb/dut" + "RTL_FILELIST=${module_root}/filelists/rtl.f" + "TB_FILELIST=${module_root}/filelists/tb.f" + "CONSTRAINT_DIR=${module_root}/constraints" + "OPENROAD_CONFIG=${module_root}/config/openroad.mk" + "OPENROAD_CONSTRAINT_FILE=${module_root}/constraints/timing.sdc" + "OPENROAD_EVIDENCE_POLICY=${module_root}/config/openroad-evidence.json" + "OPENROAD_EVIDENCE_TOOL=${flow_root}/ci/openroad_evidence.py" + "OPENROAD_EXECUTION_MODE=local" + "OPENROAD_FLOW_ROOT=${orfs_root}" + "OPENROAD_PLATFORM=nangate45" +) + +run_profile() { + local profile="$1" + env "${common_environment[@]}" \ + OPENROAD_DESIGN_NAME="dut_${profile}" \ + OPENROAD_FLOW_VARIANT="${profile}" \ + REPORT_DIR="${module_root}/reports/${profile}" \ + WORK_DIR="${module_root}/work/${profile}" \ + "${flow_root}/flows/openroad/run.sh" +} + +# Concurrent profile runs must retain independent ORFS and normalized evidence. +run_profile narrow & +narrow_pid=$! +run_profile wide & +wide_pid=$! +wait "${narrow_pid}" +wait "${wide_pid}" +for profile in narrow wide; do + test "$(<"${module_root}/reports/${profile}/openroad/status.txt")" = PASS + grep -Fq '"status": "PASS"' \ + "${module_root}/reports/${profile}/openroad/evidence.json" + test -s "${module_root}/work/${profile}/openroad/results/nangate45/dut_${profile}/${profile}/6_final.gds" +done + +expect_failure() { + local expected="$1" + shift + if "$@" >"${fixture_root}/failure.log" 2>&1; then + echo "Expected OpenROAD failure containing: ${expected}" >&2 + exit 1 + fi + if ! grep -Fq "${expected}" "${fixture_root}/failure.log"; then + echo "OpenROAD failure did not contain: ${expected}" >&2 + cat "${fixture_root}/failure.log" >&2 + exit 1 + fi +} + +wide_root="${module_root}/work/wide/openroad" + +expect_failure "Container image reference digest does not match the recorded digest" \ + "${flow_root}/ci/openroad_evidence.py" \ + --module-root "${module_root}" \ + --policy "${module_root}/config/openroad-evidence.json" \ + --evidence-root "${wide_root}" \ + --design-config "${module_root}/config/openroad.mk" \ + --constraint "${module_root}/constraints/timing.sdc" \ + --output "${module_root}/reports/image-mismatch.json" \ + --execution-mode container --design dut_wide --platform nangate45 --variant wide \ + --image-reference "openroad/orfs@sha256:$(printf '0%.0s' {1..64})" \ + --image-digest "sha256:d995618be9f2bcdfa5538b885123463070dfbf178bea1818716d4652fe0fa380" +grep -Fq '"status": "FAIL"' "${module_root}/reports/image-mismatch.json" + +rm "${wide_root}/results/nangate45/dut_wide/wide/6_final.gds" +expect_failure "Missing required artifact final_gds" \ + "${flow_root}/ci/openroad_evidence.py" \ + --module-root "${module_root}" \ + --policy "${module_root}/config/openroad-evidence.json" \ + --evidence-root "${wide_root}" \ + --design-config "${module_root}/config/openroad.mk" \ + --constraint "${module_root}/constraints/timing.sdc" \ + --output "${module_root}/reports/missing-artifact.json" \ + --execution-mode local --design dut_wide --platform nangate45 --variant wide +grep -Fq '"status": "FAIL"' "${module_root}/reports/missing-artifact.json" + +: >"${wide_root}/results/nangate45/dut_wide/wide/6_final.gds" +expect_failure "Empty required artifact final_gds" \ + "${flow_root}/ci/openroad_evidence.py" \ + --module-root "${module_root}" \ + --policy "${module_root}/config/openroad-evidence.json" \ + --evidence-root "${wide_root}" \ + --design-config "${module_root}/config/openroad.mk" \ + --constraint "${module_root}/constraints/timing.sdc" \ + --output "${module_root}/reports/empty-artifact.json" \ + --execution-mode local --design dut_wide --platform nangate45 --variant wide +grep -Fq '"status": "FAIL"' "${module_root}/reports/empty-artifact.json" + +printf 'fixture\n' >"${wide_root}/results/nangate45/dut_wide/wide/6_final.gds" +printf 'Number of violations = 2\n' \ + >"${wide_root}/logs/nangate45/dut_wide/wide/5_2_route.log" +expect_failure "Metric routing_violations is 2, expected maximum 0" \ + "${flow_root}/ci/openroad_evidence.py" \ + --module-root "${module_root}" \ + --policy "${module_root}/config/openroad-evidence.json" \ + --evidence-root "${wide_root}" \ + --design-config "${module_root}/config/openroad.mk" \ + --constraint "${module_root}/constraints/timing.sdc" \ + --output "${module_root}/reports/routing-violation.json" \ + --execution-mode local --design dut_wide --platform nangate45 --variant wide + +printf 'Number of violations = 0\n' \ + >"${wide_root}/logs/nangate45/dut_wide/wide/5_2_route.log" +sed -i 's/setup violation count 0/setup violation count 3/' \ + "${wide_root}/reports/nangate45/dut_wide/wide/6_finish.rpt" +expect_failure "Metric setup_violations is 3, expected maximum 0" \ + "${flow_root}/ci/openroad_evidence.py" \ + --module-root "${module_root}" \ + --policy "${module_root}/config/openroad-evidence.json" \ + --evidence-root "${wide_root}" \ + --design-config "${module_root}/config/openroad.mk" \ + --constraint "${module_root}/constraints/timing.sdc" \ + --output "${module_root}/reports/setup-violation.json" \ + --execution-mode local --design dut_wide --platform nangate45 --variant wide + +expect_failure "must use an immutable @sha256 digest" env \ + "${common_environment[@]}" \ + OPENROAD_EXECUTION_MODE=container \ + OPENROAD_ORFS_IMAGE=openroad/orfs:latest \ + REPORT_DIR="${module_root}/reports/floating" \ + WORK_DIR="${module_root}/work/floating" \ + OPENROAD_DESIGN_NAME=dut \ + OPENROAD_FLOW_VARIANT=floating \ + "${flow_root}/flows/openroad/run.sh" +test "$(<"${module_root}/reports/floating/openroad/status.txt")" = FAIL + +expect_failure "container runtime is unavailable" env \ + "${common_environment[@]}" \ + OPENROAD_EXECUTION_MODE=container \ + OPENROAD_CONTAINER_RUNTIME=mosaic-missing-runtime \ + OPENROAD_ORFS_IMAGE=openroad/orfs@sha256:d995618be9f2bcdfa5538b885123463070dfbf178bea1818716d4652fe0fa380 \ + REPORT_DIR="${module_root}/reports/runtime" \ + WORK_DIR="${module_root}/work/runtime" \ + OPENROAD_DESIGN_NAME=dut \ + OPENROAD_FLOW_VARIANT=runtime \ + "${flow_root}/flows/openroad/run.sh" +test "$(<"${module_root}/reports/runtime/openroad/status.txt")" = FAIL + +skip_reports="${module_root}/reports/skip" +env \ + MODULE_ROOT="${module_root}" \ + REPORT_DIR="${skip_reports}" \ + MOSAIC_FLOW_IDS=openroad \ + FLOW_openroad=enabled \ + FLOW_DEPENDENCIES_openroad= \ + DISABLED_FLOWS=openroad \ + "${flow_root}/ci/run_flow.sh" openroad false >/dev/null +test "$(<"${skip_reports}/openroad/status.txt")" = SKIP +grep -Fq 'Disabled by DISABLED_FLOWS' \ + "${skip_reports}/openroad/skip_reason.txt" + +cat >"${module_root}/tools/fake-runtime" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +case "${1:-}" in + --version) + echo "fake-runtime 1.0" + ;; + image) + [[ "${2:-}" == inspect && -f "${FAKE_RUNTIME_STATE}" ]] + ;; + pull) + printf 'available\n' >"${FAKE_RUNTIME_STATE}" + ;; + save) + [[ "${2:-}" == --output ]] + printf 'cached layers\n' >"${3}" + ;; + load) + [[ "${2:-}" == --input && -s "${3}" ]] + printf 'available\n' >"${FAKE_RUNTIME_STATE}" + ;; + *) + echo "Unexpected fake runtime command: $*" >&2 + exit 2 + ;; +esac +EOF +chmod +x "${module_root}/tools/fake-runtime" +cache_root="${fixture_root}/image-cache" +runtime_state="${fixture_root}/runtime-state" +pinned_image="openroad/orfs@sha256:d995618be9f2bcdfa5538b885123463070dfbf178bea1818716d4652fe0fa380" +env \ + PATH="${module_root}/tools:${PATH}" \ + FAKE_RUNTIME_STATE="${runtime_state}" \ + OPENROAD_CONTAINER_RUNTIME=fake-runtime \ + OPENROAD_ORFS_IMAGE="${pinned_image}" \ + OPENROAD_IMAGE_CACHE_ROOT="${cache_root}" \ + "${flow_root}/ci/cache_openroad_image.sh" >/dev/null +archive="${cache_root}/d995618be9f2bcdfa5538b885123463070dfbf178bea1818716d4652fe0fa380.tar" +test -s "${archive}" +rm "${runtime_state}" +env \ + PATH="${module_root}/tools:${PATH}" \ + FAKE_RUNTIME_STATE="${runtime_state}" \ + OPENROAD_CONTAINER_RUNTIME=fake-runtime \ + OPENROAD_ORFS_IMAGE="${pinned_image}" \ + OPENROAD_IMAGE_CACHE_ROOT="${cache_root}" \ + "${flow_root}/ci/cache_openroad_image.sh" >/dev/null +test -s "${runtime_state}" + +echo "OpenROAD execution and physical evidence policy tests passed"