mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The fifteen headers of SPIR-V words under runtime/ are no longer tracked. tools/build_shaders.py builds all of them: the GLSL with glslc, the SPIR-V assembly with spirv-as, and the two shaders assembled in Python from their scripts. build_game.py builds the missing and the stale ones before it compiles the runtime, and the build kit, the launcher release and the AppImage build them before they copy runtime/, so a player's PC still needs no shader compiler. git ignores the headers and check_content.py refuses a compiled shader in a commit. The headers built this way are byte for byte the ones removed here. vk_mvobj_fs.h is now mvobj_fs_spv.h, named like the others. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
118 lines
4.8 KiB
Python
118 lines
4.8 KiB
Python
"""Refuse files that look like game content, keys, generated game code or compiled shaders.
|
|
|
|
Runs in CI on every push and pull request, and locally as the pre-commit hook
|
|
(.githooks/pre-commit). With no arguments it checks every tracked file; with
|
|
--staged it checks the files staged for the next commit.
|
|
|
|
Standard library only, so it runs before any environment is set up.
|
|
"""
|
|
|
|
import argparse
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
MAX_BYTES = 1024 * 1024
|
|
|
|
# Switch containers, executables and game data formats.
|
|
BLOCKED_SUFFIXES = {
|
|
".xci", ".xcz", ".nsp", ".nsz", ".nca", ".ncz", ".tik", ".cert",
|
|
".nso", ".nro", ".nrr", ".npdm", ".keys",
|
|
".bfres", ".bntx", ".bnsh", ".sarc", ".szs", ".zs", ".pack", ".byml",
|
|
".bgyml", ".msbt", ".bars", ".bwav", ".bfwav", ".bfstm", ".bfttf",
|
|
".bfotf", ".sharcb", ".webm",
|
|
}
|
|
|
|
# ExeFS modules carry no extension.
|
|
BLOCKED_NAMES = {"main", "sdk", "rtld", "subsdk0", "subsdk1", "main.npdm"}
|
|
|
|
# Written by the code generator into every file it emits.
|
|
GENERATED_MARKER = "NX2NAVI_" + "GENERATED_FROM_GAME_CODE"
|
|
|
|
# A prod.keys / title.keys line: name containing key, kek or seed, then hex.
|
|
KEY_LINE = re.compile(
|
|
r"^\s*[A-Za-z0-9_]*(key|kek|seed)[A-Za-z0-9_]*\s*=\s*[0-9A-Fa-f]{32,}\s*$",
|
|
re.MULTILINE,
|
|
)
|
|
|
|
|
|
def list_files(staged: bool) -> list[Path]:
|
|
if staged:
|
|
cmd = ["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z"]
|
|
else:
|
|
# Tracked files plus untracked ones that are not ignored: everything a commit could pick up.
|
|
cmd = ["git", "ls-files", "-z", "--cached", "--others", "--exclude-standard"]
|
|
out = subprocess.run(cmd, check=True, capture_output=True).stdout
|
|
return [Path(p) for p in out.decode("utf-8").split("\0") if p]
|
|
|
|
|
|
def staged_bytes(path: Path) -> bytes | None:
|
|
"""The content of path as staged for the next commit (the index), not the working copy, which may differ."""
|
|
r = subprocess.run(["git", "cat-file", "blob", f":{path.as_posix()}"], capture_output=True)
|
|
return r.stdout if r.returncode == 0 else None
|
|
|
|
|
|
def problems_for(path: Path, allow_large: bool = False, data: bytes | None = None) -> list[str]:
|
|
"""allow_large: files over MAX_BYTES (toolchain binaries in a release folder) are checked for the generated marker
|
|
only, not refused. data: the content to check in place of the file on disk (the staged one, for --staged)."""
|
|
found = []
|
|
if path.suffix.lower() in BLOCKED_SUFFIXES:
|
|
found.append(f"blocked file type {path.suffix}")
|
|
if path.name.lower() in BLOCKED_NAMES:
|
|
found.append("name of a Switch executable module")
|
|
if data is None:
|
|
if not path.is_file():
|
|
return found
|
|
size = path.stat().st_size
|
|
else:
|
|
size = len(data)
|
|
if size > MAX_BYTES and allow_large:
|
|
if GENERATED_MARKER.encode() in (path.read_bytes() if data is None else data):
|
|
found.append("generated from game code")
|
|
return found
|
|
if size > MAX_BYTES:
|
|
found.append(f"{size // 1024} KiB, over the {MAX_BYTES // 1024} KiB limit")
|
|
return found
|
|
if data is None:
|
|
data = path.read_bytes()
|
|
if GENERATED_MARKER.encode() in data:
|
|
found.append("generated from game code")
|
|
if KEY_LINE.search(data.decode("utf-8", errors="ignore")):
|
|
found.append("contains a key in prod.keys format")
|
|
return found
|
|
|
|
|
|
def compiled_shader(path: Path) -> bool:
|
|
"""A SPIR-V binary or a header of SPIR-V words. The repository holds its shaders as source (runtime/shaders/, the
|
|
tools/make_*.py assemblers) and tools/build_shaders.py builds runtime/<name>_spv.h from them when the game is
|
|
built. A build kit or a release folder carries the headers, so make_kit.py and make_release.py, which call
|
|
problems_for, are not held to this."""
|
|
return path.suffix.lower() == ".spv" or path.name.lower().endswith("_spv.h")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
parser.add_argument("--staged", action="store_true", help="check staged files only")
|
|
args = parser.parse_args()
|
|
|
|
failed = False
|
|
for path in list_files(args.staged):
|
|
# --staged checks what the commit takes: a marker staged and then removed from the working copy alone
|
|
# passed when the file on disk was read
|
|
problems = problems_for(path, data=staged_bytes(path) if args.staged else None)
|
|
if compiled_shader(path):
|
|
problems.append("compiled shader: the repository holds the source, tools/build_shaders.py builds this")
|
|
for problem in problems:
|
|
print(f"{path}: {problem}")
|
|
failed = True
|
|
if failed:
|
|
print("\nGame content, keys, generated game code and compiled shaders never go into the repository.")
|
|
print("See CONTRIBUTING.md.")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|