846 lines
32 KiB
Bash
Executable File
846 lines
32 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
umask 022
|
|
|
|
gate_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
|
root=$(CDPATH= cd -- "$gate_dir/../../../.." && pwd -P)
|
|
input=$gate_dir/P15-027-input.json
|
|
freebsd_src=${FREEBSD_SRC:-/work/build/freebsd-src}
|
|
base=
|
|
output=
|
|
deadline=120
|
|
|
|
while test "$#" -gt 0; do
|
|
case "$1" in
|
|
--base)
|
|
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 20; }
|
|
base=$2
|
|
shift 2
|
|
;;
|
|
--freebsd-src)
|
|
test "$#" -ge 2 || { printf '%s\n' '--freebsd-src requires a directory' >&2; exit 20; }
|
|
freebsd_src=$2
|
|
shift 2
|
|
;;
|
|
--output)
|
|
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 20; }
|
|
output=$2
|
|
shift 2
|
|
;;
|
|
*)
|
|
printf 'unknown argument: %s\n' "$1" >&2
|
|
exit 20
|
|
;;
|
|
esac
|
|
done
|
|
|
|
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 20; }
|
|
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 20; }
|
|
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 20; }
|
|
for tool in date git mktemp patch python3 sha256sum timeout; do
|
|
command -v "$tool" >/dev/null 2>&1 || {
|
|
printf 'missing required host tool: %s\n' "$tool" >&2
|
|
exit 21
|
|
}
|
|
done
|
|
case "$output" in
|
|
/*) ;;
|
|
*) output=$PWD/$output ;;
|
|
esac
|
|
case "$freebsd_src" in
|
|
/*) ;;
|
|
*) freebsd_src=$PWD/$freebsd_src ;;
|
|
esac
|
|
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 20; }
|
|
mkdir -p "$output"
|
|
work=$(mktemp -d "${TMPDIR:-/tmp}/P15-027.XXXXXX")
|
|
|
|
cleanup_trap()
|
|
{
|
|
rm -rf -- "$work"
|
|
}
|
|
trap cleanup_trap EXIT HUP INT TERM
|
|
|
|
python3 -B - "$output/command-argv.json" "$0" --base "$base" \
|
|
--freebsd-src "$freebsd_src" --output "$output" <<'PY'
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
Path(sys.argv[1]).write_text(
|
|
json.dumps(sys.argv[2:], ensure_ascii=True, separators=(",", ":")) + "\n",
|
|
encoding="ascii",
|
|
)
|
|
PY
|
|
python3 -B - "$output/ownership.json" "$work" "$output" <<'PY'
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
Path(sys.argv[1]).write_text(
|
|
json.dumps(
|
|
{
|
|
"owned_temporary_paths": [sys.argv[2]],
|
|
"persistent_output": sys.argv[3],
|
|
"owned_processes": [],
|
|
"owned_ports": [],
|
|
"qemu": "NOT_RUN",
|
|
"protected_resources_addressed": False,
|
|
},
|
|
ensure_ascii=True,
|
|
indent=2,
|
|
sort_keys=True,
|
|
) + "\n",
|
|
encoding="ascii",
|
|
)
|
|
PY
|
|
|
|
set +e
|
|
timeout -k 5 "$deadline" python3 -B - \
|
|
"$root" "$input" "$base" "$freebsd_src" "$output" "$work" "$deadline" <<'PY' \
|
|
>"$output/stdout.log" 2>"$output/stderr.log"
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(sys.argv[1])
|
|
INPUT = Path(sys.argv[2])
|
|
REQUESTED_BASE = sys.argv[3]
|
|
FREEBSD_SRC = Path(sys.argv[4])
|
|
OUTPUT = Path(sys.argv[5])
|
|
WORK = Path(sys.argv[6])
|
|
DEADLINE = int(sys.argv[7])
|
|
SPEC = json.loads(INPUT.read_text(encoding="utf-8"))
|
|
|
|
|
|
class GateFailure(RuntimeError):
|
|
def __init__(self, status: str, reason: str):
|
|
super().__init__(reason)
|
|
self.status = status
|
|
self.reason = reason
|
|
|
|
|
|
def sha256_bytes(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def sha256_path(path: Path) -> str:
|
|
return sha256_bytes(path.read_bytes())
|
|
|
|
|
|
def write_json(path: Path, value: Any) -> None:
|
|
path.write_text(
|
|
json.dumps(value, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
|
|
encoding="ascii",
|
|
)
|
|
|
|
|
|
def run(
|
|
argv: list[str],
|
|
*,
|
|
cwd: Path | None = None,
|
|
input_bytes: bytes | None = None,
|
|
allowed: set[int] | None = None,
|
|
timeout_seconds: int = 30,
|
|
) -> subprocess.CompletedProcess[bytes]:
|
|
try:
|
|
completed = subprocess.run(
|
|
argv,
|
|
cwd=cwd,
|
|
check=False,
|
|
input=input_bytes,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
timeout=timeout_seconds,
|
|
)
|
|
except subprocess.TimeoutExpired as error:
|
|
raise GateFailure(
|
|
"INFRA_BLOCKED", f"command timed out: {' '.join(argv)}"
|
|
) from error
|
|
accepted = {0} if allowed is None else allowed
|
|
if completed.returncode not in accepted:
|
|
detail = completed.stderr.decode("utf-8", errors="replace").strip()
|
|
raise GateFailure(
|
|
"RUNNER_FAIL",
|
|
f"command failed ({completed.returncode}): {' '.join(argv)}: {detail}",
|
|
)
|
|
return completed
|
|
|
|
|
|
def git_text(repository: Path, *args: str) -> str:
|
|
return run(["git", "-C", str(repository), *args]).stdout.decode(
|
|
"utf-8", errors="strict"
|
|
).strip()
|
|
|
|
|
|
def source_at(commit: str, relative: str) -> bytes:
|
|
return run(
|
|
["git", "-C", str(ROOT), "show", f"{commit}:{relative}"]
|
|
).stdout
|
|
|
|
|
|
def safe_relative_path(value: str) -> Path:
|
|
path = Path(value)
|
|
if path.is_absolute() or ".." in path.parts:
|
|
raise GateFailure("RUNNER_FAIL", f"unsafe declared path: {value}")
|
|
if path.name == "introduction.md":
|
|
raise GateFailure("RUNNER_FAIL", "prohibited path declaration")
|
|
return path
|
|
|
|
|
|
def validate_hash(value: Any, label: str) -> str:
|
|
if not isinstance(value, str) or re.fullmatch(r"[0-9a-f]{64}", value) is None:
|
|
raise GateFailure("RUNNER_FAIL", f"invalid SHA-256 for {label}")
|
|
return value
|
|
|
|
|
|
def validate_runtime_item(item: Any, item_id: str) -> None:
|
|
if not isinstance(item, dict):
|
|
raise GateFailure("RUNNER_FAIL", f"runtime item is not an object: {item_id}")
|
|
expected = {
|
|
"status": "PASS",
|
|
"exit_code": 0,
|
|
"target_marker": "reached",
|
|
"cleanup": "PASS",
|
|
}
|
|
for key, value in expected.items():
|
|
if item.get(key) != value:
|
|
raise GateFailure("RUNNER_FAIL", f"runtime item {item_id} has invalid {key}")
|
|
argv = item.get("command_argv")
|
|
if not isinstance(argv, list) or not argv or not all(isinstance(v, str) for v in argv):
|
|
raise GateFailure("RUNNER_FAIL", f"runtime item {item_id} lacks exact argv")
|
|
if not isinstance(item.get("deadline_seconds"), int) or item["deadline_seconds"] <= 0:
|
|
raise GateFailure("RUNNER_FAIL", f"runtime item {item_id} lacks deadline")
|
|
for stream in ("stdout", "stderr"):
|
|
relative = safe_relative_path(item.get(stream, ""))
|
|
evidence_path = OUTPUT / relative
|
|
if not evidence_path.is_file():
|
|
raise GateFailure("RUNNER_FAIL", f"runtime item {item_id} lacks {stream}")
|
|
expected_hash = validate_hash(item.get(f"{stream}_sha256"), f"{item_id} {stream}")
|
|
if sha256_path(evidence_path) != expected_hash:
|
|
raise GateFailure("RUNNER_FAIL", f"runtime item {item_id} {stream} hash mismatch")
|
|
|
|
|
|
def validate_runtime_record(
|
|
path: Path,
|
|
architecture: dict[str, Any],
|
|
modules: dict[str, dict[str, Any]],
|
|
resolved_base: str,
|
|
patch_sha256: str,
|
|
) -> dict[str, Any]:
|
|
record = json.loads(path.read_text(encoding="utf-8"))
|
|
expected_identity = {
|
|
"schema": 1,
|
|
"architecture": architecture["id"],
|
|
"uname_s": "FreeBSD",
|
|
"uname_m": architecture["expected_uname_m"],
|
|
"uname_p": architecture["expected_uname_p"],
|
|
"dut_base": resolved_base,
|
|
"temporary_patch_sha256": patch_sha256,
|
|
"freebsd_source_head": SPEC["freebsd_source"]["head"],
|
|
}
|
|
for key, value in expected_identity.items():
|
|
if record.get(key) != value:
|
|
raise GateFailure("RUNNER_FAIL", f"native record identity mismatch: {key}")
|
|
if record.get("status") not in {"PASS", "DUT_FAIL"}:
|
|
raise GateFailure("RUNNER_FAIL", "native record status is not authoritative")
|
|
if record.get("cleanup") != "PASS" or record.get("target_marker") != "reached":
|
|
raise GateFailure("RUNNER_FAIL", "native record did not reach target with cleanup")
|
|
if record.get("module_sha256") != {
|
|
name: value["sha256"] for name, value in modules.items()
|
|
}:
|
|
raise GateFailure("RUNNER_FAIL", "native module hashes differ from cross artifacts")
|
|
fixtures = record.get("fixture_sha256")
|
|
if not isinstance(fixtures, list) or len(fixtures) < 4:
|
|
raise GateFailure("RUNNER_FAIL", "native record lacks four fixture hashes")
|
|
for index, value in enumerate(fixtures):
|
|
validate_hash(value, f"native fixture {index}")
|
|
capability = record.get("kernel_zstdio")
|
|
validate_runtime_item(capability, "kernel-zstdio-capability")
|
|
if capability.get("options_zstdio") is not True:
|
|
raise GateFailure("RUNNER_FAIL", "native ZSTDIO capability is not enabled")
|
|
operations = record.get("operations")
|
|
if not isinstance(operations, dict):
|
|
raise GateFailure("RUNNER_FAIL", "native operations are absent")
|
|
required = SPEC["native_runtime_contract"]["required_operations"]
|
|
if set(operations) != set(required):
|
|
raise GateFailure("RUNNER_FAIL", "native operation set is not exact")
|
|
for item_id in required:
|
|
validate_runtime_item(operations[item_id], item_id)
|
|
disabled = operations["zstdio0-zstd-read"]
|
|
if disabled.get("errno") != "EOPNOTSUPP":
|
|
raise GateFailure("RUNNER_FAIL", "disabled native Zstd errno is not EOPNOTSUPP")
|
|
return record
|
|
|
|
|
|
def extract_repo_source(resolved_base: str, destination: Path) -> None:
|
|
archive = run(
|
|
[
|
|
"git", "-C", str(ROOT), "archive", "--format=tar", resolved_base,
|
|
"repo-pre-15/src", "repo-pre-15/tests/pre15/probes/ondisk_layout.c",
|
|
],
|
|
timeout_seconds=60,
|
|
).stdout
|
|
destination.mkdir(parents=True)
|
|
archive_path = WORK / "repo-source.tar"
|
|
archive_path.write_bytes(archive)
|
|
with tarfile.open(archive_path, mode="r:") as tar:
|
|
tar.extractall(destination, filter="data")
|
|
|
|
|
|
def compile_architecture(
|
|
architecture: dict[str, Any],
|
|
resolved_base: str,
|
|
patch_bytes: bytes,
|
|
compiler: Path,
|
|
) -> dict[str, Any]:
|
|
arch_id = architecture["id"]
|
|
arch_root = WORK / "cross" / arch_id
|
|
source_root = arch_root / "source"
|
|
extract_repo_source(resolved_base, source_root)
|
|
run(["patch", "-p1"], cwd=source_root, input_bytes=patch_bytes)
|
|
src = source_root / "repo-pre-15/src"
|
|
sys_root = FREEBSD_SRC / "sys"
|
|
resource_include = Path(
|
|
run([str(compiler), "-print-resource-dir"]).stdout.decode("ascii").strip()
|
|
) / "include"
|
|
common_flags = [
|
|
"-O2", "-pipe", "-fno-common", "-fno-strict-aliasing", "-D_KERNEL",
|
|
"-DKLD_MODULE", "-nostdinc", "-ffreestanding", "-fwrapv",
|
|
"-fno-asynchronous-unwind-tables", "-fno-omit-frame-pointer",
|
|
"-fstack-protector", "-Wall", "-Wstrict-prototypes",
|
|
"-Wmissing-prototypes", "-Wpointer-arith", "-Wcast-qual", "-Wundef",
|
|
"-Wno-pointer-sign", "-Wmissing-include-dirs", "-Wno-unknown-pragmas",
|
|
"-Wno-address-of-packed-member", "-Wno-format-zero-length", "-std=gnu17",
|
|
"-D__printf__=__freebsd_kprintf__", f"--target={architecture['target_triple']}",
|
|
*architecture["kernel_cflags"],
|
|
]
|
|
modules: dict[str, dict[str, Any]] = {}
|
|
for zstdio in (0, 1):
|
|
build = arch_root / f"zstdio{zstdio}"
|
|
build.mkdir(parents=True)
|
|
(build / "opt_global.h").write_bytes(b"")
|
|
(build / "machine").symlink_to(sys_root / architecture["machine_header_dir"] / "include")
|
|
for alias, relative in architecture["header_aliases"].items():
|
|
(build / alias).symlink_to(sys_root / relative)
|
|
for mode in ("-p", "-q", "-h"):
|
|
generated = run(
|
|
["awk", "-f", str(sys_root / "tools/vnode_if.awk"),
|
|
str(sys_root / "kern/vnode_if.src"), mode],
|
|
cwd=build,
|
|
).stdout
|
|
suffix = {"-p": "vnode_if_newproto.h", "-q": "vnode_if_typedef.h", "-h": "vnode_if.h"}[mode]
|
|
(build / suffix).write_bytes(generated)
|
|
include_flags = [
|
|
"-include", str(build / "opt_global.h"), "-I", str(build),
|
|
"-I", str(sys_root), "-I", str(sys_root / "contrib/ck/include"),
|
|
]
|
|
objects = []
|
|
stdout_parts = []
|
|
stderr_parts = []
|
|
for source_name in SPEC["cross_build_contract"]["module_sources"]:
|
|
obj = build / f"{Path(source_name).stem}.o"
|
|
argv = [str(compiler), *common_flags, *include_flags]
|
|
if source_name == "decompressor_zstd.c":
|
|
argv.extend(["-I", str(sys_root / "contrib/zstd/lib/freebsd")])
|
|
if zstdio == 1:
|
|
argv.append("-DZSTDIO")
|
|
argv.extend(["-c", str(src / source_name), "-o", str(obj)])
|
|
completed = run(argv, cwd=build, timeout_seconds=60)
|
|
stdout_parts.append(completed.stdout)
|
|
stderr_parts.append(completed.stderr)
|
|
objects.append(obj)
|
|
module = build / "erofs.ko"
|
|
linked = run(
|
|
[str(compiler), f"--target={architecture['target_triple']}",
|
|
"-r", "-nostdlib", *map(str, objects), "-o", str(module)],
|
|
cwd=build,
|
|
timeout_seconds=60,
|
|
)
|
|
stdout_parts.append(linked.stdout)
|
|
stderr_parts.append(linked.stderr)
|
|
nm_output = run(["nm", "-u", str(module)]).stdout
|
|
(build / "build.stdout").write_bytes(b"".join(stdout_parts))
|
|
(build / "build.stderr").write_bytes(b"".join(stderr_parts))
|
|
(build / "nm-u.txt").write_bytes(nm_output)
|
|
modules[f"zstdio{zstdio}"] = {
|
|
"path": str(module),
|
|
"sha256": sha256_path(module),
|
|
"size_bytes": module.stat().st_size,
|
|
"nm_u_sha256": sha256_bytes(nm_output),
|
|
}
|
|
|
|
probe_root = arch_root / "probes"
|
|
probe_root.mkdir()
|
|
include_root = probe_root / "include"
|
|
include_root.mkdir()
|
|
(include_root / "machine").symlink_to(
|
|
sys_root / architecture["machine_header_dir"] / "include"
|
|
)
|
|
for alias, relative in architecture["header_aliases"].items():
|
|
(include_root / alias).symlink_to(sys_root / relative)
|
|
probe_flags = [
|
|
f"--target={architecture['target_triple']}", "-std=gnu17", "-nostdinc",
|
|
"-isystem", str(resource_include), "-isystem", str(FREEBSD_SRC / "include"),
|
|
"-isystem", str(include_root), "-isystem", str(sys_root),
|
|
]
|
|
layout = source_root / "repo-pre-15/tests/pre15/probes/ondisk_layout.c"
|
|
layout_result = run(
|
|
[str(compiler), *probe_flags, "-Wall", "-Wextra", "-Werror",
|
|
"-fsyntax-only", "-Xclang", "-fdump-record-layouts", str(layout)],
|
|
timeout_seconds=60,
|
|
)
|
|
(probe_root / "layout.stdout").write_bytes(layout_result.stdout)
|
|
(probe_root / "layout.stderr").write_bytes(layout_result.stderr)
|
|
unaligned = probe_root / "unaligned-endian.c"
|
|
unaligned.write_text(
|
|
SPEC["cross_build_contract"]["unaligned_endian_probe_source"],
|
|
encoding="ascii",
|
|
)
|
|
assembly = probe_root / "unaligned-endian.s"
|
|
endian_result = run(
|
|
[str(compiler), *probe_flags, *architecture["kernel_cflags"],
|
|
"-O2", "-S", str(unaligned), "-o", str(assembly)],
|
|
timeout_seconds=60,
|
|
)
|
|
(probe_root / "unaligned-endian.stdout").write_bytes(endian_result.stdout)
|
|
(probe_root / "unaligned-endian.stderr").write_bytes(endian_result.stderr)
|
|
if assembly.stat().st_size == 0:
|
|
raise GateFailure("RUNNER_FAIL", f"empty unaligned/endian probe for {arch_id}")
|
|
return {
|
|
"status": "PASS",
|
|
"temporary_source": str(source_root),
|
|
"modules": modules,
|
|
"layout_probe_sha256": sha256_path(layout),
|
|
"layout_output_sha256": sha256_bytes(layout_result.stdout),
|
|
"unaligned_endian_probe_sha256": sha256_path(unaligned),
|
|
"unaligned_endian_assembly_sha256": sha256_path(assembly),
|
|
}
|
|
|
|
|
|
def render_argv(values: list[str], substitutions: dict[str, str]) -> list[str]:
|
|
rendered = []
|
|
for value in values:
|
|
for key, replacement in substitutions.items():
|
|
value = value.replace("{" + key + "}", replacement)
|
|
rendered.append(value)
|
|
return rendered
|
|
|
|
|
|
def run_native(
|
|
architecture: dict[str, Any],
|
|
declaration: dict[str, Any],
|
|
modules: dict[str, dict[str, Any]],
|
|
resolved_base: str,
|
|
patch_sha256: str,
|
|
) -> dict[str, Any]:
|
|
runner_relative = safe_relative_path(declaration.get("runner_path", ""))
|
|
runner = ROOT / runner_relative
|
|
if not runner.is_file():
|
|
raise GateFailure("INFRA_BLOCKED", f"declared native runner is absent: {runner_relative}")
|
|
if sha256_path(runner) != validate_hash(
|
|
declaration.get("runner_sha256"), f"{architecture['id']} runner"
|
|
):
|
|
raise GateFailure("INFRA_BLOCKED", "declared native runner identity changed")
|
|
native_output = OUTPUT / "native" / architecture["id"]
|
|
native_output.mkdir(parents=True)
|
|
substitutions = {
|
|
"runner": str(runner),
|
|
"architecture": architecture["id"],
|
|
"module_zstdio0": modules["zstdio0"]["path"],
|
|
"module_zstdio1": modules["zstdio1"]["path"],
|
|
"output": str(native_output),
|
|
}
|
|
preflight_argv = render_argv(declaration["preflight_argv"], substitutions)
|
|
preflight = run(
|
|
preflight_argv,
|
|
cwd=ROOT,
|
|
timeout_seconds=declaration["preflight_deadline_seconds"],
|
|
)
|
|
(native_output / "preflight.runner.stdout").write_bytes(preflight.stdout)
|
|
(native_output / "preflight.runner.stderr").write_bytes(preflight.stderr)
|
|
preflight_record = json.loads(
|
|
(native_output / "preflight.json").read_text(encoding="utf-8")
|
|
)
|
|
for key, value in {
|
|
"status": "PASS", "exit_code": 0, "target_marker": "reached",
|
|
"cleanup": "PASS", "uname_s": "FreeBSD",
|
|
"uname_m": architecture["expected_uname_m"],
|
|
"uname_p": architecture["expected_uname_p"],
|
|
}.items():
|
|
if preflight_record.get(key) != value:
|
|
raise GateFailure("INFRA_BLOCKED", f"native preflight mismatch: {key}")
|
|
runtime_argv = render_argv(declaration["runtime_argv"], substitutions)
|
|
runtime = run(
|
|
runtime_argv,
|
|
cwd=ROOT,
|
|
allowed={0, 10},
|
|
timeout_seconds=declaration["runtime_deadline_seconds"],
|
|
)
|
|
(native_output / "runner.stdout").write_bytes(runtime.stdout)
|
|
(native_output / "runner.stderr").write_bytes(runtime.stderr)
|
|
return validate_runtime_record(
|
|
native_output / "runtime-result.json",
|
|
architecture,
|
|
modules,
|
|
resolved_base,
|
|
patch_sha256,
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-027":
|
|
raise GateFailure("RUNNER_FAIL", "invalid P15-027 input identity")
|
|
if SPEC.get("gate") != "G07" or SPEC.get("batch") != "B36":
|
|
raise GateFailure("RUNNER_FAIL", "invalid G07/B36 input identity")
|
|
if SPEC.get("prohibited_paths") != ["introduction.md"]:
|
|
raise GateFailure("RUNNER_FAIL", "prohibited-path contract changed")
|
|
resolved = git_text(ROOT, "rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
|
if resolved != SPEC["required_base"]:
|
|
raise GateFailure(
|
|
"INFRA_BLOCKED", f"P15-027 must replay {SPEC['required_base']}, got {resolved}"
|
|
)
|
|
source_tree = git_text(ROOT, "rev-parse", f"{resolved}:repo-pre-15/src")
|
|
if source_tree != SPEC["repo_source_tree_oid"]:
|
|
raise GateFailure("INFRA_BLOCKED", "frozen EROFS source tree changed")
|
|
source_hashes = {
|
|
path: sha256_bytes(source_at(resolved, path))
|
|
for path in SPEC["source_sha256"]
|
|
}
|
|
if source_hashes != SPEC["source_sha256"]:
|
|
raise GateFailure("INFRA_BLOCKED", "frozen gate source identity changed")
|
|
write_json(OUTPUT / "source-sha256.json", source_hashes)
|
|
|
|
addendum_paths = (
|
|
"repo-pre-15/tests/pre15/gates/P15-027.sh",
|
|
"repo-pre-15/tests/pre15/gates/P15-027-input.json",
|
|
"repo-pre-15/docs/pre15-stage0/P15-027.md",
|
|
)
|
|
addendum_hashes = {
|
|
path: sha256_path(ROOT / path) for path in addendum_paths
|
|
}
|
|
write_json(OUTPUT / "gate-addendum-sha256.json", addendum_hashes)
|
|
if sha256_path(ROOT / "repo-pre-15/src/Makefile") != SPEC["source_sha256"]["repo-pre-15/src/Makefile"]:
|
|
raise GateFailure("RUNNER_FAIL", "production Makefile differs from frozen BASE")
|
|
|
|
patch_spec = SPEC["temporary_makefile_relaxation"]
|
|
patch_bytes = patch_spec["unified_diff"].encode("ascii")
|
|
if sha256_bytes(patch_bytes) != patch_spec["sha256"]:
|
|
raise GateFailure("RUNNER_FAIL", "input-recorded temporary patch hash mismatch")
|
|
patch_check = WORK / "patch-check"
|
|
makefile_copy = patch_check / "repo-pre-15/src/Makefile"
|
|
makefile_copy.parent.mkdir(parents=True)
|
|
makefile_copy.write_bytes(source_at(resolved, patch_spec["path"]))
|
|
patch_result = run(["patch", "-p1"], cwd=patch_check, input_bytes=patch_bytes)
|
|
(OUTPUT / "makefile-relaxation.patch").write_bytes(patch_bytes)
|
|
(OUTPUT / "patch.stdout").write_bytes(patch_result.stdout)
|
|
(OUTPUT / "patch.stderr").write_bytes(patch_result.stderr)
|
|
if sha256_path(makefile_copy) != patch_spec["patched_makefile_sha256"]:
|
|
raise GateFailure("RUNNER_FAIL", "temporary Makefile relaxation result changed")
|
|
patch_record = {
|
|
"path": patch_spec["path"],
|
|
"sha256": patch_spec["sha256"],
|
|
"patched_makefile_sha256": sha256_path(makefile_copy),
|
|
"production_makefile_changed": False,
|
|
"temporary_apply": "PASS",
|
|
}
|
|
write_json(OUTPUT / "temporary-patch.json", patch_record)
|
|
|
|
if not FREEBSD_SRC.is_dir():
|
|
raise GateFailure("INFRA_BLOCKED", f"FreeBSD source is absent: {FREEBSD_SRC}")
|
|
freebsd = SPEC["freebsd_source"]
|
|
freebsd_head = git_text(FREEBSD_SRC, "rev-parse", "HEAD")
|
|
freebsd_prefix = git_text(FREEBSD_SRC, "rev-parse", "--show-prefix")
|
|
if freebsd_head != freebsd["head"] or freebsd_prefix != freebsd["git_prefix"]:
|
|
raise GateFailure("INFRA_BLOCKED", "FreeBSD source identity changed")
|
|
git_root = Path(git_text(FREEBSD_SRC, "rev-parse", "--show-toplevel"))
|
|
freebsd_source_tree = git_text(
|
|
git_root, "rev-parse", f"{freebsd_head}:{freebsd['git_tree_path']}"
|
|
)
|
|
freebsd_sys_tree = git_text(
|
|
git_root, "rev-parse", f"{freebsd_head}:{freebsd['git_tree_path']}/sys"
|
|
)
|
|
if freebsd_source_tree != freebsd["source_tree_oid"] or freebsd_sys_tree != freebsd["sys_tree_oid"]:
|
|
raise GateFailure("INFRA_BLOCKED", "FreeBSD source tree object changed")
|
|
freebsd_hashes = {
|
|
path: sha256_path(FREEBSD_SRC / path) for path in freebsd["sha256"]
|
|
}
|
|
if freebsd_hashes != freebsd["sha256"]:
|
|
raise GateFailure("INFRA_BLOCKED", "selected FreeBSD source hashes changed")
|
|
|
|
compiler_path = Path(shutil.which("clang") or "").resolve()
|
|
if not compiler_path.is_file():
|
|
raise GateFailure("INFRA_BLOCKED", "clang is unavailable")
|
|
compiler = SPEC["toolchain"]["compiler"]
|
|
compiler_version = run([str(compiler_path), "--version"]).stdout.decode(
|
|
"utf-8", errors="strict"
|
|
).splitlines()[0]
|
|
if (
|
|
str(compiler_path) != compiler["realpath"]
|
|
or sha256_path(compiler_path) != compiler["sha256"]
|
|
or compiler_version != compiler["version"]
|
|
):
|
|
raise GateFailure("INFRA_BLOCKED", "cross compiler identity changed")
|
|
compiler_targets = run([str(compiler_path), "--print-targets"]).stdout.decode(
|
|
"utf-8", errors="strict"
|
|
)
|
|
|
|
architectures = SPEC["architectures"]
|
|
if [item.get("id") for item in architectures] != ["arm64", "riscv64", "i386"]:
|
|
raise GateFailure("RUNNER_FAIL", "architecture audit set is not exact")
|
|
if sum(item.get("pointer_bits") == 32 for item in architectures) < 1:
|
|
raise GateFailure("RUNNER_FAIL", "architecture audit lacks a 32-bit target")
|
|
source_support = {}
|
|
for architecture in architectures:
|
|
arch_id = architecture["id"]
|
|
header = FREEBSD_SRC / "sys" / architecture["machine_header_dir"] / "include/param.h"
|
|
if not header.is_file():
|
|
raise GateFailure("INFRA_BLOCKED", f"FreeBSD source lacks {arch_id}")
|
|
if sha256_path(header) != architecture["param_h_sha256"]:
|
|
raise GateFailure("INFRA_BLOCKED", f"FreeBSD {arch_id} source identity changed")
|
|
target_pattern = rf"^\s*{re.escape(architecture['compiler_backend'])}\s+-"
|
|
if re.search(target_pattern, compiler_targets, re.MULTILINE) is None:
|
|
raise GateFailure("INFRA_BLOCKED", f"clang lacks {arch_id} backend")
|
|
source_support[arch_id] = {
|
|
"status": "PASS",
|
|
"freebsd_machine_headers": str(header),
|
|
"param_h_sha256": sha256_path(header),
|
|
"compiler_backend": architecture["compiler_backend"],
|
|
"target_triple": architecture["target_triple"],
|
|
"pointer_bits": architecture["pointer_bits"],
|
|
}
|
|
|
|
probe_source = SPEC["cross_build_contract"]["unaligned_endian_probe_source"].encode("ascii")
|
|
if sha256_bytes(probe_source) != SPEC["cross_build_contract"]["unaligned_endian_probe_sha256"]:
|
|
raise GateFailure("RUNNER_FAIL", "input-recorded unaligned/endian probe changed")
|
|
inventory = {
|
|
"schema": 1,
|
|
"host": {"system": platform.system(), "machine": platform.machine()},
|
|
"local_host_is_qualifying_native_freebsd": False,
|
|
"local_host_reason": "host is not FreeBSD and is not any candidate architecture",
|
|
"declared_native_environments": {
|
|
item["id"]: item["native_environment"] is not None for item in architectures
|
|
},
|
|
"emulator_tools": {
|
|
item["id"]: shutil.which(item["emulator_tool"]) for item in architectures
|
|
},
|
|
"emulator_binary_is_native_runtime_evidence": False,
|
|
"source_toolchain_support": source_support,
|
|
"protected_resources_addressed": False,
|
|
"inventory_rule": "only an explicit, validated same-architecture FreeBSD runner declaration can start cross work",
|
|
}
|
|
write_json(OUTPUT / "runtime-environment-inventory.json", inventory)
|
|
write_json(
|
|
OUTPUT / "freebsd-source-identity.json",
|
|
{
|
|
"path": str(FREEBSD_SRC),
|
|
"git_root": str(git_root),
|
|
"git_prefix": freebsd_prefix,
|
|
"git_tree_path": freebsd["git_tree_path"],
|
|
"head": freebsd_head,
|
|
"source_tree_oid": freebsd_source_tree,
|
|
"sys_tree_oid": freebsd_sys_tree,
|
|
"sha256": freebsd_hashes,
|
|
},
|
|
)
|
|
write_json(
|
|
OUTPUT / "toolchain-identity.json",
|
|
{
|
|
"compiler_realpath": str(compiler_path),
|
|
"compiler_sha256": sha256_path(compiler_path),
|
|
"compiler_version": compiler_version,
|
|
"print_targets_sha256": sha256_bytes(compiler_targets.encode("utf-8")),
|
|
},
|
|
)
|
|
|
|
results = []
|
|
go_architectures = []
|
|
for architecture in architectures:
|
|
declaration = architecture["native_environment"]
|
|
record: dict[str, Any] = {
|
|
"architecture": architecture["id"],
|
|
"machine_arch": architecture["machine_arch"],
|
|
"pointer_bits": architecture["pointer_bits"],
|
|
"source_toolchain": source_support[architecture["id"]],
|
|
}
|
|
if declaration is None:
|
|
record.update(
|
|
{
|
|
"native_environment": {
|
|
"status": "STOP",
|
|
"reason": "no declared same-architecture FreeBSD runtime environment",
|
|
},
|
|
"cross_build_and_probes": {
|
|
"status": "NOT_RUN",
|
|
"reason": "cross-only evidence cannot authorize allowlisting",
|
|
},
|
|
"native_runtime": {
|
|
"status": "NOT_RUN",
|
|
"reason": "same-architecture FreeBSD runtime is absent",
|
|
},
|
|
"decision": "STOP",
|
|
}
|
|
)
|
|
results.append(record)
|
|
continue
|
|
for key in (
|
|
"runner_path", "runner_sha256", "preflight_argv", "runtime_argv",
|
|
"preflight_deadline_seconds", "runtime_deadline_seconds",
|
|
):
|
|
if key not in declaration:
|
|
raise GateFailure("RUNNER_FAIL", f"incomplete native declaration: {key}")
|
|
cross = compile_architecture(
|
|
architecture, resolved, patch_bytes, compiler_path
|
|
)
|
|
native = run_native(
|
|
architecture,
|
|
declaration,
|
|
cross["modules"],
|
|
resolved,
|
|
patch_spec["sha256"],
|
|
)
|
|
native_pass = native["status"] == "PASS"
|
|
record.update(
|
|
{
|
|
"native_environment": {"status": "PASS", "declaration": declaration},
|
|
"cross_build_and_probes": cross,
|
|
"native_runtime": native,
|
|
"decision": "GO" if native_pass else "STOP",
|
|
}
|
|
)
|
|
if native_pass:
|
|
go_architectures.append(architecture["id"])
|
|
results.append(record)
|
|
|
|
overall_status = "GO" if go_architectures else "STOP"
|
|
result = {
|
|
"schema": 1,
|
|
"gate": "G07",
|
|
"candidate": "P15-027",
|
|
"batch": "B36",
|
|
"status": overall_status,
|
|
"reason": (
|
|
"at least one architecture completed its own cross and native requirements"
|
|
if go_architectures
|
|
else "no architecture has a declared same-architecture FreeBSD runtime"
|
|
),
|
|
"requested_base": REQUESTED_BASE,
|
|
"resolved_base": resolved,
|
|
"repo_source_tree_oid": source_tree,
|
|
"deadline_seconds": DEADLINE,
|
|
"expected_exit_code": 0 if go_architectures else 22,
|
|
"architectures": results,
|
|
"go_architectures": go_architectures,
|
|
"allowlist_authorization": go_architectures,
|
|
"false_go_guard": "each authorized architecture must independently have cross_build_and_probes=PASS and native_runtime=PASS",
|
|
"b36": "AUTHORIZED" if go_architectures else "STOP-NO-SOURCE",
|
|
"production_makefile_guard_preserved": True,
|
|
"production_source_changed": False,
|
|
"qemu": "NOT_RUN" if not go_architectures else "runner-defined",
|
|
"full_feature_suite": "NOT_RUN",
|
|
"smoke_suite": "NOT_RUN",
|
|
"protected_resources_addressed": False,
|
|
}
|
|
write_json(OUTPUT / "result.json", result)
|
|
print(json.dumps(result, ensure_ascii=True, sort_keys=True))
|
|
return 0 if go_architectures else 22
|
|
|
|
|
|
try:
|
|
raise SystemExit(main())
|
|
except GateFailure as error:
|
|
failure = {
|
|
"schema": 1,
|
|
"gate": "G07",
|
|
"candidate": "P15-027",
|
|
"status": error.status,
|
|
"reason": error.reason,
|
|
"requested_base": REQUESTED_BASE,
|
|
"deadline_seconds": DEADLINE,
|
|
"b36": "NOT_RUN",
|
|
"target_marker": "not_reached",
|
|
"production_source_changed": False,
|
|
"protected_resources_addressed": False,
|
|
}
|
|
write_json(OUTPUT / "result.json", failure)
|
|
print(json.dumps(failure, ensure_ascii=True, sort_keys=True))
|
|
raise SystemExit({"RUNNER_FAIL": 20, "INFRA_BLOCKED": 21}.get(error.status, 20))
|
|
PY
|
|
gate_rc=$?
|
|
set -e
|
|
|
|
cleanup_status=PASS
|
|
if ! rm -rf -- "$work"; then
|
|
cleanup_status=FAIL
|
|
gate_rc=20
|
|
fi
|
|
trap - EXIT HUP INT TERM
|
|
printf 'owned temporary path removed: %s\ncleanup=%s\n' \
|
|
"$work" "$cleanup_status" >"$output/cleanup.log"
|
|
|
|
python3 -B - "$output" "$gate_rc" "$cleanup_status" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
|
|
output = Path(sys.argv[1])
|
|
exit_code = int(sys.argv[2])
|
|
cleanup = sys.argv[3]
|
|
if cleanup != "PASS":
|
|
status = "RUNNER_FAIL"
|
|
origin = "runner"
|
|
elif exit_code == 0:
|
|
status = "PASS"
|
|
origin = "gate"
|
|
elif exit_code == 22:
|
|
status = "STOP"
|
|
origin = "gate"
|
|
elif exit_code in (124, 137):
|
|
status = "INFRA_BLOCKED"
|
|
origin = "infrastructure"
|
|
else:
|
|
status = "RUNNER_FAIL"
|
|
origin = "runner"
|
|
(output / "attempt.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"schema": 1,
|
|
"exit_code": exit_code,
|
|
"status": status,
|
|
"failure_origin": origin,
|
|
"cleanup": cleanup,
|
|
"target_marker": "reached" if status in {"PASS", "STOP"} else "not_reached",
|
|
},
|
|
ensure_ascii=True,
|
|
indent=2,
|
|
sort_keys=True,
|
|
) + "\n",
|
|
encoding="ascii",
|
|
)
|
|
lines = []
|
|
for path in sorted(output.iterdir(), key=lambda item: item.name):
|
|
if path.is_file() and path.name != "SHA256SUMS":
|
|
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
lines.append(f"{digest} {path.name}")
|
|
(output / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="ascii")
|
|
PY
|
|
|
|
printf 'P15-027 gate exit=%s cleanup=%s output=%s\n' \
|
|
"$gate_rc" "$cleanup_status" "$output"
|
|
exit "$gate_rc"
|