#!/usr/bin/env python3 """mkdist.py -- build the tester-distribution zip. Layout (matches what the player bats expect): BT411--/ play_solo.bat join.bat join_lan.bat (from players/) build/Release/btl4.exe (pod build -- fallback) build-glass/Release/btl4.exe (preferred: clickable cockpit) content/... (git-TRACKED files only -- the working tree carries dev junk that must not ship) The bats prefer build-glass when present and set BT_PLATFORM=glass with it, so testers boot the glass experience (PadRIO clicks, trigger config, bindings.txt keymap) with the pod build as fallback. Usage: python tools/mkdist.py [outdir] (default outdir = dist/) """ import os import re import subprocess import sys import time import zipfile REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def main(): os.chdir(REPO) outdir = sys.argv[1] if len(sys.argv) > 1 else os.path.join(REPO, "dist") os.makedirs(outdir, exist_ok=True) # Name by the repo version convention (BT411_4.11.NNN, tools/btversion.cmake: # build = commit count). Falls back to date+hash if the header is absent. name = None for h in ("build/btversion.h", "build-glass/btversion.h"): if os.path.exists(h): m = re.search(r'BT_VERSION_STRING\s+"([^"]+)"', open(h).read()) if m: name = "BT411_" + m.group(1) break if name is None: githash = subprocess.check_output( ["git", "rev-parse", "--short", "HEAD"]).decode().strip() name = "BT411-%s-%s" % (time.strftime("%Y-%m-%d"), githash) tracked = subprocess.check_output( ["git", "ls-files", "-z", "content/"]).decode().split("\0") tracked = [t for t in tracked if t] # ONE unified build (2026-07-21): every layer lives in the single exe. # THE LICENSE GATE: BT_STEAM is compile-time (Steamworks SDK terms bar a # public release with Steam inside). Read the configure's answer from # the CMake cache; a steam-OFF zip must carry neither play_steam.bat nor # steam_api.dll (a stale DLL can linger from an earlier ON build). steam_on = False try: cache = open("build/CMakeCache.txt", encoding="utf-8", errors="ignore").read() steam_on = "BT_STEAM:BOOL=ON" in cache except OSError: pass print(" BT_STEAM=%s (from build/CMakeCache.txt)" % ("ON" if steam_on else "OFF")) if not steam_on: name += "-nosteam" # distinct name: never clobbers the dev-flavor zip zpath = os.path.join(outdir, name + ".zip") exes = ["build/Release/btl4.exe"] bats = ["players/play_solo.bat", "players/join.bat", "players/join_lan.bat"] if steam_on: bats.append("players/play_steam.bat") for p in exes + bats: if not os.path.exists(p): sys.exit("MISSING: %s -- build/refresh it first" % p) # Runtime DLLs living next to each exe (OpenAL32.dll etc.) -- the exe # fails to load without them (caught by the clean-extract boot test). for exe in list(exes): d = os.path.dirname(exe) for f in sorted(os.listdir(d)): if not f.lower().endswith(".dll"): continue if f.lower() == "steam_api.dll" and not steam_on: print(" (steam OFF: leaving stale %s out of the zip)" % f) continue exes.append(d + "/" + f) # System runtime DLLs the exe needs on a bare tester machine (32-bit # d3dx9 + MSVC runtimes -- carried in redist/, shipped next to the exe; # the first zips included them and machines without the redists fail # to load the exe otherwise). redist = ["redist/" + f for f in sorted(os.listdir("redist")) if f.lower().endswith(".dll")] if os.path.isdir("redist") else [] total = 0 with zipfile.ZipFile(zpath, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z: # versioned tester README at the zip root if os.path.exists("players/README.txt"): readme = open("players/README.txt", encoding="utf-8").read() readme = readme.replace("{VERSION}", name.replace("BT411_", "")) lines = readme.splitlines(True) if steam_on: # keep steam lines, drop marker lines = [l.replace("{STEAM}", "") for l in lines] else: # steam-free zip: strip them lines = [l for l in lines if "{STEAM}" not in l] readme = "".join(lines) z.writestr(name + "/README.txt", readme) for p in bats: # bats at the zip ROOT (next to arc = os.path.basename(p) # content\ + build\, per the guard) z.write(p, name + "/" + arc) total += os.path.getsize(p) for p in exes: z.write(p, name + "/" + p) total += os.path.getsize(p) for p in redist: # next to the exe z.write(p, name + "/build/Release/" + os.path.basename(p)) total += os.path.getsize(p) for p in tracked: if not os.path.exists(p): print(" (skipping tracked-but-absent: %s)" % p) continue z.write(p, name + "/" + p) total += os.path.getsize(p) zsz = os.path.getsize(zpath) print("wrote %s" % zpath) print(" %d files, %.1f MB raw -> %.1f MB zipped" % (len(bats) + len(exes) + len(tracked), total / 1048576.0, zsz / 1048576.0)) if __name__ == "__main__": main()