zapret-kvn/scripts/prepare_core_release.py
loop-uh d5a4715771
Some checks failed
Windows project source guards / test (push) Has been cancelled
feat: use official Amnezia transport and organize runtime modules
2026-09-06 00:14:08 +03:00

116 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Resolve upstream once, update both platform pins, then freeze a release pair.
Run before source verification/commit. Repeating the same pair only validates
the freeze offline; a different pair always checks official GitHub upstream.
No commits, pushes, tags, publication or credentials are handled here.
"""
from __future__ import annotations
import argparse
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import re
import sys
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from scripts.check_core_release_freeze import digest, verify
from scripts.resolve_core_versions import atomic_write_lock, resolve_lock, update_amnezia_runtime
def android_pin_changes(android: Path, pin: dict) -> dict[Path, str]:
properties_path = android / "core.properties"
properties_text = properties_path.read_text(encoding="utf-8")
properties = dict(line.split("=", 1) for line in properties_text.splitlines() if "=" in line)
if properties["GO_VERSION"] != pin["toolchain"]["version"].removeprefix("go"):
raise ValueError("Update and validate both toolchains before release")
module, version = pin["module"], pin["version"]
previous = properties["ANDROID_AMNEZIAWG_GO"].split("@", 1)[1]
patch_path = android / "core-patches/0005-official-amnezia-wg-unified.patch"
patch = patch_path.read_text(encoding="utf-8")
if previous != version:
# Only retarget existing added module lines. An upstream transitive/API
# change that needs further edits will fail the normal readonly builds.
# Keep hunk lengths valid: substitute checksums in place, not remove rows.
patch = re.sub(rf'(?m)^(\+\s*{re.escape(module)} ){re.escape(previous)}(?=[ /\n])', rf'\g<1>{version}', patch)
for suffix, checksum in (("", pin["module_sum"]), ("/go.mod", pin["module_go_mod_sum"])):
pattern = rf'(?m)^(\+{re.escape(module)} {re.escape(version + suffix)} )h1:[^\n]+$'
patch, count = re.subn(pattern, lambda m: m.group(1) + checksum, patch)
if count != 1:
raise ValueError("Amnezia patch format changed; review module pin update")
manifest_path = android / "core-patches/series.sha256"
manifest = manifest_path.read_text(encoding="utf-8")
patch_hash = hashlib.sha256(patch.encode()).hexdigest()
manifest, count = re.subn(r'(?m)^[0-9a-f]{64}(\s+core-patches/0005-official-amnezia-wg-unified.patch)$', patch_hash + r'\1', manifest)
if count != 1:
raise ValueError("Missing official Amnezia patch in Android manifest")
updates = {
"ANDROID_WIREGUARD_GO": f"{module}@{version}",
"ANDROID_AMNEZIAWG_GO": f"{module}@{version}",
"CORE_PATCH_SHA256": hashlib.sha256(manifest.encode()).hexdigest(),
}
for key, value in updates.items():
properties_text, count = re.subn(rf'(?m)^{key}=.*$', f"{key}={value}", properties_text)
if count != 1:
raise ValueError(f"Missing or repeated Android pin {key}")
return {properties_path: properties_text, patch_path: patch, manifest_path: manifest}
def prepare(windows: Path, android: Path, windows_version: str, android_tag: str) -> dict:
if not re.fullmatch(r"\d+\.\d+\.\d+", windows_version) or not re.fullmatch(r"v\d+\.\d+\.\d+", android_tag):
raise ValueError("Only exact stable versions can be frozen")
releases = {"windows": windows_version, "android": android_tag}
receipt = windows / "core-release-freeze.json"
if receipt.exists() and json.loads(receipt.read_text())["releases"] == releases:
freeze = verify(windows, "windows", windows_version)
if verify(android, "android", android_tag) != freeze:
raise ValueError("Platform freezes differ")
return freeze
# Golden vectors and the dependency fix must remain identical across repos.
for left, right in (
("runtime/amnezia/testdata/wg_awg_golden.json", "wireguard-import/src/test/resources/wg_awg_golden.json"),
("core-patches/sing-udp.json", "core-patches/sing-udp.json"),
("scripts/prepare_sing_udp_patch.py", "scripts/prepare_sing_udp_patch.py"),
):
if (windows / left).read_bytes() != (android / right).read_bytes():
raise ValueError(f"Shared platform contract differs: {left}")
lock_path = windows / "scripts/core-lock.windows-x64.json"
lock = resolve_lock(json.loads(lock_path.read_text(encoding="utf-8")))
changes = android_pin_changes(android, lock["amnezia"])
# Restore this script's own pin edits if compatibility validation fails.
paths = [lock_path, windows / "runtime/amnezia/go.mod", windows / "runtime/amnezia/go.sum", *changes]
original = {path: path.read_bytes() for path in paths}
try:
update_amnezia_runtime(lock["amnezia"], windows)
for path, text in changes.items():
path.write_text(text, encoding="utf-8", newline="\n")
atomic_write_lock(lock_path, lock)
except Exception:
for path, data in original.items():
path.write_bytes(data)
raise
inputs = {
"windows": {name: digest(windows / name) for name in (
"scripts/core-lock.windows-x64.json", "runtime/amnezia/go.mod", "runtime/amnezia/go.sum")},
"android": {name: digest(android / name) for name in (
"core.properties", "core-patches/series.sha256", "core-patches/0005-official-amnezia-wg-unified.patch")},
}
freeze = {"schema": 1, "releases": releases, "checked_at_utc": datetime.now(timezone.utc).isoformat(),
"amnezia": lock["amnezia"], "inputs": inputs}
for root in (windows, android):
atomic_write_lock(root / "core-release-freeze.json", freeze)
verify(windows, "windows", windows_version)
verify(android, "android", android_tag)
return freeze
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--windows-version", required=True)
parser.add_argument("--android-tag", required=True)
parser.add_argument("--android-root", type=Path, default=ROOT.parent / "ZapretKVN")
args = parser.parse_args()
freeze = prepare(ROOT, args.android_root.resolve(), args.windows_version, args.android_tag)
print(json.dumps({"releases": freeze["releases"], "amnezia": freeze["amnezia"]["version"]}))