Files
lmxopcua/scripts/validation/opcua-lifecycle-memory/late-slope/prepare.py
T
2026-11-10 17:59:55 -05:00

98 lines
5.1 KiB
Python

#!/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()