Files
claudeandClaude Opus 5.5 6ace26e012 Phase 9: the private pack to hand out
build/...-private.mrpack lists every mod by download link, CurseForge-only ones
included, with the credits and non-affiliation notice inside and no third-party jar.
tools/check_private_pack.py downloads all of it and checks the hashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 16:49:46 +01:00

117 lines
5.0 KiB
Python

"""The build's pinned tools (tools/build-lock.toml, tools/requirements.txt) and the environment it runs in.
Each tool is downloaded once into build/.cache and checked against its hash, so what a build runs depends on the
repo, not on what the machine has installed. COBBLESURVIVAL_CACHE moves the cache (tools/check_reproducible.py shares
one between its builds: everything in it is checked by hash, so it can't change their result).
"""
import hashlib
import os
import platform
import shutil
import subprocess
import sys
import tarfile
import tomllib
import urllib.parse
import urllib.request
import venv
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
LOCK = tomllib.loads((ROOT / "tools/build-lock.toml").read_text())
CACHE = Path(os.environ.get("COBBLESURVIVAL_CACHE", ROOT / "build/.cache"))
# Nothing the build writes may depend on the machine's clock zone, language or Python's per-run hash seed.
BUILD_ENV = {"TZ": "UTC", "LC_ALL": "C.UTF-8", "PYTHONHASHSEED": "0", "PYTHONUTF8": "1"}
def digest(path, algorithm="sha256"):
return hashlib.new(algorithm, Path(path).read_bytes()).hexdigest()
def fetch(url, dest, expected, algorithm="sha256"):
"""[dest], downloaded from [url] if it isn't there yet; exits if the file is not the one pinned."""
if dest.exists() and digest(dest, algorithm) == expected:
return dest
dest.parent.mkdir(parents=True, exist_ok=True)
part = dest.with_name(dest.name + ".part")
request = urllib.request.Request(url, headers={"User-Agent": "cobblemon-survival-pack build"})
with urllib.request.urlopen(request, timeout=300) as response, open(part, "wb") as out:
shutil.copyfileobj(response, out)
if digest(part, algorithm) != expected:
part.unlink()
sys.exit(f"{url} does not match the pinned {algorithm} {expected}")
part.replace(dest)
return dest
def download(name):
"""A file listed under [downloads] in tools/build-lock.toml."""
entry = LOCK["downloads"][name]
return fetch(entry["url"], CACHE / name, entry["sha256"])
def download_url(meta):
"""Where a packwiz metafile's jar comes from; a CurseForge-only mod records no URL, only the file's id."""
if "url" in meta["download"]:
return meta["download"]["url"]
file_id = meta["update"]["curseforge"]["file-id"]
return f"https://edge.forgecdn.net/files/{file_id // 1000}/{file_id % 1000}/{urllib.parse.quote(meta['filename'])}"
def fetch_mod(meta):
"""The jar a packwiz metafile pins, checked against its hash."""
file = meta["download"]
return fetch(download_url(meta), CACHE / "mods" / meta["filename"], file["hash"], file["hash-format"])
def mod_jar(slug):
"""A mod's jar as pack/mods/<slug>.pw.toml pins it, for the tools that read a mod's own data."""
return fetch_mod(tomllib.loads((ROOT / f"pack/mods/{slug}.pw.toml").read_text()))
def jdk():
"""The pinned JDK's home, the only one Gradle gets to compile with."""
pin = LOCK["jdk"]
here = f"{platform.system().lower()}-{platform.machine()}"
if here != pin["platform"]:
sys.exit(f"tools/build-lock.toml pins a JDK for {pin['platform']}, this machine is {here}")
home = CACHE / f"jdk-{pin['version']}"
if not (home / "bin/javac").exists():
archive = fetch(pin["url"], CACHE / f"jdk-{pin['version']}.tar.gz", pin["sha256"])
unpacking = home.with_name(home.name + ".part")
shutil.rmtree(unpacking, ignore_errors=True)
unpacking.mkdir(parents=True)
with tarfile.open(archive) as tar:
tar.extractall(unpacking, filter="tar")
shutil.rmtree(home, ignore_errors=True)
next(unpacking.iterdir()).rename(home)
unpacking.rmdir()
return home
def packwiz():
"""The pinned packwiz, built from its pinned commit with the pinned Go."""
pin = LOCK["packwiz"]
binary = CACHE / f"packwiz-{pin['version']}" / "packwiz"
if not binary.exists():
if not shutil.which("go"):
sys.exit("building the pinned packwiz needs Go (https://go.dev/dl/)")
env = os.environ | {"GOBIN": str(binary.parent), "GOTOOLCHAIN": pin["go"], "GOFLAGS": "-trimpath"}
subprocess.run(["go", "install", f"{pin['module']}@{pin['version']}"], env=env, check=True)
return binary
def enter_venv():
"""Re-runs the calling script in a private venv with exactly tools/requirements.txt and the build's environment."""
requirements = ROOT / "tools/requirements.txt"
home = CACHE / f"venv-{digest(requirements)[:12]}"
if Path(sys.prefix) == home and all(os.environ.get(k) == v for k, v in BUILD_ENV.items()):
return
if f"{sys.version_info[0]}.{sys.version_info[1]}" != LOCK["python"]:
print(f"warning: tools/build-lock.toml pins Python {LOCK['python']}, this is {sys.version.split()[0]}", file=sys.stderr)
python = home / "bin/python"
if not python.exists():
venv.create(home, with_pip=True)
subprocess.run([python, "-m", "pip", "install", "--quiet", "--require-hashes", "-r", requirements], check=True)
os.execve(python, [str(python), sys.argv[0], *sys.argv[1:]], os.environ | BUILD_ENV)