"""Assemble the release folder of docs/installer.md ("Release layout") from pinned downloads. .venv-py314\\Scripts\\python tools\\make_release.py --out build/release/TotK-Setup [--zip build/release/TotK-Setup.zip] .venv-py314\\Scripts\\python tools\\make_release.py --out build/release/TotK-Setup --prove Needs Python 3.14 (tarfile reads the MSYS2 .zst packages), uv on PATH and network access on the first run. Downloads are cached in toolchain/release-dl/ and checked against the sha256 in PINS; a mismatch stops the build. The folder holds: python/ python.org embeddable 3.14.9 + the packages of tools/requirements-installer.txt + tkinter (the Tcl/Tk 9.0 files come out of the python.org installer's own component MSI, unpacked with msiexec /a and expand.exe; the installer is never run) llvm/ llvm-mingw, x86_64 only, the files clang and lld need to compile and link totk.exe runtime/ libtotkrt.a (build_game.py --make-runtime-lib with llvm/), the runtime headers, romfs.c, patches.c and ov_zstd.c (compiled per install), deps/ (headers and import libraries of SDL3, Opus, libvpx, Vulkan and VMA) bin/ totk_xlate.exe (+ .id), hactool.exe, SDL3.dll (upstream), libopus-0.dll, ninja.exe recomp/, tools/, data/, installer/, licenses/, THIRD_PARTY_NOTICES.md, totk.ini.example VERSION the repository's VERSION plus the commit, 0.1.0+973c626; the installer's release string TotK-Setup.exe installer/launcher/launcher.c built with llvm/ (-mwindows): starts python/pythonw.exe -m installer.gui --prove compiles generated/v121 and links build/release-check/totk.exe with nothing but the folder's python/, llvm/, runtime/ and bin/ (PATH holds the Windows system directory and those), then runs the four probes there. It needs a generated/v121 and build/extracted/v121 from the usual game build (they are read, nothing is written to them). The folder contains nothing derived from the game: check_content runs over it before the script finishes. Then the folder's own python runs a check-only installer pass (no game files: it must get through the release, toolchain and host checks and stop at the missing game files) and the window's --selftest. """ import argparse import hashlib import json import os import shutil import struct import subprocess import sys import tarfile import time import urllib.request import zipfile from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import check_content # noqa: E402 import shader_list # noqa: E402 ROOT = Path(__file__).resolve().parent.parent MAIN_ROOT = (ROOT / 'build').resolve().parent # build/, generated/, toolchain/ and refs/ are junctions in a worktree DL = MAIN_ROOT / 'toolchain' / 'release-dl' WORK = MAIN_ROOT / 'toolchain' / 'release-work' # unpacked downloads MSYS2 = 'https://repo.msys2.org/mingw/ucrt64/' SDL_VERSION = '3.4.18' LLVM_VERSION = '20261020' MNG_COMMIT = 'b75be8cfa704df3701e86130c8ced268e79d6b11' # MacNativeGames, the shader list (refs/MacNativeGames) # name: (url, sha256) PINS = { 'python-embed': ('https://www.python.org/ftp/python/3.14.9/python-3.14.9-embed-amd64.zip', '3c01d7cfc666f746a6f064f60fb2156ab0ec464d3d738ab796371a46a9bb24a2'), 'python-installer': ('https://www.python.org/ftp/python/3.14.9/python-3.14.9-amd64.exe', 'a247a0b612244a803f507b39fb7121e3602e4160d59c3b99d98e735289c53394'), 'llvm-mingw': (f'https://github.com/mstorsjo/llvm-mingw/releases/download/{LLVM_VERSION}/' f'llvm-mingw-{LLVM_VERSION}-ucrt-x86_64.zip', 'fd5462b7f5d93b2ff981fcd3f9c7e427e47f3203c64ea91c27bcc444eebd74cd'), 'sdl3-dll': (f'https://github.com/libsdl-org/SDL/releases/download/release-{SDL_VERSION}/SDL3-{SDL_VERSION}-win32-x64.zip', '75c2c0fc74e7d1206aaedc22893a35887332f4f85f5c8b74fddc10d177985f2c'), 'sdl3-devel': (f'https://github.com/libsdl-org/SDL/releases/download/release-{SDL_VERSION}/SDL3-devel-{SDL_VERSION}-mingw.tar.gz', '049b9dd711e3fb3f8d84feb998dfec54a75e17cf539c4a80ff4d058d839771cf'), 'ninja': ('https://github.com/ninja-build/ninja/releases/download/v1.13.2/ninja-win.zip', '07fc8261b42b20e71d1720b39068c2e14ffcee6396b76fb7a795fb460b78dc65'), 'opus': (MSYS2 + 'mingw-w64-ucrt-x86_64-opus-1.6.1-2-any.pkg.tar.zst', 'bb114bcf26225f50f874497ebfd6b8224fff2a20bda3f8ee45cd07ce5f02657b'), 'libvpx': (MSYS2 + 'mingw-w64-ucrt-x86_64-libvpx-1.17.0-1-any.pkg.tar.zst', '8ff6919dbb88ea26ce28d22fd81d6f0d6e200cf56a4dd81ffc2d33124fe98024'), 'libvpx-src': ('https://github.com/webmproject/libvpx/archive/refs/tags/v1.17.0.tar.gz', # LICENSE and PATENTS only '1020f184046187baa2985dbde38e0691f49c44088bca7a1842b0236c6081dc0a'), 'vulkan-headers': (MSYS2 + 'mingw-w64-ucrt-x86_64-vulkan-headers-1~1.4.363.0-1-any.pkg.tar.zst', 'be602b2657cace531316e78b1eda4087e8f7f84ebb877f08fba198a2db06406a'), 'vulkan-loader': (MSYS2 + 'mingw-w64-ucrt-x86_64-vulkan-loader-1~1.4.363.0-1-any.pkg.tar.zst', # import library only '065076fe13d39ab97eea4740596426d5ae741b5044e48e1c9495d04835c92dbe'), 'vma': (MSYS2 + 'mingw-w64-ucrt-x86_64-vulkan-memory-allocator-3.4.0-1-any.pkg.tar.zst', '44c81162337915afecda3185f66fd98268a54454580b2717080c9dfef57fe781'), 'zstd': (MSYS2 + 'mingw-w64-ucrt-x86_64-zstd-1.5.7-2-any.pkg.tar.zst', # static libzstd.a (runtime/resolution.c) 'dbdb8427280046a2b41697780aa4c52983b708082b0da4755951dc3bea96ca89'), } # The component MSI inside the python.org installer that carries Tcl/Tk and tkinter (Burn names its payloads a0..a27). TCLTK_PAYLOAD = ('a12', 'ac87609db3f715404dd41a989c2f1706ea1512f3d86217f938d11c63af8eb505') SDL3_DLL_SHA256 = '2b4b128d8d510cc2d442693cf848c050d700d43dc667afe9e5fd82b0570d667e' # SDL3.dll inside the SDL zip # llvm-mingw: what clang and lld need to compile and link totk.exe for x86_64-w64-mingw32 (dynamic LLVM, libc++, compiler-rt). LLVM_BIN = {'clang.exe', 'clang++.exe', 'clang-23.exe', 'ld.lld.exe', 'llvm-ar.exe', 'llvm-ranlib.exe', 'llvm-nm.exe', 'llvm-objdump.exe', 'llvm-objcopy.exe', 'llvm-strip.exe', 'llvm-readobj.exe', 'llvm-dlltool.exe', 'llvm-rc.exe', 'llvm-cvtres.exe', 'llvm-windres.exe', 'llvm-wrapper.exe', 'mingw32-common.cfg', 'x86_64-w64-windows-gnu.cfg', 'x86_64-pc-windows-gnu.cfg', 'x86_64-w64-mingw32-clang.exe', 'x86_64-w64-mingw32-clang++.exe', 'libLLVM-23.dll', 'libclang-cpp.dll', 'libc++.dll', 'libunwind.dll', 'libwinpthread-1.dll', 'libffi-8.dll'} LLVM_SKIP_DIRS = {'aarch64-w64-mingw32', 'armv7-w64-mingw32', 'i686-w64-mingw32', 'busybox', 'python', 'share'} LLVM_SKIP_SUFFIXES = {'.idl', '.tlb'} # tools/ files the installer steps run (and what they import), copied from the repo. TOOLS_FILES = ['build_game.py', 'build_shaders.py', 'button_icons.py', 'check_content.py', 'extract.py', 'hostmem.py', 'make_romfs_patches.py', 'pack_shader_cache.py', 'prepare_shaders.py', 'romfs_index.py', 'shader_census.py', 'shader_list.py', 'ultrawide_layout.py'] TOOLS_DIRS = ['vendor/macnativegames', 'data'] # data/: expected hashes RUNTIME_SUFFIXES = {'.h', '.inc', '.ico'} # .ico: the installer window icon PROBES = ['--probe-kernel', '--probe-vkexec', '--probe-nvn-tex', '--probe-shader'] def sha256_file(path): h = hashlib.sha256() with open(path, 'rb') as f: for block in iter(lambda: f.read(1 << 20), b''): h.update(block) return h.hexdigest() def fetch(name): """The pinned download, from the cache or the network, checked against its sha256.""" url, want = PINS[name] DL.mkdir(parents=True, exist_ok=True) path = DL / Path(url.split('?')[0]).name.replace('%7E', '~') if name == 'libvpx-src': path = DL / 'libvpx-1.17.0.tar.gz' if not path.is_file(): print(f'download {url}') with urllib.request.urlopen(url) as r, open(path, 'wb') as f: shutil.copyfileobj(r, f) got = sha256_file(path) if got != want: sys.exit(f'{path}: sha256 {got}, pinned {want}') return path def unpack(name, member_filter=None): """Extract a pinned archive into toolchain/release-work/ (once) and return that directory.""" dest = WORK / name stamp = dest / '.unpacked' if stamp.is_file(): return dest shutil.rmtree(dest, ignore_errors=True) dest.mkdir(parents=True) path = fetch(name) if path.suffix == '.zip': with zipfile.ZipFile(path) as z: z.extractall(dest) else: with tarfile.open(path) as t: t.extractall(dest, filter='data') stamp.write_text('') return dest def copy_tree(src, dst, skip=lambda rel: False): """Copy src into dst file by file (sorted), leaving out what skip(relative posix path) names.""" for p in sorted(src.rglob('*')): rel = p.relative_to(src) if p.is_dir() or skip(rel.as_posix()): continue (dst / rel).parent.mkdir(parents=True, exist_ok=True) shutil.copy2(p, dst / rel) def clean_env(*path_dirs): """A process environment without MSYS2 and the user's tools: the Windows system directory and path_dirs only.""" system = os.environ.get('SystemRoot', r'C:\Windows') keep = ('SYSTEMROOT', 'SYSTEMDRIVE', 'TEMP', 'TMP', 'USERPROFILE', 'LOCALAPPDATA', 'APPDATA', 'COMSPEC', 'PATHEXT', 'NUMBER_OF_PROCESSORS') # os.environ keys are upper case on Windows env = {k: v for k, v in os.environ.items() if k.upper() in keep} env['PATH'] = os.pathsep.join([*(str(Path(d)) for d in path_dirs), rf'{system}\System32', system]) return env def run(cmd, **kw): print('+', ' '.join(str(c) for c in cmd), flush=True) return subprocess.run([str(c) for c in cmd], check=True, **kw) def mib(n): return f'{n / (1 << 20):.1f} MiB' def dir_size(p): return sum(f.stat().st_size for f in Path(p).rglob('*') if f.is_file()) # ---- python/ ------------------------------------------------------------------------------------------------------- def tcltk_files(): """Directory holding the python.org installer's tcltk payload, unpacked (not installed) into toolchain/release-work.""" dest = WORK / 'tcltk' if (dest / 'DLLs' / '_tkinter.pyd').is_file(): return dest exe = fetch('python-installer') data = exe.read_bytes() cabs = [] pos = data.find(b'MSCF\0\0\0\0') while pos >= 0: cabs.append((struct.unpack_from('/include and the x86_64 Windows builtins; no sanitizers, no other targets if rel.startswith('lib/clang/') and '/include/' in rel: return False return not rel.endswith('/lib/windows/libclang_rt.builtins-x86_64.a') return Path(rel).suffix.lower() in LLVM_SKIP_SUFFIXES or rel.startswith('include/ddk/') copy_tree(src, dst, skip) return dst # ---- bin/ and deps --------------------------------------------------------------------------------------------------- def build_bin(out): b = out / 'bin' b.mkdir(parents=True, exist_ok=True) for f in ('totk_xlate.exe', 'totk_xlate.id'): shutil.copy2(MAIN_ROOT / 'build' / 'xlate' / f, b / f) shutil.copy2(MAIN_ROOT / 'toolchain' / 'hactool' / 'hactool.exe', b / 'hactool.exe') dll = unpack('sdl3-dll') / 'SDL3.dll' if sha256_file(dll) != SDL3_DLL_SHA256: sys.exit(f'{dll}: not the pinned SDL3.dll') shutil.copy2(dll, b / 'SDL3.dll') shutil.copy2(unpack('opus') / 'ucrt64' / 'bin' / 'libopus-0.dll', b / 'libopus-0.dll') shutil.copy2(unpack('ninja') / 'ninja.exe', b / 'ninja.exe') def build_deps(out): """runtime/deps: the headers and import libraries build_game.py --deps expects (include/, lib/).""" d = out / 'runtime' / 'deps' (d / 'include').mkdir(parents=True, exist_ok=True) (d / 'lib').mkdir(parents=True, exist_ok=True) sdl = unpack('sdl3-devel') / f'SDL3-{SDL_VERSION}' / 'x86_64-w64-mingw32' copy_tree(sdl / 'include', d / 'include') shutil.copy2(sdl / 'lib' / 'libSDL3.dll.a', d / 'lib') for pkg, dirs, libs in (('opus', ['opus'], ['libopus.dll.a']), ('libvpx', ['vpx'], ['libvpx.a']), ('vulkan-headers', ['vulkan', 'vk_video'], []), ('vulkan-loader', [], ['libvulkan-1.dll.a']), ('vma', ['vma'], [])): root = unpack(pkg) / 'ucrt64' for inc in dirs: copy_tree(root / 'include' / inc, d / 'include' / inc) for lib in libs: shutil.copy2(root / 'lib' / lib, d / 'lib') zstd = unpack('zstd') / 'ucrt64' for name in ('zstd.h', 'zstd_errors.h'): shutil.copy2(zstd / 'include' / name, d / 'include') shutil.copy2(zstd / 'lib' / 'libzstd.a', d / 'lib') def collect_licenses(out): lic = out / 'licenses' lic.mkdir(parents=True, exist_ok=True) pairs = [ (unpack('opus') / 'ucrt64' / 'share' / 'licenses' / 'opus' / 'COPYING', 'opus-COPYING.txt'), (unpack('vulkan-headers') / 'ucrt64' / 'share' / 'licenses' / 'vulkan-headers' / 'LICENSE', 'vulkan-headers-LICENSE.txt'), (unpack('vulkan-loader') / 'ucrt64' / 'share' / 'licenses' / 'vulkan-loader' / 'LICENSE', 'vulkan-loader-LICENSE.txt'), (unpack('vma') / 'ucrt64' / 'share' / 'licenses' / 'VulkanMemoryAllocator' / 'LICENSE', 'vulkan-memory-allocator-LICENSE.txt'), (unpack('sdl3-dll') / 'LICENSE.txt', 'SDL3-LICENSE.txt'), (unpack('zstd') / 'ucrt64' / 'share' / 'licenses' / 'zstd' / 'LICENSE', 'zstd-LICENSE.txt'), (ROOT / 'runtime' / 'shaders' / 'ffx' / 'LICENSE.txt', 'FidelityFX-FSR-LICENSE.txt'), (MAIN_ROOT / 'toolchain' / 'hactool' / 'LICENSE', 'hactool-LICENSE.txt'), (MAIN_ROOT / 'refs' / 'MacNativeGames' / 'LICENSE', 'MacNativeGames-LICENSE.txt'), ] for src, name in pairs: shutil.copy2(src, lic / name) with tarfile.open(fetch('libvpx-src')) as t: # the MSYS2 package carries no licence text; the notice needs both for member, name in (('libvpx-1.17.0/LICENSE', 'libvpx-LICENSE.txt'), ('libvpx-1.17.0/PATENTS', 'libvpx-PATENTS.txt')): (lic / name).write_bytes(t.extractfile(member).read()) for name in ('LICENSE.TXT',): shutil.copy2(out / 'llvm' / name, lic / 'llvm-mingw-LICENSE.txt') # ---- runtime/, recomp/, tools/, data/ --------------------------------------------------------------------------------- def runtime_lib(out, llvm): """Build libtotkrt.a with the folder's llvm/ and copy it, the headers and the install-side sources into runtime/.""" build = MAIN_ROOT / 'build' / 'release-check' / 'rtlib' gen = MAIN_ROOT / 'generated' / 'v121' run([sys.executable, ROOT / 'tools' / 'build_game.py', '--make-runtime-lib', '--toolchain', llvm, '--deps', out / 'runtime' / 'deps', '--ninja', out / 'bin' / 'ninja.exe', '--gen', gen, '--out', build], env=clean_env(llvm / 'bin', out / 'bin')) rt = out / 'runtime' shutil.copy2(build / 'libtotkrt.a', rt / 'libtotkrt.a') for p in sorted((ROOT / 'runtime').rglob('*')): if p.suffix in RUNTIME_SUFFIXES and p.is_file(): (rt / p.relative_to(ROOT / 'runtime')).parent.mkdir(parents=True, exist_ok=True) shutil.copy2(p, rt / p.relative_to(ROOT / 'runtime')) for name in ('romfs.c', 'patches.c', 'ov_zstd.c'): # build_game.py INSTALL_SIDE shutil.copy2(ROOT / 'runtime' / name, rt / name) def build_sources(out): copy_tree(ROOT / 'recomp', out / 'recomp', lambda r: not r.endswith(('.py', '.txt')) or '__pycache__' in r) for f in TOOLS_FILES: (out / 'tools').mkdir(exist_ok=True) shutil.copy2(ROOT / 'tools' / f, out / 'tools' / f) for d in TOOLS_DIRS: copy_tree(ROOT / 'tools' / d, out / 'tools' / d, lambda r: '__pycache__' in r or r.endswith('.md')) (out / 'data').mkdir(exist_ok=True) text = shader_list.history(MAIN_ROOT / 'refs' / 'MacNativeGames', MNG_COMMIT) (out / 'data' / 'shader_list.tsv').write_text(text, encoding='utf-8', newline='\n') copy_tree(ROOT / 'installer', out / 'installer', lambda r: '__pycache__' in r or r.startswith('launcher/')) shutil.copy2(ROOT / 'totk.ini.example', out / 'totk.ini.example') shutil.copy2(ROOT / 'THIRD_PARTY_NOTICES.md', out / 'THIRD_PARTY_NOTICES.md') (out / 'VERSION').write_text(version() + '\n', encoding='utf-8') def version(): """The port version from VERSION with the commit as build metadata: 0.1.0+973c626. The commit keeps two releases of one version apart, so the installer reruns the steps an earlier release made.""" r = subprocess.run(['git', '-C', ROOT, 'rev-parse', '--short', 'HEAD'], capture_output=True, text=True) if r.returncode or not r.stdout.strip(): sys.exit('no git version: not a git checkout') return (ROOT / 'VERSION').read_text(encoding='utf-8').strip() + '+' + r.stdout.strip() # ---- TotK-Setup.exe -------------------------------------------------------------------------------------------------- def c_wide(text): """A C wide string literal of text: ASCII as is, everything else as a universal character name.""" bs = chr(92) out = [] for ch in text: if ch in (bs, '"'): out.append(bs + ch) elif 32 <= ord(ch) < 127: out.append(ch) elif ord(ch) < 0x10000: out.append(f'{bs}u{ord(ch):04x}') else: sys.exit(f'launcher text: {ch!r} is outside the BMP') return 'L"' + ''.join(out) + '"' def launcher_header(path): """launcher_text.h: the launcher's two messages in English and Russian, taken from the installer's catalog.""" lines = ['/* generated by tools/make_release.py from installer/text/{en,ru}.json */'] for lang in ('en', 'ru'): texts = json.loads((ROOT / 'installer' / 'text' / f'{lang}.json').read_text(encoding='utf-8')) for key in ('title', 'python_missing'): lines.append(f'#define LAUNCHER_{key.upper()}_{lang.upper()} {c_wide(texts["launcher." + key])}') path.parent.mkdir(parents=True, exist_ok=True) path.write_text('\n'.join(lines) + '\n', encoding='ascii') def build_launcher(out, llvm): work = MAIN_ROOT / 'build' / 'release-check' / 'launcher' launcher_header(work / 'launcher_text.h') env = clean_env(llvm / 'bin') res = work / 'launcher_rc.o' run([llvm / 'bin' / 'llvm-windres.exe', '-I', ROOT / 'runtime', '-O', 'coff', ROOT / 'installer' / 'launcher' / 'launcher.rc', '-o', res], env=env) run([llvm / 'bin' / 'clang.exe', '-O2', '-mwindows', '-Wall', '-Wl,-s', '-I', work, ROOT / 'installer' / 'launcher' / 'launcher.c', res, '-o', out / 'TotK-Setup.exe'], env=env) # ---- checks ---------------------------------------------------------------------------------------------------------- def check_folder(out): """check_content over every file of the release folder; a hit stops the build.""" bad = [] for p in sorted(out.rglob('*')): if p.is_file(): bad += [f'{p.relative_to(out)}: {problem}' for problem in check_content.problems_for(p, allow_large=True)] if bad: sys.exit('game-derived content in the release folder:\n' + '\n'.join(bad)) print(f'check_content: {sum(1 for p in out.rglob("*") if p.is_file())} files, no hit') def check_bundle(out): """The folder's own python runs the installer without game files and the window's selftest.""" chk = MAIN_ROOT / 'build' / 'release-check' / 'bundle' shutil.rmtree(chk, ignore_errors=True) env = clean_env(out / 'python') py = out / 'python' / 'python.exe' r = subprocess.run([py, '-m', 'installer', '--lang', 'en-US', '--dest', chk / 'install', '--temp', chk / 'temp'], cwd=out, env=env, capture_output=True, text=True, timeout=600) events = [json.loads(line) for line in r.stdout.splitlines() if line.startswith('{')] errors = [e['id'] for e in events if e['type'] == 'error'] started = any(e['type'] == 'start' and e.get('release') == (out / 'VERSION').read_text().strip() for e in events) steps = [e['phase'] for e in events if e['type'] == 'progress'] if errors != ['check.inputs_needed'] or not started or 'inputs' not in steps: sys.exit(f'bundle check: exit {r.returncode}, errors {errors}, release read {started}\n{r.stdout[-2000:]}\n{r.stderr[-2000:]}') print(f'bundle check: the installer ran the host check from the folder and stopped at the game files (exit {r.returncode})') r = subprocess.run([py, '-m', 'installer.gui', '--selftest'], cwd=out, env=env, capture_output=True, text=True, timeout=300) if r.returncode: sys.exit(f'bundle check: window selftest exit {r.returncode}\n{r.stdout[-2000:]}\n{r.stderr[-2000:]}') print('bundle check: the window built every screen in both languages and closed') def sizes(out): rows = [(p.name if p.is_dir() else p.name, dir_size(p) if p.is_dir() else p.stat().st_size) for p in sorted(out.iterdir())] for name, n in rows: print(f' {name:26} {mib(n):>12}') print(f' {"total":26} {mib(sum(n for _, n in rows)):>12}') def write_zip(out, path): path.parent.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as z: for p in sorted(out.rglob('*')): if p.is_file(): info = zipfile.ZipInfo((Path(out.name) / p.relative_to(out)).as_posix(), (1980, 1, 1, 0, 0, 0)) info.compress_type = zipfile.ZIP_DEFLATED info.external_attr = 0o644 << 16 z.writestr(info, p.read_bytes(), compresslevel=6) print(f'{path}: {mib(path.stat().st_size)}') def prove(out): """Compile generated/v121 and link totk.exe with the folder's tools only, then run the probes next to it.""" out = out.resolve() gen = MAIN_ROOT / 'generated' / 'v121' ext = MAIN_ROOT / 'build' / 'extracted' / 'v121' chk = MAIN_ROOT / 'build' / 'release-check' env = clean_env(out / 'python', out / 'llvm' / 'bin', out / 'bin') t0 = time.time() run([out / 'python' / 'python.exe', out / 'tools' / 'build_game.py', '--toolchain', out / 'llvm', '--deps', out / 'runtime' / 'deps', '--runtime', out / 'runtime', '--runtime-lib', out / 'runtime' / 'libtotkrt.a', '--ninja', out / 'bin' / 'ninja.exe', '--gen', gen, '--out', chk, '--exefs', ext / 'exefs', '--romfs', ext / 'romfs', '--profile', chk / 'profile'], env=env) print(f'prove: build {time.time() - t0:.0f} s') for dll in ('SDL3.dll', 'libopus-0.dll'): shutil.copy2(out / 'bin' / dll, chk / dll) failed = [] for probe in PROBES: r = subprocess.run([str(chk / 'totk.exe'), probe], cwd=chk, env=clean_env(chk), capture_output=True, text=True, timeout=900) tail = (r.stdout.strip().splitlines() or [''])[-1] print(f'{probe}: exit {r.returncode}: {tail}') if r.returncode: failed.append(probe) if failed: sys.exit(f'probes failed: {failed}') def main(): ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument('--out', type=Path, required=True, help='release folder (replaced)') ap.add_argument('--zip', type=Path, help='also write the folder as this zip') ap.add_argument('--prove', action='store_true', help='only run the compile-and-probe check on an existing folder') args = ap.parse_args() out = args.out.resolve() if args.prove: prove(out) return if out.exists(): shutil.rmtree(out) out.mkdir(parents=True) build_python(out) llvm = build_llvm(out) build_bin(out) build_deps(out) runtime_lib(out, llvm) build_sources(out) build_launcher(out, llvm) collect_licenses(out) check_folder(out) check_bundle(out) sizes(out) if args.zip: write_zip(out, args.zip.resolve()) if __name__ == '__main__': main()