98 lines
5.1 KiB
Python
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()
|