From b6a5cc302c293ec3d9eb99cac25c15cfa68ab70a Mon Sep 17 00:00:00 2001 From: Michael Schienbein Date: Mon, 9 Nov 2026 12:23:30 -0600 Subject: [PATCH] Key the shaders step by the translator the build actually uses. The shaders step key hashed only the kit's bin/nx2navi_xlate.id, but the build takes the translator from the kit's bin/ or, in a source checkout, from build/xlate/ (xlate_source). From a source checkout, main() worked out the shaders key right after the compile step and stopped with a FileNotFoundError on bin/nx2navi_xlate.id before any shader was prepared. The key now hashes the identity file beside the translator xlate_source finds, so a source checkout is keyed by build/xlate/nx2navi_xlate.id and a kit's key is unchanged. A build without a translator stops with the existing message that names tools/build_xlate.py. The prompts test kit gets a translator stand-in beside its identity file, and a new test covers a kit that has the translator only under build/xlate/. Co-Authored-By: Claude Sonnet 5.5 --- docs/launcher.md | 3 ++- tests/launcher_test.py | 35 ++++++++++++++++++++++++++++++++++- tools/make_game.py | 6 ++++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/docs/launcher.md b/docs/launcher.md index 71cea9bd..ff2e2da5 100644 --- a/docs/launcher.md +++ b/docs/launcher.md @@ -305,7 +305,8 @@ pruning, unsafe archives, the background update (a release staged beside the run `staged.json` marker, a staged release installed after a restart, no install while the game runs, the install once it exited, nothing by itself with Install Updates Automatically off, a failed staging tried again after the next check), the font decoding on a system font wrapped like the game's, `make_game.py` on a synthetic -`.nup` (events, skipped steps, `--rebuild` without the dump, step keys), the RomFS step beside the lift and compile +`.nup` (events, skipped steps, `--rebuild` without the dump, step keys, the shaders key following the translator the +build uses), the RomFS step beside the lift and compile and its failure stopping the build, the prompts step with a stand-in prompt tool (when it runs again, and that it makes no other step run), the button art a release copies and the packages and tools the prompts need in a release, the compile step's object cache with the system's C compiler and its diff --git a/tests/launcher_test.py b/tests/launcher_test.py index 277d1c30..0a9f6c7c 100644 --- a/tests/launcher_test.py +++ b/tests/launcher_test.py @@ -14,7 +14,8 @@ Run: python tests/launcher_test.py (Python 3.14 for the zstd tests; the window - gamefiles: the .bfotf font wrapping and zstd dictionaries, on a font of this system made into a fake RomFS - the button art of a release: the files tools/prompt_art.py copies, and the packages and tools the prompts need - make_game.py: a synthetic .nup through the check, exefs and romfs steps with --events, skipped steps on a second - run, --rebuild without the game files, the prompts step with a stand-in prompt tool, the object cache of the compile + run, --rebuild without the game files, the shaders key following the translator the build uses (the kit's bin/ or + a source checkout's build/xlate/), the prompts step with a stand-in prompt tool, the object cache of the compile step with the system's C compiler, and its windres call under folders with spaces (a script in windres's place) - jobs: an install driven by a fake make_game.py, with and without the prompts row - the window: --selftest draws every screen (skipped without a display) @@ -1412,6 +1413,37 @@ class MakeGameTest(Tmp): steps = json.loads((self.out / 'build-cache' / 'state.json').read_text())['steps'] self.assertNotIn('romfs', steps) + def test_shaders_key_names_the_translator_the_build_uses(self): + """A source checkout has its translator in build/xlate/ (tools/build_xlate.py), not in the kit's bin/: the + shaders key follows its identity, so a rebuilt translator prepares the shaders again. A kit's bin/ comes first + when both exist, and a kit with no translator is refused with the way to build one.""" + from unittest import mock + kit = self.tmp / 'kit' + (kit / 'tools').mkdir(parents=True) + (kit / 'tools' / 'prepare_shaders.py').write_text('') + xlate = kit / 'build' / 'xlate' + xlate.mkdir(parents=True) + (xlate / self.mg.XLATE_NAME).write_bytes(b'') + (xlate / 'nx2navi_xlate.id').write_text('xlate 1\n') + + def key(): + return self.mg.step_key('shaders', 'game', self.out) + with mock.patch.object(self.mg, 'KIT', kit): + first = key() + (xlate / 'nx2navi_xlate.id').write_text('xlate 2\n') + self.assertNotEqual(key(), first) + (kit / 'bin').mkdir() + (kit / 'bin' / self.mg.XLATE_NAME).write_bytes(b'') + (kit / 'bin' / 'nx2navi_xlate.id').write_text('xlate 3\n') + from_bin = key() + (xlate / 'nx2navi_xlate.id').write_text('xlate 4\n') + self.assertEqual(key(), from_bin) + shutil.rmtree(kit / 'bin') + shutil.rmtree(xlate) + with contextlib.redirect_stderr(io.StringIO()) as err, self.assertRaises(SystemExit): + key() + self.assertIn('tools/build_xlate.py', err.getvalue()) + def test_prompts_step(self): """With button art in the kit, the last step draws the button prompts (a fake tools/pad_prompts.py), one progress event per folder. It runs again only when the art, the bind_ lines or a folder changed, and then no @@ -1420,6 +1452,7 @@ class MakeGameTest(Tmp): kit = self.tmp / 'kit' (kit / 'tools').mkdir(parents=True) (kit / 'bin').mkdir() + (kit / 'bin' / self.mg.XLATE_NAME).write_bytes(b'') # the translator the shaders key names (xlate_source) (kit / 'bin' / 'nx2navi_xlate.id').write_text('xlate 1\n') (kit / 'tools' / 'prepare_shaders.py').write_text('') for name in ('prompt_art.py', 'button_icons.py'): diff --git a/tools/make_game.py b/tools/make_game.py index 566befb2..e243e849 100644 --- a/tools/make_game.py +++ b/tools/make_game.py @@ -1543,8 +1543,10 @@ def step_key(step, identity, out=None): f'|cpu {cpu_tag()}') if step == 'compile': return compile_key - # shaders: the built game and the translator that prepared them - return f'shaders-1|{compile_key}|{sha256_file(KIT / "tools" / "prepare_shaders.py")}|{sha256_file(KIT / "bin" / "nx2navi_xlate.id")}' + # shaders: the built game and the translator that prepared them, the one xlate_source finds (the kit's bin/, or a + # source checkout's build/xlate/) + return (f'shaders-1|{compile_key}|{sha256_file(KIT / "tools" / "prepare_shaders.py")}' + f'|{sha256_file(xlate_source() / "nx2navi_xlate.id")}') def main():