"""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.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 mod_jar(slug): """A mod's jar as pack/mods/.pw.toml pins it, for the tools that read a mod's own data.""" meta = tomllib.loads((ROOT / f"pack/mods/{slug}.pw.toml").read_text()) file = meta["download"] return fetch(file["url"], CACHE / "mods" / meta["filename"], file["hash"], file["hash-format"]) 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)