#!/usr/bin/env python3 """Prepare a fresh immutable candidate bundle. This command never launches the experiment.""" import argparse from datetime import datetime, timezone import hashlib import json from pathlib import Path import shutil import subprocess from build_saturation_observer import build def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--libraries", type=Path, required=True) parser.add_argument("--image", required=True) options = parser.parse_args() source = Path(__file__).resolve().parent repo = source.parents[2] revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip() if subprocess.check_output(["git", "status", "--porcelain"], cwd=repo, text=True).strip(): raise RuntimeError("Commit and review the exact source before preparing immutable artifacts") if not options.image.startswith("sha256:"): raise ValueError("Use an immutable Linux .NET SDK image digest") root = options.output.resolve() root.mkdir(parents=True, exist_ok=False) project = repo / "tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj" command = ["dotnet", "publish", str(project), "-c", "Release", "-r", "linux-arm64", "--self-contained", "true", "-p:RuntimeFrameworkVersion=10.0.14", "-p:PublishSingleFile=false", "-p:PublishTrimmed=false", "-p:UseAppHost=true", "--artifacts-path", str(root / "build"), "-o", str(root / "app"), "-m:2", "--nologo"] with (root / "publish.log").open("w") as log: subprocess.run(command, cwd=repo, stdout=log, stderr=subprocess.STDOUT, check=True) observer = build(root / "observer-build", options.libraries, True) shutil.copytree(observer, root / "observer") tools = root / "tools" tools.mkdir() for name in ["launch_saturation.py", "container_saturation.sh", "SATURATION.md"]: shutil.copy2(source / name, tools / name) manifest = {str(path.relative_to(root)): sha(path) for directory in ["app", "observer", "tools"] for path in sorted((root / directory).rglob("*")) if path.is_file()} for config in ["app/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.runtimeconfig.json", "observer/observer.runtimeconfig.json"]: runtime = json.loads((root / config).read_text()) included = runtime["runtimeOptions"]["includedFrameworks"] if not included or any(value["version"] != "10.0.14" for value in included): raise RuntimeError("Published participant is not the prospectively fixed self-contained runtime") dependencies = json.loads((root / "app/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.deps.json").read_text())["libraries"] gateway_libraries = [name for name in dependencies if name.startswith("ZB.MOM.WW.HistorianGateway.")] if not gateway_libraries or any(name.rsplit("/", 1)[-1] != "1.0.0" for name in gateway_libraries): raise RuntimeError("The reviewed candidate requires the landed HistorianGateway 1.0.0 graph") metadata = root / "module-info-build" metadata.mkdir() (metadata / "module-info.csproj").write_text( 'Exe' 'net10.0enable' 'false' '') with (metadata / "build.log").open("w") as log: subprocess.run(["dotnet", "build", str(metadata / "module-info.csproj"), "-c", "Release", "-m:2"], stdout=log, stderr=subprocess.STDOUT, check=True) modules = json.loads(subprocess.check_output(["dotnet", str(metadata / "bin/Release/net10.0/module-info.dll"), str(root), str(root / "app"), str(root / "observer")], text=True)) receipt = {"protocol": "sdk2-saturation-v1", "sourceRevision": revision, "image": options.image, "rid": "linux-arm64", "runtimeVersion": "10.0.14", "selfContained": True, "canonicalCycles": 384, "completedRestarts": 96, "settlingCycles": 1, "maximumPhaseSeconds": 1800, "windows": [[321, 352], [353, 384]], "observerBufferMiB": 16, "runtimeKeywords": "0x18", "rundown": False, "acceptance": False, "preparedUtc": datetime.now(timezone.utc).isoformat(), "publishCommand": command, "sdkLibraries": [name for name in dependencies if name.startswith("OPCFoundation.")], "gatewayLibraries": gateway_libraries, "managedModules": modules, "testSourceSha256": sha(project.parent / "Sdk2LifecycleSoakTests.cs"), "observerSourceSha256": sha(source / "SaturationObserver.cs"), "files": manifest} receipt_path = root / "receipt.json" receipt_path.write_text(json.dumps(receipt, indent=2) + "\n") print(json.dumps({"bundle": str(root), "receiptSha256": sha(receipt_path), "sourceRevision": revision, "launched": False}, indent=2)) if __name__ == "__main__": main()