#!/usr/bin/env python3 from __future__ import annotations import argparse import hashlib import json import shutil import subprocess import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from scripts.dependency_lock import DEFAULT_LOCK, load_lock # noqa: E402 def component(name: str, kind: str, spec: dict[str, object]) -> dict[str, object]: license_name = str(spec.get("license", "NOASSERTION")) license_value = ( {"name": license_name} if license_name == "NOASSERTION" or license_name.startswith("LicenseRef-") else {"id": license_name} ) item: dict[str, object] = { "type": "library" if kind != "toolchain" else "application", "name": name, "licenses": [{"license": license_value}], "externalReferences": [{"type": "distribution", "url": spec["url"]}], } if "revision" in spec: item["version"] = spec["revision"] item["properties"] = [{"name": "vcs:revision", "value": spec["revision"]}] if "nar_hash" in spec: item["properties"].append({"name": "nix:narHash", "value": spec["nar_hash"]}) else: item["version"] = spec.get("version", spec.get("sha256", "unknown")) item["hashes"] = [{"alg": "SHA-256", "content": spec["sha256"]}] return item def container_component(name: str, spec: dict[str, object]) -> dict[str, object]: license_name = str(spec.get("license", "NOASSERTION")) license_value = ( {"name": license_name} if license_name == "NOASSERTION" or license_name.startswith("LicenseRef-") else {"id": license_name} ) digest = str(spec["digest"]) return { "type": "container", "name": f"container/{name}", "version": digest.removeprefix("sha256:"), "licenses": [{"license": license_value}], "externalReferences": [{"type": "distribution", "url": spec["url"]}], "hashes": [{"alg": "SHA-256", "content": digest.removeprefix("sha256:")}], "properties": [{"name": "oci:image", "value": spec["image"]}], } def make_sbom(lock: dict[str, object]) -> dict[str, object]: components = [component(name, "source", spec) for name, spec in lock["sources"].items()] components.extend(component(name, "archive", spec) for name, spec in lock["archives"].items()) components.extend( container_component(name, spec) for name, spec in lock["container_images"].items() ) components.extend( component(f"nix/{name}", "source", spec) for name, spec in lock["nix_inputs"].items() ) for platform, tools in lock["toolchains"].items(): components.extend( component(f"{platform}/{name}", "toolchain", spec) for name, spec in tools.items() ) return { "bomFormat": "CycloneDX", "specVersion": "1.5", "version": 1, "components": components, } def main() -> int: parser = argparse.ArgumentParser(description="Create retroSoC source deliverables") parser.add_argument("--root", type=Path, required=True) parser.add_argument("--lock", type=Path, default=DEFAULT_LOCK) parser.add_argument("--variant-root", type=Path, required=True) parser.add_argument("--output-dir", type=Path, required=True) args = parser.parse_args() root = args.root.resolve() manifest_path = args.variant_root.resolve() / "meta/manifest.json" manifest = json.loads(manifest_path.read_text(encoding="utf-8")) config = manifest["configuration"] lock = load_lock(args.lock) soc = config.get("SOC", "MINI") product_root = root / "rtl" / soc.lower() args.output_dir.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory( prefix="retrosoc-package-", dir=args.output_dir.parent ) as temp: staging = Path(temp) / "package" staging.mkdir() timing_contract = Path(temp) / ("tiny_core.sdc" if soc == "TINY" else "commercial_timing_contract.tcl") timing_generator = (root / "physical/smoke/sta/opensta/generate_sdc.py" if soc == "TINY" else root / "physical/commercial/scripts/generate_timing_contract.py") if config.get("MINI_MODE", "PRODUCT") == "MPW": dynamic_core_filelist = ( args.variant_root / "generated/mpw" / config["SIMU"].lower() / "core/core.fl" ) dynamic_ip_filelist = ( args.variant_root / "generated/mpw" / config["SIMU"].lower() / "ip/ip.fl" ) else: dynamic_core_filelist = ( args.variant_root / "generated/user_extensions/legacy_core.fl" ) dynamic_ip_filelist = ( args.variant_root / "generated/user_extensions/legacy_ip.fl" ) subprocess.run( [ "python3", str(timing_generator), "--domains", str(product_root / "integration/clock_reset_domains.json"), "--pin-map", str(product_root / "pin_map/pin_map.json"), "--output", str(timing_contract), ], cwd=root, check=True, ) command = [ "python3", str(root / "physical/smoke/syn/tools/export_soc_sources.py"), "sv", "--output-dir", str(staging), "--pdk", config["PDK"], "--simu", config["SIMU"], "--jtag-idcode", config.get("JTAG_IDCODE", "DEADBEEF"), "--ext-clk-hz", str(config.get("EXT_CLK_HZ", 72_000_000)), "--aud-clk-hz", str(config.get("AUD_CLK_HZ", 18_432_000)), "--clint-timebase-hz", str(config.get("CLINT_TIMEBASE_HZ", 1_000_000)), "--dynamic-core-filelist", str(dynamic_core_filelist), "--dynamic-ip-filelist", str(dynamic_ip_filelist), "--memory-map-filelist", str(args.variant_root / "generated/memory_map/memory_map.fl"), "--soc-topology-filelist", str(args.variant_root / "generated/soc_topology/soc_topology.fl"), "--user-extensions-filelist", str(args.variant_root / "generated/user_extensions/user_extensions.fl"), "--pin-map-filelist", str(args.variant_root / "generated/pin_map/pin_map.fl"), "--archinfo-incdir", str(args.variant_root / "generated/archinfo"), "--metadata-file", f"contracts/{timing_contract.name}={timing_contract}", ] if soc == "TINY": if config.get("SYNTH") == "YOSYS": recipe = config.get("SYNTH_RECIPE", "balanced") folder = "yosys" if recipe == "balanced" else f"yosys-{recipe}" source_dir = args.variant_root / "syn" / folder / "filelists" else: source_dir = args.variant_root / "sim" / config["SIMU"].lower() / "filelists" command.extend(("--soc", "TINY", "--source-filelist-dir", str(source_dir))) for key, flag in ( ("HAVE_PLL", "--have-pll"), ("HAVE_SRAM_IF", "--have-sram-if"), ("HAVE_SRAM_MACRO", "--have-sram-macro"), ("HAVE_SVA", "--have-sva"), ): if config.get(key) == "YES": command.append(flag) subprocess.run(command, cwd=root, check=True) command[2] = "tar" subprocess.run(command, cwd=root, check=True) shutil.copy2(args.lock, staging / "dependencies.lock.json") shutil.copy2(manifest_path, staging / "manifest.json") (staging / "sbom.cdx.json").write_text( json.dumps(make_sbom(lock), indent=2, sort_keys=True) + "\n", encoding="utf-8" ) checksums = [] for path in sorted(item for item in staging.iterdir() if item.is_file()): checksums.append(f"{hashlib.sha256(path.read_bytes()).hexdigest()} {path.name}") (staging / "SHA256SUMS").write_text("\n".join(checksums) + "\n", encoding="utf-8") if args.output_dir.exists(): shutil.rmtree(args.output_dir) shutil.copytree(staging, args.output_dir) print(f"package: {args.output_dir.resolve()}") return 0 if __name__ == "__main__": raise SystemExit(main())