1110 lines
40 KiB
Bash
Executable File
1110 lines
40 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-038-input.json
|
|
base=
|
|
output=
|
|
|
|
while test "$#" -gt 0; do
|
|
case "$1" in
|
|
--base)
|
|
test "$#" -ge 2 || { printf '%s\n' '--base requires a commit' >&2; exit 2; }
|
|
base=$2
|
|
shift 2
|
|
;;
|
|
--output)
|
|
test "$#" -ge 2 || { printf '%s\n' '--output requires a directory' >&2; exit 2; }
|
|
output=$2
|
|
shift 2
|
|
;;
|
|
*)
|
|
printf 'unknown argument: %s\n' "$1" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
test -n "$base" || { printf '%s\n' '--base is required' >&2; exit 2; }
|
|
test -n "$output" || { printf '%s\n' '--output is required' >&2; exit 2; }
|
|
test -f "$input" || { printf 'missing input: %s\n' "$input" >&2; exit 2; }
|
|
for tool in cc git pkg-config python3 sha256sum; do
|
|
command -v "$tool" >/dev/null 2>&1 || {
|
|
printf 'missing required host tool: %s\n' "$tool" >&2
|
|
exit 2
|
|
}
|
|
done
|
|
case "$output" in
|
|
/*) ;;
|
|
*) output=$PWD/$output ;;
|
|
esac
|
|
test ! -e "$output" || { printf 'refusing existing output: %s\n' "$output" >&2; exit 2; }
|
|
mkdir -p "$output"
|
|
|
|
PYTHONDONTWRITEBYTECODE=1 python3 -B - "$root" "$input" "$base" "$output" <<'PY'
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import statistics
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(sys.argv[1])
|
|
INPUT = Path(sys.argv[2])
|
|
REQUESTED_BASE = sys.argv[3]
|
|
OUTPUT = Path(sys.argv[4])
|
|
SPEC = json.loads(INPUT.read_text(encoding="ascii"))
|
|
|
|
|
|
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:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def write_json(path: Path, value: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="ascii")
|
|
|
|
|
|
def run(
|
|
argv: list[str], *, timeout: int, log: Path | None = None,
|
|
allowed: set[int] | None = None,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
try:
|
|
completed = subprocess.run(
|
|
argv,
|
|
check=False,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
timeout=timeout,
|
|
)
|
|
except subprocess.TimeoutExpired as error:
|
|
if log is not None:
|
|
log.write_text(
|
|
"$ " + " ".join(argv) + f"\n[TIMEOUT after {timeout}s]\n",
|
|
encoding="utf-8",
|
|
)
|
|
raise GateFailure("INFRA_BLOCKED", f"timeout after {timeout}s: {' '.join(argv)}") from error
|
|
if log is not None:
|
|
log.write_text(
|
|
"$ " + " ".join(argv) + "\n" + completed.stdout +
|
|
f"\n[exit {completed.returncode}]\n",
|
|
encoding="utf-8",
|
|
)
|
|
expected = {0} if allowed is None else allowed
|
|
if completed.returncode not in expected:
|
|
raise GateFailure(
|
|
"RUNNER_FAIL",
|
|
f"command failed ({completed.returncode}): {' '.join(argv)}",
|
|
)
|
|
return completed
|
|
|
|
|
|
def git(*args: str) -> str:
|
|
return run(["git", "-C", str(ROOT), *args], timeout=30).stdout.strip()
|
|
|
|
|
|
def source_at(commit: str, relative: str) -> bytes:
|
|
completed = run(
|
|
["git", "-C", str(ROOT), "show", f"{commit}:{relative}"],
|
|
timeout=30,
|
|
)
|
|
return completed.stdout.encode("utf-8")
|
|
|
|
|
|
def verify_identity() -> tuple[str, dict[str, Any]]:
|
|
if SPEC.get("schema") != 1 or SPEC.get("candidate") != "P15-038":
|
|
raise GateFailure("INFRA_BLOCKED", "invalid P15-038 input identity")
|
|
if SPEC.get("gate") != "G05" or SPEC.get("scope") != (
|
|
"host-codec-cost-oracle-not-guest-vnode-performance"
|
|
):
|
|
raise GateFailure("INFRA_BLOCKED", "invalid G05 scope")
|
|
resolved = git("rev-parse", f"{REQUESTED_BASE}^{{commit}}")
|
|
if resolved != SPEC["required_base"]:
|
|
raise GateFailure(
|
|
"INFRA_BLOCKED",
|
|
f"P15-038 must replay {SPEC['required_base']}, got {resolved}",
|
|
)
|
|
hashes = {
|
|
relative: sha256_bytes(source_at(resolved, relative))
|
|
for relative in SPEC["source_sha256"]
|
|
}
|
|
if hashes != SPEC["source_sha256"]:
|
|
raise GateFailure("INFRA_BLOCKED", "frozen B32/source identity changed")
|
|
package_names = {
|
|
"liblz4": "liblz4",
|
|
"liblzma": "liblzma",
|
|
"zlib": "zlib",
|
|
"libzstd": "libzstd",
|
|
}
|
|
versions = {
|
|
name: run(["pkg-config", "--modversion", package], timeout=30).stdout.strip()
|
|
for name, package in package_names.items()
|
|
}
|
|
if versions != SPEC["libraries"]:
|
|
raise GateFailure(
|
|
"INFRA_BLOCKED",
|
|
f"host codec library identity changed: {versions!r}",
|
|
)
|
|
zdata = source_at(resolved, "repo-pre-15/src/zdata.c").decode("utf-8")
|
|
internal = source_at(resolved, "repo-pre-15/src/internal.h").decode("utf-8")
|
|
required_tokens = (
|
|
"cache->nid == vi->nid",
|
|
"cache->decoded_size == decoded_size",
|
|
"cache->map.m_pa == map->m_pa",
|
|
"cache->map.m_la == map->m_la",
|
|
"cache->map.m_plen == map->m_plen",
|
|
"cache->map.m_llen == map->m_llen",
|
|
"cache->map.m_deviceid == map->m_deviceid",
|
|
"cache->map.m_algorithmformat == map->m_algorithmformat",
|
|
"cache->map.m_flags == map->m_flags",
|
|
"cache->state == EROFS_ZCACHE_INFLIGHT || cache->waiters != 0",
|
|
"cache->error = error",
|
|
"cache->closing = true",
|
|
"Z_EROFS_CACHE_BYPASS",
|
|
)
|
|
missing = [token for token in required_tokens if token not in zdata]
|
|
if missing:
|
|
raise GateFailure("STOP", f"B32 key/inflight/failure contract is not closed: {missing!r}")
|
|
if zdata.count("map->m_algorithmformat == Z_EROFS_COMPRESSION_LZMA") != 1:
|
|
raise GateFailure("STOP", "frozen admission is not exactly LZMA-only")
|
|
for token in ("EROFS_ZCACHE_EMPTY", "EROFS_ZCACHE_INFLIGHT", "EROFS_ZCACHE_READY", "EROFS_ZCACHE_FAILED"):
|
|
if token not in internal:
|
|
raise GateFailure("STOP", f"B32 state is missing: {token}")
|
|
return resolved, {"libraries": versions, "source_sha256": hashes}
|
|
|
|
|
|
class Budget:
|
|
def __init__(self, limit: int):
|
|
self.limit = limit
|
|
self.used = 0
|
|
self.peak = 0
|
|
self.lock = threading.Lock()
|
|
|
|
def reserve(self, amount: int) -> bool:
|
|
with self.lock:
|
|
if amount > self.limit - self.used:
|
|
return False
|
|
self.used += amount
|
|
self.peak = max(self.peak, self.used)
|
|
return True
|
|
|
|
def release(self, amount: int) -> None:
|
|
with self.lock:
|
|
if amount > self.used:
|
|
raise GateFailure("STOP", "global budget accounting underflow")
|
|
self.used -= amount
|
|
|
|
|
|
class Cache:
|
|
def __init__(self, budget: Budget, local_limit: int, minimum_work: int, enabled: bool = True):
|
|
self.budget = budget
|
|
self.local_limit = local_limit
|
|
self.minimum_work = minimum_work
|
|
self.enabled = enabled
|
|
self.condition = threading.Condition()
|
|
self.state = "EMPTY"
|
|
self.key: tuple[int, ...] | None = None
|
|
self.data: bytes | None = None
|
|
self.error = 0
|
|
self.waiters = 0
|
|
self.charged = 0
|
|
self.peak = 0
|
|
self.closing = False
|
|
self.owners = 0
|
|
self.evictions = 0
|
|
self.reclaims = 0
|
|
self.bypasses = 0
|
|
|
|
def eligible(self, decoded_size: int, decode_work: int) -> bool:
|
|
return (
|
|
self.enabled
|
|
and decoded_size <= self.local_limit
|
|
and decode_work >= self.minimum_work
|
|
)
|
|
|
|
def _release_locked(self) -> None:
|
|
if self.charged:
|
|
self.budget.release(self.charged)
|
|
self.charged = 0
|
|
self.data = None
|
|
|
|
def claim(self, key: tuple[int, ...], decoded_size: int, decode_work: int) -> tuple[str, bytes | int | None]:
|
|
if not self.eligible(decoded_size, decode_work):
|
|
self.bypasses += 1
|
|
return "BYPASS", None
|
|
with self.condition:
|
|
if self.closing:
|
|
self.bypasses += 1
|
|
return "BYPASS", None
|
|
if self.state != "EMPTY" and self.key == key:
|
|
if self.state == "READY":
|
|
if self.data is None:
|
|
raise GateFailure("STOP", "READY cache has no data")
|
|
return "HIT", self.data
|
|
if self.state == "INFLIGHT":
|
|
self.waiters += 1
|
|
self.condition.notify_all()
|
|
while self.state == "INFLIGHT":
|
|
self.condition.wait()
|
|
if self.key != key:
|
|
raise GateFailure("STOP", "waiter generation key changed")
|
|
if self.state == "READY":
|
|
result: tuple[str, bytes | int | None] = ("HIT", self.data)
|
|
elif self.state == "FAILED" and self.error > 0:
|
|
result = ("ERROR", self.error)
|
|
else:
|
|
raise GateFailure("STOP", "waiter received untyped publication")
|
|
self.waiters -= 1
|
|
if self.state == "FAILED" and self.waiters == 0:
|
|
self.error = 0
|
|
self.state = "EMPTY"
|
|
if self.waiters == 0:
|
|
self.condition.notify_all()
|
|
return result
|
|
if self.state == "FAILED":
|
|
if self.waiters:
|
|
self.bypasses += 1
|
|
return "BYPASS", None
|
|
self.error = 0
|
|
self.state = "EMPTY"
|
|
if self.state == "INFLIGHT" or self.waiters:
|
|
self.bypasses += 1
|
|
return "BYPASS", None
|
|
if self.state == "READY":
|
|
self._release_locked()
|
|
self.evictions += 1
|
|
self.state = "EMPTY"
|
|
if not self.budget.reserve(decoded_size):
|
|
self.bypasses += 1
|
|
return "BYPASS", None
|
|
self.charged = decoded_size
|
|
self.peak = max(self.peak, self.charged)
|
|
self.key = key
|
|
self.error = 0
|
|
self.state = "INFLIGHT"
|
|
self.owners += 1
|
|
return "OWNER", None
|
|
|
|
def wait_for_waiters(self, expected: int) -> None:
|
|
with self.condition:
|
|
if not self.condition.wait_for(lambda: self.waiters == expected, timeout=10):
|
|
raise GateFailure("STOP", "same-key waiters did not register")
|
|
|
|
def complete(self, key: tuple[int, ...], data: bytes | None, error: int) -> None:
|
|
if (error == 0) != (data is not None):
|
|
raise GateFailure("STOP", "owner completion is untyped")
|
|
with self.condition:
|
|
if self.state != "INFLIGHT" or self.key != key:
|
|
raise GateFailure("STOP", "owner lost inflight key")
|
|
if error == 0:
|
|
assert data is not None
|
|
if len(data) != self.charged:
|
|
raise GateFailure("STOP", "published bytes do not match reservation")
|
|
self.data = data
|
|
self.state = "READY"
|
|
else:
|
|
if error < 1:
|
|
raise GateFailure("STOP", "published errno is not positive")
|
|
self._release_locked()
|
|
self.error = error
|
|
self.state = "FAILED"
|
|
if self.waiters == 0:
|
|
self.error = 0
|
|
self.state = "EMPTY"
|
|
self.condition.notify_all()
|
|
|
|
def reclaim(self) -> str:
|
|
with self.condition:
|
|
if self.state == "INFLIGHT" or self.waiters:
|
|
return "BUSY"
|
|
if self.state == "READY":
|
|
self._release_locked()
|
|
self.state = "EMPTY"
|
|
self.reclaims += 1
|
|
return "RECLAIMED"
|
|
return "EMPTY"
|
|
|
|
def fini(self, closing: threading.Event | None = None) -> None:
|
|
with self.condition:
|
|
self.closing = True
|
|
if closing is not None:
|
|
closing.set()
|
|
while self.state == "INFLIGHT" or self.waiters:
|
|
self.condition.wait()
|
|
self._release_locked()
|
|
self.error = 0
|
|
self.state = "EMPTY"
|
|
|
|
|
|
def payload(key: tuple[int, ...], size: int) -> bytes:
|
|
seed = sum((index + 1) * value for index, value in enumerate(key)) & 0xFF
|
|
return bytes(((seed + index * 29) & 0xFF) for index in range(size))
|
|
|
|
|
|
def run_wave(cache: Cache, key: tuple[int, ...], size: int, error: int) -> dict[str, Any]:
|
|
workers = 16
|
|
barrier = threading.Barrier(workers)
|
|
results: list[tuple[str, bytes | int | None] | None] = [None] * workers
|
|
expected = payload(key, size)
|
|
|
|
def worker(index: int) -> None:
|
|
barrier.wait(timeout=10)
|
|
result = cache.claim(key, size, size * 2)
|
|
if result[0] == "OWNER":
|
|
cache.wait_for_waiters(workers - 1)
|
|
cache.complete(key, None if error else expected, error)
|
|
results[index] = ("ERROR", error) if error else ("HIT", expected)
|
|
else:
|
|
results[index] = result
|
|
|
|
threads = [threading.Thread(target=worker, args=(index,)) for index in range(workers)]
|
|
for thread in threads:
|
|
thread.start()
|
|
for thread in threads:
|
|
thread.join(timeout=15)
|
|
if any(thread.is_alive() for thread in threads):
|
|
raise GateFailure("STOP", "state-model worker did not terminate")
|
|
if error:
|
|
if any(result != ("ERROR", error) for result in results):
|
|
raise GateFailure("STOP", "same-key waiters did not receive one typed failure")
|
|
elif any(result != ("HIT", expected) for result in results):
|
|
raise GateFailure("STOP", "same-key waiters did not receive identical bytes")
|
|
return {"error": error, "owners": 1, "waiters": workers - 1}
|
|
|
|
|
|
def run_state_model() -> dict[str, Any]:
|
|
config = SPEC["budget"]
|
|
extent = SPEC["benchmark"]["extent_bytes"]
|
|
global_budget = Budget(config["global_bytes"])
|
|
cache = Cache(
|
|
global_budget,
|
|
config["per_mount_bytes"],
|
|
config["minimum_decode_work_bytes"],
|
|
)
|
|
key = (42, extent, 4096, 0, 8192, extent, 0, 1, 1)
|
|
success = run_wave(cache, key, extent, 0)
|
|
failure_key = (43, extent, 8192, 0, 8192, extent, 0, 2, 1)
|
|
failure = run_wave(cache, failure_key, extent, 97)
|
|
retry = cache.claim(failure_key, extent, extent * 2)
|
|
if retry[0] != "OWNER":
|
|
raise GateFailure("STOP", "typed failure is not retryable")
|
|
retry_payload = payload(failure_key, extent)
|
|
cache.complete(failure_key, retry_payload, 0)
|
|
|
|
before = sha256_bytes(retry_payload)
|
|
changed = list(failure_key)
|
|
changed[2] += 4096
|
|
changed_key = tuple(changed)
|
|
result = cache.claim(changed_key, extent, extent * 2)
|
|
if result[0] != "OWNER":
|
|
raise GateFailure("STOP", "ready key eviction did not create an owner")
|
|
cache.complete(changed_key, payload(changed_key, extent), 0)
|
|
result = cache.claim(failure_key, extent, extent * 2)
|
|
if result[0] != "OWNER":
|
|
raise GateFailure("STOP", "evicted key reuse did not create a fresh owner")
|
|
after_payload = payload(failure_key, extent)
|
|
cache.complete(failure_key, after_payload, 0)
|
|
after = sha256_bytes(after_payload)
|
|
if before != after:
|
|
raise GateFailure("STOP", "eviction changed decoded bytes")
|
|
|
|
inflight_key = (44, extent, 12288, 0, 8192, extent, 0, 3, 1)
|
|
if cache.claim(inflight_key, extent, extent * 2)[0] != "OWNER":
|
|
raise GateFailure("STOP", "reclaim setup did not create owner")
|
|
if cache.reclaim() != "BUSY":
|
|
raise GateFailure("STOP", "reclaim did not refuse inflight state")
|
|
fallback_key = (45, extent, 16384, 0, 8192, extent, 0, 0, 1)
|
|
if cache.claim(fallback_key, extent, extent * 2)[0] != "BYPASS":
|
|
raise GateFailure("STOP", "different-key inflight miss did not bypass")
|
|
fallback_hash = sha256_bytes(payload(fallback_key, extent))
|
|
cache.complete(inflight_key, payload(inflight_key, extent), 0)
|
|
if cache.reclaim() != "RECLAIMED" or global_budget.used != 0:
|
|
raise GateFailure("STOP", "ready reclaim did not release budget")
|
|
|
|
first = Cache(global_budget, extent, config["minimum_decode_work_bytes"])
|
|
second = Cache(global_budget, extent, config["minimum_decode_work_bytes"])
|
|
if first.claim(key, extent, extent * 2)[0] != "OWNER":
|
|
raise GateFailure("STOP", "global budget setup failed")
|
|
first.complete(key, payload(key, extent), 0)
|
|
if second.claim(changed_key, extent, extent * 2)[0] != "OWNER":
|
|
raise GateFailure("STOP", "global budget should fit two reservations")
|
|
second.complete(changed_key, payload(changed_key, extent), 0)
|
|
third = Cache(global_budget, extent, config["minimum_decode_work_bytes"])
|
|
if third.claim(fallback_key, extent, extent * 2)[0] != "BYPASS":
|
|
raise GateFailure("STOP", "global exhaustion did not use no-cache fallback")
|
|
exhausted_hash = sha256_bytes(payload(fallback_key, extent))
|
|
if first.reclaim() != "RECLAIMED":
|
|
raise GateFailure("STOP", "global reclaim did not free a reservation")
|
|
if third.claim(fallback_key, extent, extent * 2)[0] != "OWNER":
|
|
raise GateFailure("STOP", "reclaimed global budget was not reusable")
|
|
third.complete(fallback_key, payload(fallback_key, extent), 0)
|
|
second.fini()
|
|
third.fini()
|
|
|
|
shutdown = Cache(global_budget, extent, config["minimum_decode_work_bytes"])
|
|
shutdown_key = (46, extent, 20480, 0, 8192, extent, 0, 1, 1)
|
|
if shutdown.claim(shutdown_key, extent, extent * 2)[0] != "OWNER":
|
|
raise GateFailure("STOP", "unmount setup did not create owner")
|
|
closing = threading.Event()
|
|
finished = threading.Event()
|
|
|
|
def finish_cache() -> None:
|
|
shutdown.fini(closing)
|
|
finished.set()
|
|
|
|
thread = threading.Thread(target=finish_cache)
|
|
thread.start()
|
|
if not closing.wait(timeout=10) or finished.is_set():
|
|
raise GateFailure("STOP", "unmount did not wait for inflight owner")
|
|
if shutdown.claim(changed_key, extent, extent * 2)[0] != "BYPASS":
|
|
raise GateFailure("STOP", "closing cache accepted a new owner")
|
|
shutdown.complete(shutdown_key, payload(shutdown_key, extent), 0)
|
|
thread.join(timeout=10)
|
|
if thread.is_alive() or not finished.is_set() or global_budget.used != 0:
|
|
raise GateFailure("STOP", "unmount drain did not close cleanly")
|
|
|
|
disabled = Cache(global_budget, extent, config["minimum_decode_work_bytes"], enabled=False)
|
|
if disabled.claim(key, extent, extent * 2)[0] != "BYPASS":
|
|
raise GateFailure("STOP", "disabled policy did not bypass")
|
|
low_work = Cache(global_budget, extent, config["minimum_decode_work_bytes"])
|
|
if low_work.claim(key, extent, config["minimum_decode_work_bytes"] - 1)[0] != "BYPASS":
|
|
raise GateFailure("STOP", "low-cost request did not bypass")
|
|
oversized = Cache(global_budget, extent // 2, config["minimum_decode_work_bytes"])
|
|
if oversized.claim(key, extent, extent * 2)[0] != "BYPASS":
|
|
raise GateFailure("STOP", "oversized request did not bypass")
|
|
no_cache_hash = sha256_bytes(payload(key, extent))
|
|
|
|
if global_budget.peak > global_budget.limit or cache.peak > cache.local_limit:
|
|
raise GateFailure("STOP", "state model exceeded hard budget")
|
|
return {
|
|
"budget": {
|
|
"global_limit": global_budget.limit,
|
|
"global_peak": global_budget.peak,
|
|
"global_remaining": global_budget.used,
|
|
"mount_limit": cache.local_limit,
|
|
"mount_peak": cache.peak,
|
|
},
|
|
"correctness": {
|
|
"eviction_sha256_before": before,
|
|
"eviction_sha256_after": after,
|
|
"fallback_sha256": fallback_hash,
|
|
"global_exhaustion_fallback_sha256": exhausted_hash,
|
|
"no_cache_sha256": no_cache_hash,
|
|
},
|
|
"coverage": [
|
|
"owner-waiter-success",
|
|
"typed-failure-retry",
|
|
"exact-key-reuse",
|
|
"ready-eviction",
|
|
"inflight-reclaim-busy",
|
|
"ready-reclaim",
|
|
"global-exhaustion-fallback",
|
|
"unmount-drain",
|
|
"disabled-policy",
|
|
"low-work-bypass",
|
|
"oversize-bypass",
|
|
],
|
|
"failure_wave": failure,
|
|
"lock_order": SPEC["lock_order"],
|
|
"status": "PASS",
|
|
"success_wave": success,
|
|
}
|
|
|
|
|
|
BENCH_SOURCE = r'''
|
|
#define _POSIX_C_SOURCE 200809L
|
|
#include <inttypes.h>
|
|
#include <limits.h>
|
|
#include <lz4.h>
|
|
#include <lzma.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <zlib.h>
|
|
#include <zstd.h>
|
|
|
|
enum codec_id { CODEC_LZ4, CODEC_LZMA, CODEC_DEFLATE, CODEC_ZSTD, CODEC_COUNT };
|
|
|
|
struct fixture {
|
|
enum codec_id id;
|
|
const char *name;
|
|
unsigned char *compressed;
|
|
size_t compressed_size;
|
|
};
|
|
|
|
static void
|
|
fail(const char *message)
|
|
{
|
|
fprintf(stderr, "P15-038 codec oracle failure: %s\n", message);
|
|
exit(10);
|
|
}
|
|
|
|
static uint64_t
|
|
now_ns(void)
|
|
{
|
|
struct timespec time;
|
|
|
|
if (clock_gettime(CLOCK_MONOTONIC, &time) != 0)
|
|
fail("clock_gettime");
|
|
return ((uint64_t)time.tv_sec * UINT64_C(1000000000) + (uint64_t)time.tv_nsec);
|
|
}
|
|
|
|
static uint32_t
|
|
xorshift32(uint32_t *state)
|
|
{
|
|
uint32_t value = *state;
|
|
|
|
value ^= value << 13;
|
|
value ^= value >> 17;
|
|
value ^= value << 5;
|
|
*state = value;
|
|
return (value);
|
|
}
|
|
|
|
static uint64_t
|
|
hash_update(uint64_t hash, const unsigned char *data, size_t size)
|
|
{
|
|
size_t index;
|
|
|
|
for (index = 0; index < size; ++index) {
|
|
hash ^= data[index];
|
|
hash *= UINT64_C(1099511628211);
|
|
}
|
|
return (hash);
|
|
}
|
|
|
|
static void
|
|
write_file(const char *path, const void *data, size_t size)
|
|
{
|
|
FILE *stream = fopen(path, "wb");
|
|
|
|
if (stream == NULL || fwrite(data, 1, size, stream) != size || fclose(stream) != 0)
|
|
fail("write fixture artifact");
|
|
}
|
|
|
|
static void
|
|
make_payload(unsigned char *payload, size_t size, uint32_t seed)
|
|
{
|
|
unsigned char block[4096];
|
|
size_t index;
|
|
|
|
for (index = 0; index < sizeof(block); ++index)
|
|
block[index] = (unsigned char)xorshift32(&seed);
|
|
for (index = 0; index < size; ++index)
|
|
payload[index] = block[index % sizeof(block)];
|
|
}
|
|
|
|
static struct fixture
|
|
compress_fixture(enum codec_id id, const char *name, const unsigned char *payload, size_t size)
|
|
{
|
|
struct fixture fixture = { .id = id, .name = name };
|
|
size_t capacity, output_position;
|
|
uLongf zlib_size;
|
|
int amount;
|
|
|
|
switch (id) {
|
|
case CODEC_LZ4:
|
|
capacity = (size_t)LZ4_compressBound((int)size);
|
|
fixture.compressed = malloc(capacity);
|
|
amount = LZ4_compress_default((const char *)payload,
|
|
(char *)fixture.compressed, (int)size, (int)capacity);
|
|
if (amount <= 0)
|
|
fail("LZ4 compression");
|
|
fixture.compressed_size = (size_t)amount;
|
|
break;
|
|
case CODEC_LZMA:
|
|
capacity = lzma_stream_buffer_bound(size);
|
|
fixture.compressed = malloc(capacity);
|
|
output_position = 0;
|
|
if (lzma_easy_buffer_encode(6, LZMA_CHECK_NONE, NULL, payload, size,
|
|
fixture.compressed, &output_position, capacity) != LZMA_OK)
|
|
fail("LZMA compression");
|
|
fixture.compressed_size = output_position;
|
|
break;
|
|
case CODEC_DEFLATE:
|
|
capacity = (size_t)compressBound((uLong)size);
|
|
fixture.compressed = malloc(capacity);
|
|
zlib_size = (uLongf)capacity;
|
|
if (compress2(fixture.compressed, &zlib_size, payload, (uLong)size, 6) != Z_OK)
|
|
fail("Deflate compression");
|
|
fixture.compressed_size = (size_t)zlib_size;
|
|
break;
|
|
case CODEC_ZSTD:
|
|
capacity = ZSTD_compressBound(size);
|
|
fixture.compressed = malloc(capacity);
|
|
fixture.compressed_size = ZSTD_compress(
|
|
fixture.compressed, capacity, payload, size, 3);
|
|
if (ZSTD_isError(fixture.compressed_size))
|
|
fail("Zstd compression");
|
|
break;
|
|
default:
|
|
fail("unknown compression codec");
|
|
}
|
|
if (fixture.compressed == NULL)
|
|
fail("compressed allocation");
|
|
return (fixture);
|
|
}
|
|
|
|
static void
|
|
decode(const struct fixture *fixture, unsigned char *output, size_t output_size)
|
|
{
|
|
size_t input_position, output_position, amount;
|
|
uint64_t memory_limit;
|
|
uLongf zlib_size;
|
|
int decoded;
|
|
|
|
switch (fixture->id) {
|
|
case CODEC_LZ4:
|
|
decoded = LZ4_decompress_safe((const char *)fixture->compressed,
|
|
(char *)output, (int)fixture->compressed_size, (int)output_size);
|
|
if (decoded != (int)output_size)
|
|
fail("LZ4 decode");
|
|
break;
|
|
case CODEC_LZMA:
|
|
memory_limit = UINT64_MAX;
|
|
input_position = 0;
|
|
output_position = 0;
|
|
if (lzma_stream_buffer_decode(&memory_limit, 0, NULL,
|
|
fixture->compressed, &input_position, fixture->compressed_size,
|
|
output, &output_position, output_size) != LZMA_OK ||
|
|
input_position != fixture->compressed_size || output_position != output_size)
|
|
fail("LZMA decode");
|
|
break;
|
|
case CODEC_DEFLATE:
|
|
zlib_size = (uLongf)output_size;
|
|
if (uncompress(output, &zlib_size, fixture->compressed,
|
|
(uLong)fixture->compressed_size) != Z_OK || zlib_size != output_size)
|
|
fail("Deflate decode");
|
|
break;
|
|
case CODEC_ZSTD:
|
|
amount = ZSTD_decompress(output, output_size,
|
|
fixture->compressed, fixture->compressed_size);
|
|
if (ZSTD_isError(amount) || amount != output_size)
|
|
fail("Zstd decode");
|
|
break;
|
|
default:
|
|
fail("unknown decompression codec");
|
|
}
|
|
}
|
|
|
|
static uint64_t
|
|
workload(const struct fixture *fixture, const unsigned char *payload,
|
|
size_t extent_size, size_t read_size, const size_t *offsets, size_t reads,
|
|
int neutral, size_t budget, size_t minimum_work, size_t *peak, uint64_t *hashp)
|
|
{
|
|
unsigned char *cached = NULL, *decoded, *slice;
|
|
uint64_t begin, end, hash = UINT64_C(1469598103934665603);
|
|
size_t index;
|
|
int admit;
|
|
|
|
admit = (fixture->id == CODEC_LZMA || neutral) && extent_size <= budget &&
|
|
fixture->compressed_size + extent_size >= minimum_work;
|
|
slice = malloc(read_size);
|
|
if (slice == NULL)
|
|
fail("slice allocation");
|
|
*peak = 0;
|
|
begin = now_ns();
|
|
for (index = 0; index < reads; ++index) {
|
|
if (cached == NULL) {
|
|
decoded = malloc(extent_size);
|
|
if (decoded == NULL)
|
|
fail("decode allocation");
|
|
decode(fixture, decoded, extent_size);
|
|
memcpy(slice, decoded + offsets[index], read_size);
|
|
if (admit) {
|
|
cached = decoded;
|
|
if (extent_size > *peak)
|
|
*peak = extent_size;
|
|
} else {
|
|
free(decoded);
|
|
}
|
|
} else {
|
|
memcpy(slice, cached + offsets[index], read_size);
|
|
}
|
|
if (memcmp(slice, payload + offsets[index], read_size) != 0)
|
|
fail("random read bytes differ");
|
|
hash = hash_update(hash, slice, read_size);
|
|
}
|
|
end = now_ns();
|
|
free(cached);
|
|
free(slice);
|
|
*hashp = hash;
|
|
return (end - begin);
|
|
}
|
|
|
|
static void
|
|
eviction_oracle(const struct fixture *first, const struct fixture *second,
|
|
size_t extent_size, const char *directory)
|
|
{
|
|
unsigned char *cache = malloc(extent_size);
|
|
char path[PATH_MAX];
|
|
|
|
if (cache == NULL)
|
|
fail("eviction allocation");
|
|
decode(first, cache, extent_size);
|
|
snprintf(path, sizeof(path), "%s/eviction-before.bin", directory);
|
|
write_file(path, cache, extent_size);
|
|
free(cache);
|
|
cache = malloc(extent_size);
|
|
if (cache == NULL)
|
|
fail("second eviction allocation");
|
|
decode(second, cache, extent_size);
|
|
free(cache);
|
|
cache = malloc(extent_size);
|
|
if (cache == NULL)
|
|
fail("reused eviction allocation");
|
|
decode(first, cache, extent_size);
|
|
snprintf(path, sizeof(path), "%s/eviction-after.bin", directory);
|
|
write_file(path, cache, extent_size);
|
|
free(cache);
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
static const char *names[CODEC_COUNT] = { "lz4", "lzma", "deflate", "zstd" };
|
|
struct fixture fixtures[CODEC_COUNT];
|
|
unsigned char *payload, *verification;
|
|
size_t *offsets;
|
|
size_t extent_size, read_size, reads, samples, budget, minimum_work;
|
|
size_t codec, sample, peak_current, peak_candidate;
|
|
uint32_t seed;
|
|
uint64_t current_ns, candidate_ns, current_hash, candidate_hash;
|
|
char path[PATH_MAX];
|
|
|
|
if (argc != 9)
|
|
fail("usage");
|
|
extent_size = (size_t)strtoull(argv[1], NULL, 10);
|
|
read_size = (size_t)strtoull(argv[2], NULL, 10);
|
|
reads = (size_t)strtoull(argv[3], NULL, 10);
|
|
samples = (size_t)strtoull(argv[4], NULL, 10);
|
|
budget = (size_t)strtoull(argv[5], NULL, 10);
|
|
minimum_work = (size_t)strtoull(argv[6], NULL, 10);
|
|
seed = (uint32_t)strtoul(argv[7], NULL, 10);
|
|
if (extent_size == 0 || extent_size > INT_MAX || read_size == 0 ||
|
|
read_size > extent_size || reads == 0 || samples != 5)
|
|
fail("invalid fixed workload");
|
|
payload = malloc(extent_size);
|
|
verification = malloc(extent_size);
|
|
offsets = malloc(reads * sizeof(*offsets));
|
|
if (payload == NULL || verification == NULL || offsets == NULL)
|
|
fail("workload allocation");
|
|
make_payload(payload, extent_size, seed);
|
|
for (codec = 0; codec < CODEC_COUNT; ++codec) {
|
|
fixtures[codec] = compress_fixture((enum codec_id)codec, names[codec],
|
|
payload, extent_size);
|
|
decode(&fixtures[codec], verification, extent_size);
|
|
if (memcmp(payload, verification, extent_size) != 0)
|
|
fail("full decode verification");
|
|
snprintf(path, sizeof(path), "%s/%s.compressed", argv[8], names[codec]);
|
|
write_file(path, fixtures[codec].compressed, fixtures[codec].compressed_size);
|
|
}
|
|
snprintf(path, sizeof(path), "%s/payload.bin", argv[8]);
|
|
write_file(path, payload, extent_size);
|
|
for (sample = 0; sample < reads; ++sample)
|
|
offsets[sample] = (size_t)xorshift32(&seed) % (extent_size - read_size + 1);
|
|
|
|
printf("{\"schema\":1,\"scope\":\"host-codec-cost-only\",\"samples\":%zu,\"reads_per_sample\":%zu,\"codecs\":[", samples, reads);
|
|
for (codec = 0; codec < CODEC_COUNT; ++codec) {
|
|
if (codec != 0)
|
|
printf(",");
|
|
printf("{\"codec\":\"%s\",\"compressed_bytes\":%zu,\"current_ns\":[",
|
|
fixtures[codec].name, fixtures[codec].compressed_size);
|
|
for (sample = 0; sample < samples; ++sample) {
|
|
if ((sample & 1) == 0) {
|
|
current_ns = workload(&fixtures[codec], payload, extent_size,
|
|
read_size, offsets, reads, 0, budget, minimum_work,
|
|
&peak_current, ¤t_hash);
|
|
candidate_ns = workload(&fixtures[codec], payload, extent_size,
|
|
read_size, offsets, reads, 1, budget, minimum_work,
|
|
&peak_candidate, &candidate_hash);
|
|
} else {
|
|
candidate_ns = workload(&fixtures[codec], payload, extent_size,
|
|
read_size, offsets, reads, 1, budget, minimum_work,
|
|
&peak_candidate, &candidate_hash);
|
|
current_ns = workload(&fixtures[codec], payload, extent_size,
|
|
read_size, offsets, reads, 0, budget, minimum_work,
|
|
&peak_current, ¤t_hash);
|
|
}
|
|
if (sample != 0)
|
|
printf(",");
|
|
printf("%" PRIu64, current_ns);
|
|
if (current_hash != candidate_hash || peak_candidate > budget)
|
|
fail("sample hash or budget mismatch");
|
|
/* Keep paired values for the second fixed-size arrays. */
|
|
snprintf(path, sizeof(path), "%s/sample-%zu-%zu.tmp", argv[8], codec, sample);
|
|
FILE *record = fopen(path, "w");
|
|
if (record == NULL || fprintf(record, "%" PRIu64 " %zu %zu %" PRIu64 "\n",
|
|
candidate_ns, peak_current, peak_candidate, current_hash) < 0 || fclose(record) != 0)
|
|
fail("sample record");
|
|
}
|
|
printf("],\"candidate_ns\":[");
|
|
for (sample = 0; sample < samples; ++sample) {
|
|
uint64_t saved_candidate, saved_hash;
|
|
FILE *record;
|
|
snprintf(path, sizeof(path), "%s/sample-%zu-%zu.tmp", argv[8], codec, sample);
|
|
record = fopen(path, "r");
|
|
if (record == NULL || fscanf(record, "%" SCNu64 " %zu %zu %" SCNu64,
|
|
&saved_candidate, &peak_current, &peak_candidate, &saved_hash) != 4 || fclose(record) != 0)
|
|
fail("read sample record");
|
|
if (sample != 0)
|
|
printf(",");
|
|
printf("%" PRIu64, saved_candidate);
|
|
}
|
|
printf("],\"current_peak_bytes\":[");
|
|
for (sample = 0; sample < samples; ++sample) {
|
|
uint64_t saved_candidate, saved_hash;
|
|
FILE *record;
|
|
snprintf(path, sizeof(path), "%s/sample-%zu-%zu.tmp", argv[8], codec, sample);
|
|
record = fopen(path, "r");
|
|
if (record == NULL || fscanf(record, "%" SCNu64 " %zu %zu %" SCNu64,
|
|
&saved_candidate, &peak_current, &peak_candidate, &saved_hash) != 4 || fclose(record) != 0)
|
|
fail("read current peak");
|
|
if (sample != 0)
|
|
printf(",");
|
|
printf("%zu", peak_current);
|
|
}
|
|
printf("],\"candidate_peak_bytes\":[");
|
|
for (sample = 0; sample < samples; ++sample) {
|
|
uint64_t saved_candidate, saved_hash;
|
|
FILE *record;
|
|
snprintf(path, sizeof(path), "%s/sample-%zu-%zu.tmp", argv[8], codec, sample);
|
|
record = fopen(path, "r");
|
|
if (record == NULL || fscanf(record, "%" SCNu64 " %zu %zu %" SCNu64,
|
|
&saved_candidate, &peak_current, &peak_candidate, &saved_hash) != 4 || fclose(record) != 0)
|
|
fail("read candidate peak");
|
|
if (sample != 0)
|
|
printf(",");
|
|
printf("%zu", peak_candidate);
|
|
remove(path);
|
|
}
|
|
printf("]}");
|
|
}
|
|
eviction_oracle(&fixtures[CODEC_DEFLATE], &fixtures[CODEC_ZSTD], extent_size, argv[8]);
|
|
printf("],\"status\":\"PASS\"}\n");
|
|
for (codec = 0; codec < CODEC_COUNT; ++codec)
|
|
free(fixtures[codec].compressed);
|
|
free(offsets);
|
|
free(verification);
|
|
free(payload);
|
|
return (0);
|
|
}
|
|
'''
|
|
|
|
|
|
def run_benchmark() -> tuple[dict[str, Any], dict[str, str]]:
|
|
artifacts = OUTPUT / "artifacts"
|
|
artifacts.mkdir()
|
|
source = artifacts / "P15-038-codec-bench.c"
|
|
source.write_text(BENCH_SOURCE.lstrip(), encoding="ascii")
|
|
config = SPEC["benchmark"]
|
|
budget = SPEC["budget"]
|
|
with tempfile.TemporaryDirectory(prefix="p15-038-") as temporary:
|
|
temp = Path(temporary)
|
|
binary = temp / "P15-038-codec-bench"
|
|
fixture_dir = temp / "fixtures"
|
|
fixture_dir.mkdir()
|
|
flags = run(
|
|
["pkg-config", "--cflags", "--libs", "liblz4", "liblzma", "zlib", "libzstd"],
|
|
timeout=30,
|
|
).stdout.split()
|
|
run(
|
|
["cc", "-std=c11", "-O2", "-Wall", "-Wextra", "-Werror", str(source), "-o", str(binary), *flags],
|
|
timeout=60,
|
|
log=artifacts / "compile.log",
|
|
)
|
|
completed = run(
|
|
[
|
|
str(binary),
|
|
str(config["extent_bytes"]),
|
|
str(config["read_bytes"]),
|
|
str(config["random_reads_per_sample"]),
|
|
str(config["samples"]),
|
|
str(budget["per_mount_bytes"]),
|
|
str(budget["minimum_decode_work_bytes"]),
|
|
str(config["seed"]),
|
|
str(fixture_dir),
|
|
],
|
|
timeout=240,
|
|
log=artifacts / "benchmark.log",
|
|
allowed={0, 10},
|
|
)
|
|
if completed.returncode != 0:
|
|
raise GateFailure("STOP", "real codec benchmark or correctness oracle failed")
|
|
try:
|
|
report = json.loads(completed.stdout)
|
|
except json.JSONDecodeError as error:
|
|
raise GateFailure("RUNNER_FAIL", f"invalid benchmark JSON: {error}") from error
|
|
hashes = {
|
|
path.name: sha256_path(path)
|
|
for path in sorted(fixture_dir.iterdir())
|
|
if path.is_file()
|
|
}
|
|
before = hashes.pop("eviction-before.bin")
|
|
after = hashes.pop("eviction-after.bin")
|
|
if before != after:
|
|
raise GateFailure("STOP", "real codec eviction changed decoded SHA-256")
|
|
hashes["eviction-before-after.sha256"] = before
|
|
write_json(artifacts / "fixture-hashes.json", hashes)
|
|
return report, hashes
|
|
|
|
|
|
def evaluate(report: dict[str, Any], state: dict[str, Any]) -> dict[str, Any]:
|
|
config = SPEC["benchmark"]
|
|
budget = SPEC["budget"]["per_mount_bytes"]
|
|
if report.get("status") != "PASS" or report.get("samples") != config["samples"]:
|
|
raise GateFailure("STOP", "benchmark did not return exactly five complete samples")
|
|
if report.get("reads_per_sample") != config["random_reads_per_sample"]:
|
|
raise GateFailure("STOP", "benchmark logical workload changed")
|
|
summaries = []
|
|
improved = 0
|
|
for codec in report.get("codecs", []):
|
|
current = codec.get("current_ns", [])
|
|
candidate = codec.get("candidate_ns", [])
|
|
peaks = codec.get("candidate_peak_bytes", [])
|
|
if len(current) != 5 or len(candidate) != 5 or len(peaks) != 5:
|
|
raise GateFailure("STOP", f"{codec.get('codec')} has a missing sample")
|
|
if any(value <= 0 for value in current + candidate):
|
|
raise GateFailure("STOP", f"{codec['codec']} has a non-positive latency")
|
|
if any(value > budget for value in peaks):
|
|
raise GateFailure("STOP", f"{codec['codec']} exceeded the hard budget")
|
|
current_median = statistics.median(current)
|
|
candidate_median = statistics.median(candidate)
|
|
improvement = (current_median - candidate_median) * 100.0 / current_median
|
|
passed = improvement >= config["minimum_latency_improvement_percent"]
|
|
if passed:
|
|
improved += 1
|
|
summaries.append(
|
|
{
|
|
"candidate_median_ns": candidate_median,
|
|
"candidate_ns": candidate,
|
|
"candidate_peak_bytes": peaks,
|
|
"codec": codec["codec"],
|
|
"compressed_bytes": codec["compressed_bytes"],
|
|
"current_median_ns": current_median,
|
|
"current_ns": current,
|
|
"improvement_percent": improvement,
|
|
"threshold_pass": passed,
|
|
}
|
|
)
|
|
if len(summaries) != 4:
|
|
raise GateFailure("STOP", "the four-codec benchmark is incomplete")
|
|
if improved < config["minimum_improved_codecs"]:
|
|
raise GateFailure("STOP", f"only {improved} codecs met the 10 percent latency threshold")
|
|
if state.get("status") != "PASS" or state["budget"]["global_remaining"] != 0:
|
|
raise GateFailure("STOP", "cache lifecycle state model did not close")
|
|
return {
|
|
"codecs_meeting_threshold": improved,
|
|
"minimum_codecs": config["minimum_improved_codecs"],
|
|
"minimum_improvement_percent": config["minimum_latency_improvement_percent"],
|
|
"samples_included_per_variant": 5,
|
|
"summaries": summaries,
|
|
}
|
|
|
|
|
|
exit_code = 2
|
|
try:
|
|
resolved, identity = verify_identity()
|
|
write_json(OUTPUT / "identity.json", {"base": resolved, **identity})
|
|
state = run_state_model()
|
|
write_json(OUTPUT / "state-model.json", state)
|
|
benchmark, fixture_hashes = run_benchmark()
|
|
write_json(OUTPUT / "benchmark.json", benchmark)
|
|
benefit = evaluate(benchmark, state)
|
|
write_json(OUTPUT / "benefit.json", benefit)
|
|
result = {
|
|
"b33": "AUTHORIZED",
|
|
"candidate": "P15-038",
|
|
"decision": "GO",
|
|
"eviction_sha256": fixture_hashes["eviction-before-after.sha256"],
|
|
"full_feature_suite": "NOT_RUN",
|
|
"gate": "G05",
|
|
"qemu": "NOT_RUN",
|
|
"reason": (
|
|
f"state model PASS; {benefit['codecs_meeting_threshold']}/4 codecs meet "
|
|
"the fixed five-sample >=10% host codec latency threshold; hard budgets and "
|
|
"eviction hashes pass"
|
|
),
|
|
"scope": SPEC["scope"],
|
|
"source_modified": False,
|
|
"status": "GO",
|
|
}
|
|
exit_code = 0
|
|
except GateFailure as error:
|
|
result = {
|
|
"b33": "STOP-NO-SOURCE" if error.status == "STOP" else "NOT_RUN",
|
|
"candidate": "P15-038",
|
|
"decision": "STOP" if error.status == "STOP" else error.status,
|
|
"full_feature_suite": "NOT_RUN",
|
|
"gate": "G05",
|
|
"qemu": "NOT_RUN",
|
|
"reason": error.reason,
|
|
"scope": SPEC.get("scope"),
|
|
"source_modified": False,
|
|
"status": error.status,
|
|
}
|
|
exit_code = 1 if error.status == "STOP" else 2
|
|
except Exception as error:
|
|
result = {
|
|
"b33": "NOT_RUN",
|
|
"candidate": "P15-038",
|
|
"decision": "RUNNER_FAIL",
|
|
"full_feature_suite": "NOT_RUN",
|
|
"gate": "G05",
|
|
"qemu": "NOT_RUN",
|
|
"reason": f"unhandled gate error: {type(error).__name__}: {error}",
|
|
"scope": SPEC.get("scope"),
|
|
"source_modified": False,
|
|
"status": "RUNNER_FAIL",
|
|
}
|
|
exit_code = 2
|
|
write_json(OUTPUT / "result.json", result)
|
|
write_json(
|
|
OUTPUT / "cleanup.json",
|
|
{
|
|
"binary_remaining": False,
|
|
"owned_processes_started": 0,
|
|
"owned_qemu_started": False,
|
|
"owned_temp_remaining": [],
|
|
"protected_pid_touched": False,
|
|
"protected_port_touched": False,
|
|
"source_modified": False,
|
|
"status": "PASS",
|
|
},
|
|
)
|
|
lines = []
|
|
for path in sorted(OUTPUT.rglob("*")):
|
|
if path.is_file() and path.name != "SHA256SUMS":
|
|
lines.append(f"{sha256_path(path)} {path.relative_to(OUTPUT)}")
|
|
(OUTPUT / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="ascii")
|
|
print(json.dumps(result, sort_keys=True))
|
|
raise SystemExit(exit_code)
|
|
PY
|