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>
This commit is contained in:
1 parent
4083791944
commit
6ace26e012
8 files changed
+139
-6
No files matched your search
@@ -6,6 +6,7 @@ Outputs, all under build/:
|
||||
instance and the server smoke test install this
|
||||
<slug>-<ver>.mrpack Modrinth pack
|
||||
<slug>-<ver>-curseforge.zip CurseForge pack
|
||||
<slug>-<ver>-private.mrpack every mod, by download link: the pack to hand out
|
||||
SHA256SUMS what the build produced
|
||||
|
||||
Reproducible: the same commit gives the same bytes, whatever the machine has
|
||||
@@ -142,6 +143,38 @@ def normalise_export(archive):
|
||||
z.writestr(info, files[name])
|
||||
|
||||
|
||||
def write_private_export(modrinth_pack, out):
|
||||
"""The pack Adrian hands out himself (2026-11-09: no public release): the Modrinth export plus the mods only
|
||||
CurseForge hosts, by their CurseForge download links. Still no third-party jar inside. Prism installs any link;
|
||||
Modrinth's own site and app only accept their listed hosts, which is why this is separate. The credits go in too."""
|
||||
with zipfile.ZipFile(modrinth_pack) as z:
|
||||
files = {i.filename: z.read(i.filename) for i in z.infolist() if not i.is_dir()}
|
||||
index = json.loads(files["modrinth.index.json"])
|
||||
listed = {Path(entry["path"]).name for entry in index["files"]}
|
||||
for path in sorted((PACK / "mods").glob("*.pw.toml")):
|
||||
meta = tomllib.loads(path.read_text())
|
||||
if meta["filename"] in listed:
|
||||
continue
|
||||
jar = lock.fetch_mod(meta)
|
||||
side = meta.get("side", "both")
|
||||
index["files"].append({
|
||||
"path": f"mods/{meta['filename']}",
|
||||
"hashes": {"sha1": lock.digest(jar, "sha1"), "sha512": lock.digest(jar, "sha512")},
|
||||
"env": {"client": "unsupported" if side == "server" else "required", "server": "unsupported" if side == "client" else "required"},
|
||||
"downloads": [lock.download_url(meta)],
|
||||
"fileSize": jar.stat().st_size,
|
||||
})
|
||||
index["files"].sort(key=lambda entry: entry["path"])
|
||||
index["summary"] = "Unofficial fan-made modpack, not affiliated with or endorsed by Nintendo, Game Freak, Creatures Inc., " \
|
||||
"The Pokémon Company, the Cobblemon team, Mojang Studios or Microsoft. Credits: CREDITS.md."
|
||||
files["modrinth.index.json"] = (json.dumps(index, indent=1, ensure_ascii=False) + "\n").encode()
|
||||
files["overrides/CREDITS.md"] = (ROOT / "docs/licenses.md").read_bytes()
|
||||
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for name, content in files.items():
|
||||
z.writestr(name, content)
|
||||
normalise_export(out)
|
||||
|
||||
|
||||
def write_checksums(paths):
|
||||
(BUILD / "SHA256SUMS").write_text("".join(f"{lock.digest(p)} {p.relative_to(ROOT).as_posix()}\n" for p in sorted(paths)))
|
||||
|
||||
@@ -176,6 +209,11 @@ def main():
|
||||
check_overrides(out, jar)
|
||||
report[out.name] = left_out
|
||||
|
||||
private = BUILD / f"{base}-private.mrpack"
|
||||
write_private_export(BUILD / f"{base}.mrpack", private)
|
||||
check_overrides(private, jar)
|
||||
report[private.name] = []
|
||||
|
||||
write_checksums([jar, BUILD / "stage/full/pack.toml", BUILD / "stage/full/index.toml", *(BUILD / name for name in report)])
|
||||
print("\nExports (no third-party jars in overrides):")
|
||||
for name, left_out in report.items():
|
||||
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the private pack as a launcher would install it: every link really downloads the pinned file, every mod of the
|
||||
pack is listed exactly once with the right side, and no third-party jar sits inside.
|
||||
|
||||
Downloads each file fresh, so it needs the network and takes a minute or two.
|
||||
|
||||
Usage: tools/check_private_pack.py
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
import tomllib
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
import lock
|
||||
|
||||
ROOT = lock.ROOT
|
||||
PACK = ROOT / "pack"
|
||||
|
||||
|
||||
def main():
|
||||
version = tomllib.loads((PACK / "pack.toml").read_text())["version"]
|
||||
archive = ROOT / f"build/cobblemon-survival-pack-{version}-private.mrpack"
|
||||
if not archive.exists():
|
||||
sys.exit(f"{archive} is missing: run tools/build.py first")
|
||||
problems = []
|
||||
with zipfile.ZipFile(archive) as z:
|
||||
names = z.namelist()
|
||||
index = json.loads(z.read("modrinth.index.json"))
|
||||
jars = [n for n in names if n.endswith(".jar")]
|
||||
if [PurePosixPath(n).name for n in jars] != ["cobblesurvival-0.0.1.jar"]:
|
||||
problems.append(f"jars inside the pack: {jars}")
|
||||
if "overrides/CREDITS.md" not in names:
|
||||
problems.append("no credits in the pack")
|
||||
entries = {PurePosixPath(e["path"]).name: e for e in index["files"]}
|
||||
if len(entries) != len(index["files"]):
|
||||
problems.append("a file is listed twice")
|
||||
metas = [tomllib.loads(p.read_text()) for p in sorted([*(PACK / "mods").glob("*.pw.toml"), *(PACK / "shaderpacks").glob("*.pw.toml")])]
|
||||
wanted = {m["filename"]: m for m in metas}
|
||||
if set(wanted) != set(entries):
|
||||
problems.append(f"missing {sorted(set(wanted) - set(entries))}, unexpected {sorted(set(entries) - set(wanted))}")
|
||||
total = 0
|
||||
for name, entry in sorted(entries.items()):
|
||||
request = urllib.request.Request(entry["downloads"][0], headers={"User-Agent": "cobblemon-survival-pack check"})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=300) as response:
|
||||
content = response.read()
|
||||
except OSError as e:
|
||||
problems.append(f"{name}: {e}")
|
||||
continue
|
||||
total += len(content)
|
||||
if hashlib.sha512(content).hexdigest() != entry["hashes"]["sha512"] or hashlib.sha1(content).hexdigest() != entry["hashes"]["sha1"]:
|
||||
problems.append(f"{name}: the download doesn't match its hash")
|
||||
if len(content) != entry["fileSize"]:
|
||||
problems.append(f"{name}: size {len(content)}, listed {entry['fileSize']}")
|
||||
side = wanted.get(name, {}).get("side", "both")
|
||||
expected = {"client": side != "server", "server": side != "client"}
|
||||
if {k: v == "required" for k, v in entry["env"].items()} != expected:
|
||||
problems.append(f"{name}: env {entry['env']} but side {side}")
|
||||
print(f"{len(entries)} files, {total / 1e6:.0f} MB downloaded and checked; {archive.name} is {archive.stat().st_size / 1e6:.1f} MB")
|
||||
if problems:
|
||||
print("\n".join(f" - {p}" for p in problems))
|
||||
sys.exit("PRIVATE PACK CHECK FAILED")
|
||||
print("PRIVATE PACK CHECK PASSED")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+16
-3
@@ -12,6 +12,7 @@ import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tomllib
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import venv
|
||||
from pathlib import Path
|
||||
@@ -49,11 +50,23 @@ def download(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."""
|
||||
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"])
|
||||
return fetch_mod(tomllib.loads((ROOT / f"pack/mods/{slug}.pw.toml").read_text()))
|
||||
|
||||
|
||||
def jdk():
|
||||
|
||||
Reference in new issue
Block a user