V1.0
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""make_release.py: makes the files of a game's GitHub release (the tag is the game, e.g. "rac1").
|
||||
|
||||
usage: python release/make_release.py rac1 --decomp PATH_TO_RAC1_DECOMP [--version V]
|
||||
[--toolchain-zip toolchain-win64.zip]
|
||||
|
||||
Writes dist/rac1/:
|
||||
rac1.json what the launcher's Build button fetches (version, files, SHA-256s, levels)
|
||||
rac1-build.zip games/rac1 (build.py, tools/, host/, launcher/, docs/) plus the decomp's
|
||||
source as decomp/ (everything but baserom/ and .git: no game files)
|
||||
toolchain-win64.zip Python, rabbitizer and zig for Windows x64 (release/toolchain.json); reused
|
||||
from --toolchain-zip when given, else fetched (PyPI and GitHub) and assembled
|
||||
|
||||
Upload the three files to the release named after the game (gh release create rac1 dist/rac1/*).
|
||||
Players never download anything from the game itself: the build reads their own disc image."""
|
||||
import argparse, datetime, glob, hashlib, io, json, os, shutil, subprocess, sys, tarfile, tempfile, zipfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(HERE)
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("game")
|
||||
ap.add_argument("--decomp", required=True, help="the game's decomp repo (only its source is packed; baserom/ is left out)")
|
||||
ap.add_argument("--version", default=datetime.date.today().strftime("%Y.%m.%d"))
|
||||
ap.add_argument("--toolchain-zip", default=None)
|
||||
ap.add_argument("--levels", default=",".join(str(i) for i in range(19)))
|
||||
a = ap.parse_args()
|
||||
|
||||
game = a.game
|
||||
src = os.path.join(ROOT, "games", game)
|
||||
if not os.path.isdir(src): sys.exit("no games/%s" % game)
|
||||
out = os.path.join(ROOT, "dist", game)
|
||||
os.makedirs(out, exist_ok=True)
|
||||
|
||||
SKIP_DIRS = {"__pycache__", ".git", "build", "dev", "decomp", "zig-cache"}
|
||||
DECOMP_SKIP = {"baserom", ".git", ".github"}
|
||||
|
||||
|
||||
def add_tree(z, top, arc, skip):
|
||||
for root, dirs, files in os.walk(top):
|
||||
dirs[:] = sorted(d for d in dirs if d not in skip)
|
||||
for f in sorted(files):
|
||||
if f.endswith((".pyc", ".o", ".obj")) or f == "build_log.txt": continue
|
||||
p = os.path.join(root, f)
|
||||
# one fixed, old timestamp for every file: files the player compiled before are
|
||||
# newer, so a retry of the same kit reuses them (a new kit version starts clean)
|
||||
zi = zipfile.ZipInfo(os.path.join(arc, os.path.relpath(p, top)).replace(os.sep, "/"), date_time=(2020, 1, 1, 0, 0, 0))
|
||||
zi.compress_type = zipfile.ZIP_DEFLATED
|
||||
zi.external_attr = 0o644 << 16
|
||||
with open(p, "rb") as fh: z.writestr(zi, fh.read(), compresslevel=9)
|
||||
|
||||
|
||||
def sha256(p):
|
||||
h = hashlib.sha256()
|
||||
with open(p, "rb") as f:
|
||||
for b in iter(lambda: f.read(1 << 20), b""): h.update(b)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
# the build kit
|
||||
kit = os.path.join(out, "%s-build.zip" % game)
|
||||
with zipfile.ZipFile(kit, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
|
||||
add_tree(z, src, "", SKIP_DIRS)
|
||||
add_tree(z, a.decomp, "decomp", DECOMP_SKIP)
|
||||
for f in ("THIRD_PARTY_NOTICES.md",):
|
||||
if os.path.exists(os.path.join(ROOT, f)): z.write(os.path.join(ROOT, f), f)
|
||||
for n in z.namelist(): # (nothing from the game: no executable, no overlays)
|
||||
if n.startswith("decomp/baserom/") or n.endswith(("SCES_509.16", ".iso")): sys.exit("refusing to pack %s" % n)
|
||||
print("kit:", kit, os.path.getsize(kit) // 1024, "KB")
|
||||
|
||||
# the toolchain
|
||||
tcj = json.load(open(os.path.join(HERE, "toolchain.json")))
|
||||
tcz = os.path.join(out, "toolchain-win64.zip")
|
||||
if a.toolchain_zip:
|
||||
if os.path.abspath(a.toolchain_zip) != os.path.abspath(tcz): shutil.copyfile(a.toolchain_zip, tcz)
|
||||
else:
|
||||
import urllib.request
|
||||
with tempfile.TemporaryDirectory() as t:
|
||||
print("fetching", tcj["python"])
|
||||
data = urllib.request.urlopen(tcj["python"]).read()
|
||||
tarfile.open(fileobj=io.BytesIO(data)).extractall(t)
|
||||
py = os.path.join(t, "python")
|
||||
for pat in ("*.pdb", "**/*.pdb"):
|
||||
for p in glob.glob(os.path.join(py, pat), recursive=True): os.remove(p)
|
||||
for d in ("tcl", "Lib/test", "Lib/idlelib", "Lib/tkinter", "Lib/turtledemo", "Lib/ensurepip", "include", "libs", "Scripts"):
|
||||
shutil.rmtree(os.path.join(py, d), ignore_errors=True)
|
||||
w = os.path.join(t, "wheels")
|
||||
subprocess.run([sys.executable, "-m", "pip", "download", "--no-deps", "--platform", "win_amd64", "--only-binary=:all:", "-d", w,
|
||||
tcj["ziglang_wheel"], tcj["rabbitizer_wheel"]], check=True)
|
||||
for whl in glob.glob(os.path.join(w, "rabbitizer-*.whl")): zipfile.ZipFile(whl).extractall(os.path.join(py, "Lib", "site-packages"))
|
||||
for whl in glob.glob(os.path.join(w, "ziglang-*.whl")): zipfile.ZipFile(whl).extractall(os.path.join(t, "zw"))
|
||||
zg = os.path.join(t, "zig")
|
||||
shutil.move(os.path.join(t, "zw", "ziglang"), zg)
|
||||
for f in ("__init__.py", "__main__.py"):
|
||||
if os.path.exists(os.path.join(zg, f)): os.remove(os.path.join(zg, f))
|
||||
# the C library sources and headers of other systems are not needed for x86-windows-gnu
|
||||
lc = os.path.join(zg, "lib", "libc")
|
||||
for d in os.listdir(lc):
|
||||
if d in ("glibc", "musl", "darwin", "wasi", "freebsd", "netbsd", "openbsd"): shutil.rmtree(os.path.join(lc, d))
|
||||
inc = os.path.join(lc, "include")
|
||||
for d in os.listdir(inc):
|
||||
if "windows" not in d: shutil.rmtree(os.path.join(inc, d))
|
||||
for d in ("libcxx", "libcxxabi", "libunwind", "libtsan", "docs", "build-web", "init"):
|
||||
shutil.rmtree(os.path.join(zg, "lib", d), ignore_errors=True)
|
||||
with zipfile.ZipFile(tcz, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
|
||||
add_tree(z, py, "python", set())
|
||||
add_tree(z, zg, "zig", set())
|
||||
print("toolchain:", tcz, os.path.getsize(tcz) // (1 << 20), "MB")
|
||||
|
||||
# what the launcher reads (a flat object: it is read without a JSON library)
|
||||
man = {
|
||||
"game": game,
|
||||
"version": a.version,
|
||||
"levels": a.levels,
|
||||
"kit_file": os.path.basename(kit), "kit_size": os.path.getsize(kit), "kit_sha256": sha256(kit),
|
||||
"toolchain_file": os.path.basename(tcz), "toolchain_size": os.path.getsize(tcz), "toolchain_sha256": sha256(tcz),
|
||||
"toolchain_version": tcj["version"],
|
||||
}
|
||||
with open(os.path.join(out, "%s.json" % game), "w") as f: json.dump(man, f, indent=1)
|
||||
print(json.dumps(man, indent=1))
|
||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"game": "rac1",
|
||||
"version": "2026.10.13.2",
|
||||
"levels": "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18",
|
||||
"kit_file": "rac1-build.zip",
|
||||
"kit_size": 5121567,
|
||||
"kit_sha256": "abd1bf3d10e1ad8aa9f1c57eb06a609e7b3d0b90c4b9e0cbfead1e042d5934f9",
|
||||
"toolchain_file": "toolchain-win64.zip",
|
||||
"toolchain_size": 84006129,
|
||||
"toolchain_sha256": "2d56894833ca4066faf5ac008d3fa06aa40e2bb1d1fcb8f5bd26f28a70382b63",
|
||||
"toolchain_version": "py3.12.7-zig0.16.0-rabbitizer1.16.2"
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"comment": "What toolchain-win64.zip is made of (release/make_release.py fetches these when it has no toolchain zip to reuse). All are redistributable: CPython (PSF licence), zig (MIT), rabbitizer (MIT).",
|
||||
"version": "py3.12.7-zig0.16.0-rabbitizer1.16.2",
|
||||
"python": "https://github.com/astral-sh/python-build-standalone/releases/download/20241016/cpython-3.12.7+20241016-x86_64-pc-windows-msvc-install_only.tar.gz",
|
||||
"ziglang_wheel": "ziglang==0.16.0",
|
||||
"rabbitizer_wheel": "rabbitizer==1.16.2"
|
||||
}
|
||||
Reference in New Issue
Block a user