Files
BT411/tools/mkdist.py
T
arcattackandClaude Opus 5 12d6862c1f keylight: unship it -- opt-in (BT_KEYLIGHT=1), gone from all player docs
Operator decision: the player base is not on Windows 11, so the RGB keyboard
mirror should not be shipped or promised.  The feature is NOT deleted -- Cyd's
implementation stays intact behind the gate for whoever wants it -- it is
simply off by default and invisible:

  * PadRIO gate flips from opt-out (BT_KEYLIGHT=0 disables) to OPT-IN
    (BT_KEYLIGHT=1 enables).  Default boots produce zero keylight lines, and a
    stub build and a real build now behave identically for players: nothing.
  * Removed from every user-facing artifact: the README paragraph, the
    CONTROLS.md section, the CONTROLS.html footer line.
  * environ.ini template: the entry stays (it is the discoverable home for the
    opt-in) but now says OFF-unless-1, Windows 11 22H2+, and why it is
    unshipped.
  * mkdist's cut-time stub warning removed -- it existed to protect a README
    promise that no longer exists.
  * context/glass-cockpit.md records the decision, the opt-in, and the
    build-time SDK stub gate in one place.

VERIFIED: default boot logs zero keylight lines; BT_KEYLIGHT=1 engages the
feature (the stub on this machine: its one honest log line + the key map).
All three console suites pass; checkctx CLEAN.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:06:00 -05:00

187 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""mkdist.py -- build the tester-distribution zip.
Layout (matches what the player bats expect):
BT411-<date>-<hash>/
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
cache = ""
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
# TESTER-BUILD EXPIRY (BT_EXPIRE, default ON): a zip cut from an
# expire-OFF build never dies in the field -- mark it loudly so it is a
# deliberate act, not an accident.
expire_on = "BT_EXPIRE:BOOL=ON" in cache if cache else True
print(" BT_EXPIRE=%s (from build/CMakeCache.txt)" % ("ON" if expire_on else "OFF"))
if not expire_on:
name += "-noexpire"
print(" WARNING: this zip NEVER EXPIRES -- intended for distribution?")
zpath = os.path.join(outdir, name + ".zip")
exes = ["build/Release/btl4.exe"]
bats = ["players/play_solo.bat", "players/join.bat", "players/join_lan.bat",
"players/joyconfig.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]
if expire_on: # same treatment for expiry note
lines = [l.replace("{EXPIRE}", "") for l in lines]
else:
lines = [l for l in lines if "{EXPIRE}" not in l]
readme = "".join(lines)
z.writestr(name + "/README.txt", readme)
# The full controls reference travels with the game, so players get the
# 72-button panel map without the repo. Flattened to ASCII so it reads
# correctly in Notepad -- the markdown source keeps its typography.
if os.path.exists("docs/CONTROLS.md"):
controls = open("docs/CONTROLS.md", encoding="utf-8").read()
for bad, good in ((u"—", "-"), (u"", "-"), (u"", "'"),
(u"", "'"), (u"“", '"'), (u"”", '"'),
(u"×", "x"), (u"→", "->"), (u"…", "..."),
(u"↑", "Up"), (u"↓", "Down"),
(u"←", "Left"), (u"→", "Right"),
(u"⚠", "!"), (u"·", "-")):
controls = controls.replace(bad, good)
controls = controls.encode("ascii", "replace").decode("ascii")
z.writestr(name + "/CONTROLS.txt", controls)
print(" controls reference -> CONTROLS.txt")
# The same map as a page, for anyone who would rather look at the
# diagrams than read them. The source is a fragment (no doctype or
# <head>), so wrap it: without a doctype the browser drops into quirks
# mode, and without a charset the typography arrives as mojibake.
if os.path.exists("docs/CONTROLS.html"):
page = open("docs/CONTROLS.html", encoding="utf-8").read()
page = ('<!doctype html>\n<html lang="en">\n<head>\n'
'<meta charset="utf-8">\n'
'<meta name="viewport" content="width=device-width, initial-scale=1">\n'
'</head>\n<body>\n' + page + '\n</body>\n</html>\n')
z.writestr(name + "/CONTROLS.html", page.encode("utf-8"))
print(" controls page -> CONTROLS.html")
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))
# Crash-forensics: archive this build's PDB next to the zip (operator-side
# only, never shipped) so the [crash] btl4+0xNNNN offsets in player logs
# stay resolvable per distributed version.
pdb = "build/Release/btl4.pdb"
if os.path.exists(pdb):
import shutil
pdb_dest = zpath[:-4] + ".pdb"
shutil.copyfile(pdb, pdb_dest)
print(" symbols archived: %s" % pdb_dest)
if __name__ == "__main__":
main()