Build: every tool pinned (JDK, packwiz, Gradle plugins, Python packages), exports normalised, tools/check_reproducible.py proves two clean builds are byte-identical. Server-side balance: a server can load config/cobblesurvival/datapack.zip in place of the jar's data, /reload applies it, tools/server_update.py packages it and refuses when a client change is needed. JEI drop pages are sent by the server. Code-held balance numbers moved to tuning/server.json. Performance: spark in the pack (server only), tools/perf_test.py with fake players whose chunks really are active, docs/performance.md. Adrian's feedback: default keys R, U, J, §; drop pages link to Cobblemon Info and show real chances; bosses only hurt in battles; battle XP halved; channels between the islands low level; temperature ×3 with insulation doubled; hazards start at once; no item dots on the map; a wild attack starts a battle; the starter only faints; quest book on join with a basics page and an HMs page; regional forms obtainable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
4.5 KiB
Python
104 lines
4.5 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.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/<slug>.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)
|