Files
totk-pc-recomp/tools/package_build.py
T
Deon FoucheandClaude Opus 5.5 4c17621045 Write both Xbox prompt folders in the setup, the release and the builds
The setup runs make_romfs_patches.py --prompts xbox and --prompts xbox-label and warns once when either fails, the
release bundles tools/button_icons.py and fontTools, and build_game.py and package_build.py copy both folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:50:17 +10:00

112 lines
5.5 KiB
Python

"""Package a built totk.exe into a folder that runs from anywhere: build/<version>-portable by default.
The folder holds the exe, its DLLs, the shader translator (NativeAOT: no .NET), the RomFS index and patches, the
extracted ExeFS (the game code the exe loads), the shared fonts, the prepared shader and pipeline files. There is
no config file: totk.exe finds the exefs, romfs, profile and shared-fonts folders next to itself. Without --with-romfs
the 16 GB RomFS image stays out; copy build/extracted/<version>/romfs/romfs.bin to <folder>/romfs/ or set TOTK_ROMFS
(a path, relative ones to the exe's folder).
The folder contains game code and data derived from the game: for your own machines only, never publish or share it.
Saves go to <folder>/profile.
.venv/Scripts/python tools/package_build.py [--version v121] [--build build/v121] [--exe PATH/totk.exe]
[--out build/v121-portable] [--with-romfs] [--no-shader-cache]
"""
import argparse
import os
import shutil
import sys
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DLLS = ('SDL3.dll', 'libopus-0.dll', 'libiconv-2.dll')
# Prepared state next to the exe: translator answers, programs, the prepared list, pipeline cache and its keys, and the
# variant lists `totk.exe --prepare-shaders` reads (the driver keys its library cache by exe path, so each copy runs it).
STATE = ('romfs-index.bin', 'pipeline-cache.bin', 'pipeline-keys.bin', 'shader-answers.bin', 'shader-programs.bin',
'shader-prepared.bin', 'seed-answers.bin', 'seed-programs.bin')
def copy(src, dst):
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def copy_tree(src, dst, workers=16):
jobs = []
for dirpath, _, files in os.walk(src):
rel = Path(dirpath).relative_to(src)
(dst / rel).mkdir(parents=True, exist_ok=True)
jobs += [(Path(dirpath) / f, dst / rel / f) for f in files if not f.endswith('.tmp')]
with ThreadPoolExecutor(workers) as pool:
list(pool.map(lambda j: shutil.copy2(*j), jobs))
return len(jobs)
def size_of(path):
return sum(f.stat().st_size for f in Path(path).rglob('*') if f.is_file())
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--version', default='v121', help='game version (v121 or v143): the defaults below follow it')
ap.add_argument('--build', type=Path, help='build dir with the prepared state (default build/<version>)')
ap.add_argument('--exe', type=Path, help='totk.exe to package (default: <build>/totk.exe); its DLLs are taken from --build')
ap.add_argument('--xlate', type=Path, default=ROOT / 'build' / 'xlate' / 'totk_xlate.exe')
ap.add_argument('--extracted', type=Path, help='default build/extracted/<version>')
ap.add_argument('--out', type=Path, help='default build/<version>-portable')
ap.add_argument('--with-romfs', action='store_true', help='also copy romfs.bin (about 16 GB)')
ap.add_argument('--no-shader-cache', action='store_true', help='leave out shader-cache (about 2 GB): translates again')
a = ap.parse_args()
a.build = a.build or ROOT / 'build' / a.version
a.extracted = a.extracted or ROOT / 'build' / 'extracted' / a.version
a.out = a.out or ROOT / 'build' / f'{a.version}-portable'
build, out = a.build.resolve(), a.out.resolve()
exe = (a.exe or build / 'totk.exe').resolve()
fonts = a.extracted.resolve().parent / 'firmware' / 'shared-fonts'
exefs = a.extracted.resolve() / 'exefs'
romfs = a.extracted.resolve() / 'romfs' / 'romfs.bin'
need = [exe, a.xlate, exefs, fonts, build / 'romfs-patches'] + [build / d for d in DLLS] + [build / 'romfs-index.bin']
if a.with_romfs:
need.append(romfs)
missing = [str(p) for p in need if not p.exists()]
if missing:
sys.exit('package_build: missing: ' + ', '.join(missing))
if out == build:
sys.exit('package_build: --out must not be the build dir')
if not str(out).startswith(str(ROOT / 'build')) and not str(out).startswith(str(ROOT / 'generated')):
print('package_build: warning: the folder holds game code; keep it out of the repository')
out.mkdir(parents=True, exist_ok=True)
copy(exe, out / 'totk.exe')
for d in DLLS:
copy(build / d, out / d)
copy(a.xlate, out / 'totk_xlate.exe')
idf = a.xlate.with_suffix('.id')
if idf.exists():
copy(idf, out / 'totk_xlate.id')
for f in STATE:
if (build / f).exists():
copy(build / f, out / f)
else:
print(f'package_build: {f} not in {build}, skipped')
n = copy_tree(build / 'romfs-patches', out / 'romfs-patches')
for name in ('romfs-patches-xbox', 'romfs-patches-xbox-label'): # prompts = xbox (make_romfs_patches.py --prompts)
if (build / name).is_dir():
n += copy_tree(build / name, out / name)
n += copy_tree(exefs, out / 'exefs')
n += copy_tree(fonts, out / 'shared-fonts')
if not a.no_shader_cache and (build / 'shader-cache').is_dir():
n += copy_tree(build / 'shader-cache', out / 'shader-cache')
(out / 'profile').mkdir(exist_ok=True)
if a.with_romfs:
(out / 'romfs').mkdir(exist_ok=True)
print('package_build: copying romfs.bin (16 GB) ...')
shutil.copyfile(romfs, out / 'romfs' / 'romfs.bin')
print(f'package_build: {out}: {n} files in folders, {size_of(out) / 2**30:.2f} GiB'
+ ('' if a.with_romfs else '; no RomFS: put romfs.bin into romfs\\ or set TOTK_ROMFS'))
if __name__ == '__main__':
main()