#!/usr/bin/env python3 """Publish and record a fresh immutable Linux ARM64 bundle without running the workload.""" import argparse from datetime import datetime, timezone import json from pathlib import Path import shutil import subprocess import sys import tempfile import xml.etree.ElementTree as ET sys.dont_write_bytecode = True from analyze import APP, RUNTIME, SETTINGS, digest, require from bundle import CANONICAL_SOURCES, TOOLS, check_application, context_identity, image_identity, manifest, verify_bundle HERE = Path(__file__).resolve().parent REPO = HERE.parents[3] PROJECT = REPO / "tests/Server" / APP / (APP + ".csproj") SUPPORT = PROJECT.with_name("Sdk2LifecycleLateSlopeTests.cs") MODULE_SOURCE = HERE.parent / "ArtifactModuleInfo.cs" DESIGN = HERE.parent / "LATE_SLOPE.md" def check_source(revision): current = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=REPO, text=True).strip() require(revision == current, "Publish the specified committed source revision") require(not subprocess.check_output(["git", "status", "--porcelain"], cwd=REPO, text=True).strip(), "The publication checkout must be clean") require('"late-slope-result"' in SUPPORT.read_text(encoding="utf-8"), "The committed canonical test lacks late-slope mode") sources = [*(REPO / name for name in CANONICAL_SOURCES), MODULE_SOURCE, DESIGN, HERE / "prepare.py", HERE / "test_offline.py", *(HERE / name for name in TOOLS)] return {path.relative_to(REPO).as_posix(): digest(path) for path in sources} def managed_modules(build, root): project = ET.Element("Project", Sdk="Microsoft.NET.Sdk") settings = ET.SubElement(project, "PropertyGroup") for key, value in {"OutputType": "Exe", "TargetFramework": "net10.0", "ImplicitUsings": "enable", "EnableDefaultCompileItems": "false", "TreatWarningsAsErrors": "true"}.items(): ET.SubElement(settings, key).text = value ET.SubElement(ET.SubElement(project, "ItemGroup"), "Compile", Include=str(MODULE_SOURCE)) path = build / "module-info.csproj" ET.ElementTree(project).write(path, encoding="unicode") with (root / "module-info-build.log").open("w", encoding="utf-8") as log: subprocess.run(["dotnet", "build", str(path), "-c", "Release", "-m:2", "--nologo"], stdout=log, stderr=subprocess.STDOUT, check=True, timeout=300) command = ["dotnet", str(build / "bin/Release/net10.0/module-info.dll"), str(root), str(root / "app")] return json.loads(subprocess.check_output(command, text=True, timeout=60)) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--source-revision", required=True) parser.add_argument("--image", required=True) parser.add_argument("--docker-context", required=True) options = parser.parse_args() sources = check_source(options.source_revision) context = context_identity(options.docker_context) image = image_identity(options.image, context) root = options.output.resolve() root.mkdir(mode=0o700, parents=True, exist_ok=False) with tempfile.TemporaryDirectory(prefix="opcua-late-slope-publish-") as scratch: build = Path(scratch) command = ["dotnet", "publish", str(PROJECT), "-c", "Release", "-r", "linux-arm64", "--self-contained", "true", f"-p:RuntimeFrameworkVersion={RUNTIME}", "-p:PublishSingleFile=false", "-p:PublishTrimmed=false", "-p:UseAppHost=true", "--artifacts-path", str(build / "artifacts"), "-o", str(root / "app"), "-m:2", "--nologo"] with (root / "publish.log").open("w", encoding="utf-8") as log: subprocess.run(command, cwd=REPO, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=900) sdk, gateway = check_application(root / "app") modules = managed_modules(build, root) tools = root / "tools" tools.mkdir() for name in TOOLS: shutil.copy2(HERE / name, tools / name) require(check_source(options.source_revision) == sources, "Committed inputs changed during publication") require(context_identity(options.docker_context) == context and image_identity(options.image, context) == image, "The local Docker context or image changed during publication") receipt = {**SETTINGS, "kind": "bundle", "sourceRevision": options.source_revision, "sourceFiles": sources, "dockerContext": context, "image": options.image, "imageIdentity": image, "preparedUtc": datetime.now(timezone.utc).isoformat(), "publishCommand": command, "sdkLibraries": sdk, "gatewayLibraries": gateway, "managedModules": modules, "files": manifest(root)} path = root / "receipt.json" path.write_text(json.dumps(receipt, indent=2, allow_nan=False) + "\n", encoding="utf-8") sha = digest(path) verify_bundle(root, sha) print(json.dumps({"bundle": str(root), "sourceRevision": options.source_revision, "receiptSha256": sha, "acceptance": False, "workloadLaunched": False}, indent=2)) if __name__ == "__main__": main()