返回 CodeWhale
compiled-host-native-receipt.py
根目录 / scripts / compiled-host-native-receipt.py
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
146 lines PYTHON