Files
cobblemon-survival-pack/tools/check_reproducible.py
T
claudeandClaude Opus 5.5 4083791944 Phase 9: reproducible builds, balance from the server, spark check, play feedback
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>
2026-11-09 16:26:30 +01:00

76 lines
3.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Prove the build reproducible: build the working tree twice from clean copies in different folders, compare the bytes.
Copies every tracked and untracked file (not what .gitignore hides) to build/reproducible/a and build/reproducible/the/other/b,
so a leaked path shows up. Each build gets its own Gradle home with --fresh-gradle (slower: Loom sets up Minecraft
twice); the pinned tools are shared, since they're checked by hash. On a mismatch it names the jar or pack entries that differ.
Usage: tools/check_reproducible.py [--fresh-gradle]
"""
import argparse
import os
import shutil
import subprocess
import sys
import zipfile
from pathlib import Path
import lock
ROOT = lock.ROOT
WORK = ROOT / "build/reproducible"
def build(copy, fresh_gradle):
shutil.rmtree(copy, ignore_errors=True)
files = subprocess.run(["git", "ls-files", "-co", "--exclude-standard", "-z"], cwd=ROOT, check=True,
capture_output=True, text=True).stdout.split("\0")
for name in filter(None, files):
if (ROOT / name).is_file():
target = copy / name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(ROOT / name, target)
env = os.environ | {"COBBLESURVIVAL_CACHE": str(lock.CACHE)}
if fresh_gradle:
env["GRADLE_USER_HOME"] = str(copy.parent / f"{copy.name}-gradle")
with open(copy.parent / f"{copy.name}.log", "w") as log:
subprocess.run([sys.executable, str(copy / "tools/build.py")], cwd=copy, env=env, stdout=log, stderr=subprocess.STDOUT, check=True)
return dict(reversed(line.split(" ", 1)) for line in (copy / "build/SHA256SUMS").read_text().splitlines())
def entries(path):
with zipfile.ZipFile(path) as z:
return {i.filename: (i.CRC, i.date_time, i.external_attr) for i in z.infolist()}
def explain(a, b, name):
if not name.endswith((".jar", ".mrpack", ".zip")):
return
ea, eb = entries(a / name), entries(b / name)
for entry in sorted(ea.keys() | eb.keys()):
if ea.get(entry) != eb.get(entry):
print(f" {entry}: {ea.get(entry)} / {eb.get(entry)}")
if list(ea) != list(eb):
print(" entries are in a different order")
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--fresh-gradle", action="store_true", help="give each build its own empty Gradle home")
args = parser.parse_args()
a, b = WORK / "a", WORK / "the/other/b"
first, second = build(a, args.fresh_gradle), build(b, args.fresh_gradle)
different = [name for name in sorted(first.keys() | second.keys()) if first.get(name) != second.get(name)]
for name in sorted(first):
print(f"{'DIFFERENT' if name in different else 'same '} {first[name][:16]} {name}")
for name in different:
print(f" {name}:")
explain(a, b, name)
if different:
sys.exit(f"{len(different)} of {len(first)} outputs differ")
print(f"All {len(first)} outputs are byte-identical.")
if __name__ == "__main__":
main()