| 1 | #!/usr/bin/env python3 |
| 2 | """Export exact-image Native results from the existing full nextest invocation. |
| 3 | |
| 4 | This consumes JUnit output, never runs Rust or fabricates a runtime pass. The |
| 5 | completion markers are emitted by Rust only after their complete Native scenarios. Release |
| 6 | staging also requires the official CI run to be green at this exact source SHA. |
| 7 | """ |
| 8 | from __future__ import annotations |
| 9 | |
| 10 | import argparse |
| 11 | import hashlib |
| 12 | import json |
| 13 | import os |
| 14 | from pathlib import Path |
| 15 | import platform |
| 16 | import shutil |
| 17 | import sys |
| 18 | import xml.etree.ElementTree as ET |
| 19 | |
| 20 | COMPILED = "extension_host::tests::compiled_native_host_cannot_read_secrets_or_write_outside_its_data_dir" |
| 21 | MEMORY = "extension_host::tests::compiled_native_host_memory_cap_is_enforced" |
| 22 | WINDOWS = [ |
| 23 | "extension_host::windows::tests::windows_native_lpac_node_owner_boundary", |
| 24 | "extension_host::windows::tests::windows_native_lpac_bun_owner_boundary", |
| 25 | "extension_host::windows::tests::windows_native_lpac_compiled_owner_boundary", |
| 26 | "extension_host::windows::tests::windows_native_profiles_are_distinct_and_acl_grant_refuses_junctions", |
| 27 | "extension_host::windows::tests::windows_argv_and_environment_keep_exact_values_and_reject_nul", |
| 28 | "extension_host::windows::tests::windows_profile_retirement_removes_only_its_grants_and_inherited_data_on_restarts", |
| 29 | "extension_host::windows::tests::windows_profile_directory_budget_and_recorded_identity_refuse_before_overwrite", |
| 30 | ] |
| 31 | RUST_OS = {"linux": "linux", "darwin": "macos", "win32": "windows"} |
| 32 | RUST_ARCH = {"x64": "x86_64", "arm64": "aarch64"} |
| 33 | MAX_REPORT = 64 * 1024 * 1024 |
| 34 | MAX_LOG = 64 * 1024 |
| 35 | |
| 36 | |
| 37 | def digest(path: Path) -> str: |
| 38 | value = hashlib.sha256() |
| 39 | with path.open("rb") as source: |
| 40 | while chunk := source.read(1024 * 1024): |
| 41 | value.update(chunk) |
| 42 | return value.hexdigest() |
| 43 | |
| 44 | |
| 45 | def native_identity() -> tuple[str, str]: |
| 46 | native_platform = {"linux": "linux", "darwin": "darwin", "win32": "win32"}.get(sys.platform) |
| 47 | native_arch = {"x86_64": "x64", "amd64": "x64", "arm64": "arm64", "aarch64": "arm64"}.get(platform.machine().lower()) |
| 48 | if native_platform is None or native_arch is None: |
| 49 | raise ValueError("unsupported actual native runner") |
| 50 | return native_platform, native_arch |
| 51 | |
| 52 | |
| 53 | def extract(report: bytes, target_platform: str, target_arch: str) -> tuple[int, bytes]: |
| 54 | if len(report) > MAX_REPORT or b"<!DOCTYPE" in report or b"<!ENTITY" in report: |
| 55 | raise ValueError("invalid or oversized nextest report") |
| 56 | root = ET.fromstring(report) |
| 57 | required = [COMPILED, MEMORY, *(WINDOWS if target_platform == "win32" else [])] |
| 58 | records = {} |
| 59 | for case in root.iter("testcase"): |
| 60 | name = case.get("name", "") |
| 61 | if name not in required: |
| 62 | continue |
| 63 | if name in records: |
| 64 | raise ValueError(f"duplicate/retried Native testcase: {name}") |
| 65 | # quick-junit represents earlier attempt errors separately. No hidden |
| 66 | # retries, ignored cases, or successful return without the marker pass. |
| 67 | if any(child.tag in {"failure", "error", "skipped", "flakyFailure", "flakyError", "rerunFailure", "rerunError"} for child in case): |
| 68 | raise ValueError(f"Native testcase did not pass once: {name}") |
| 69 | output = "".join((child.text or "") for child in case if child.tag in {"system-out", "system-err"}) |
| 70 | records[name] = (case.get("time", "unknown"), output) |
| 71 | if set(records) != set(required): |
| 72 | raise ValueError("required Native testcase is missing: " + ", ".join(set(required) - set(records))) |
| 73 | for case, scenario in [(COMPILED, "containment"), (MEMORY, "memory")]: |
| 74 | marker = f"compiled-native-{scenario}=passed platform={RUST_OS[target_platform]} arch={RUST_ARCH[target_arch]}" |
| 75 | if marker not in records[case][1].splitlines(): |
| 76 | raise ValueError(f"compiled Native {scenario} completion marker is missing or belongs to another native target") |
| 77 | lines = ["scope=native-compiled-host", f"platform={target_platform} arch={target_arch}", f"junit_sha256={hashlib.sha256(report).hexdigest()}"] |
| 78 | for name in required: |
| 79 | time, output = records[name] |
| 80 | lines.extend([f"test={name} result=passed attempts=1 seconds={time}", output.rstrip()]) |
| 81 | log = ("\n".join(lines) + "\n").encode() |
| 82 | if len(log) > MAX_LOG: |
| 83 | raise ValueError("Native receipt log exceeds 64 KiB") |
| 84 | return len(required), log |
| 85 | |
| 86 | |
| 87 | def main() -> None: |
| 88 | parser = argparse.ArgumentParser(description=__doc__) |
| 89 | parser.add_argument("--junit", type=Path, required=True) |
| 90 | parser.add_argument("--compiled-image", type=Path, required=True) |
| 91 | parser.add_argument("--bundle", type=Path, required=True) |
| 92 | parser.add_argument("--bun", type=Path, required=True) |
| 93 | parser.add_argument("--source-sha", required=True) |
| 94 | parser.add_argument("--image-platform", required=True) |
| 95 | parser.add_argument("--image-arch", required=True) |
| 96 | parser.add_argument("--host-sha256", required=True) |
| 97 | parser.add_argument("--bundle-sha256", required=True) |
| 98 | parser.add_argument("--runtime-sha256", required=True) |
| 99 | parser.add_argument("--output", type=Path, required=True) |
| 100 | args = parser.parse_args() |
| 101 | native_platform, native_arch = native_identity() |
| 102 | if (args.image_platform, args.image_arch) != (native_platform, native_arch): |
| 103 | raise ValueError("actual image/compiler identity differs from native runner; emulation/cross-target proof does not transfer") |
| 104 | if len(args.source_sha) != 40 or any(char not in "0123456789abcdef" for char in args.source_sha): |
| 105 | raise ValueError("source SHA must identify the exact 40-hex CI checkout") |
| 106 | canonical = "codewhale-extension-host" + (".exe" if native_platform == "win32" else "") |
| 107 | if args.compiled_image.name != canonical: |
| 108 | raise ValueError("compiled Native receipt requires the canonical image basename") |
| 109 | for path, expected in [(args.compiled_image, args.host_sha256), (args.bundle, args.bundle_sha256), (args.bun, args.runtime_sha256)]: |
| 110 | if not path.is_file() or path.is_symlink() or digest(path) != expected: |
| 111 | raise ValueError(f"qualified input changed: {path.name}") |
| 112 | if args.junit.stat().st_size > MAX_REPORT: |
| 113 | raise ValueError("oversized nextest report") |
| 114 | passed, log = extract(args.junit.read_bytes(), native_platform, native_arch) |
| 115 | libc = None |
| 116 | if native_platform == "linux": |
| 117 | value = os.confstr("CS_GNU_LIBC_VERSION") |
| 118 | if not value or not value.startswith("glibc "): |
| 119 | raise ValueError("Linux native receipt requires a measured glibc runner; musl is unqualified") |
| 120 | libc = {"family": "glibc", "version": value.split(" ", 1)[1], "scope": "native-runner-observed"} |
| 121 | # A preexisting output must not be confused with this attempt's receipt. |
| 122 | args.output.mkdir(parents=True, exist_ok=False) |
| 123 | copied = args.output / canonical |
| 124 | shutil.copy2(args.compiled_image, copied) |
| 125 | if digest(copied) != args.host_sha256: |
| 126 | raise ValueError("copied compiled image differs from tested bytes") |
| 127 | (args.output / "native-containment.log").write_bytes(log) |
| 128 | receipt = { |
| 129 | "scope": "native-compiled-host", "source_sha": args.source_sha, |
| 130 | "bundle_sha256": args.bundle_sha256, "runtime_sha256": args.runtime_sha256, |
| 131 | "host_sha256": args.host_sha256, "platform": native_platform, "arch": native_arch, |
| 132 | "passed": passed, "failed": 0, "skipped": 0, |
| 133 | "log": "native-containment.log", "log_sha256": hashlib.sha256(log).hexdigest(), |
| 134 | "junit_sha256": digest(args.junit), "libc": libc, |
| 135 | } |
| 136 | (args.output / "native-receipt.json").write_text(json.dumps(receipt, indent=2) + "\n", encoding="utf-8") |
| 137 | print(json.dumps(receipt)) |
| 138 | |
| 139 | |
| 140 | if __name__ == "__main__": |
| 141 | try: |
| 142 | main() |
| 143 | except (ValueError, OSError, ET.ParseError) as error: |
| 144 | print(f"Native compiled-host receipt refused: {error}", file=sys.stderr) |
| 145 | sys.exit(1) |
| 146 |