Repository navigation
fix: security hardening passes (update offer, builder and release downloads, setup lists, shared env file, files the game reads and writes, game folders from elsewhere, HLE guest ranges) - #2430
Conversation
…te the skip file without following links hookshot_version.txt sits in the game folder, and its builder= and builder_args= lines chose the program the update offer starts and the arguments before the game's own --update and --tag. They are now taken only in the shapes the builders write: an absolute path to HookshotBuilder.exe, or to python(w).exe with "-I <path>\hookshot_builder_start.py" on Windows, an AppImage or AppRun on Linux. Anything else counts as no builder, and the offer opens the download page instead. "Skip this version" no longer writes through a user folder or skip file that is a link: lstat + O_NOFOLLOW on Linux, and on Windows a symbolic link or junction is refused (OneDrive's own reparse points are not links and still work). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Windows Python builder The Python builder for Windows followed urllib's default redirects and looked only at the last link, so an https -> http -> https chain passed; each hop must now stay on https, as the Linux updater already does, and a refused redirect is not tried again. A part stops downloading as soon as it is longer than the setup list says (it was written whole and compared afterwards), and a list or API answer stops at 8 MB. The latest-release check reads through the same opener, with the same ceiling, and only returns a tag the setup list's name rules accept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a shared hookshot_env.txt A hookshot_env.txt is passed around between players, so it may not set switches that name a file or folder. The list had drifted: RX_DUMP_IR, RX_SCREENSHOT, RX_FLASH_LOG and HOS_SCAN_DT take a folder or file prefix and were still accepted with a bare name, and RX_VK_DUMP_NATIVE wrote files into the current folder. Dump switches are now refused by family (any name with _DUMP), the four are refused by name, and tests/test_env_file.py scans host/ and gpu/ for switches whose value is used as a path and fails when the shared file could set one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the tags read from GitHub The builders took --tag as given and relied on a later mismatch with the setup list's own (checked) tag. The WPF builder's nightly lookup also returned the list's tag line unchecked, and that value went into a release API link. --tag is now checked at the start of an update in the WPF, Python and Linux builders, and every tag read from GitHub goes through the same rule ([0-9A-Za-z._+-], at most 128, not . or ..) as the Linux updater's list check, now one tag_ok function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ey come and capped scripts/ffx_sdk.py and scripts/ngx_sdk.py downloaded and copied through a fixed <name>.part, opened for writing as it was: a link left in that place (in the game folder's ffx/ or ngx/ too) had the write land on what it points to. Each download and copy now goes to a new temp file beside the target (mkstemp), is hashed while it is read, and stops once it is larger than 1 GB rather than being written whole before the SHA-256 is checked. The pinned commits and SHA-256s are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ked download The four license texts the AppImage ships were bare curl calls: not limited to https, and not hash-checked like every other download in the script. They now go through fetch with a pinned SHA-256 variable (for the LLVM, FFmpeg and dav1d versions the script pins). A test keeps any curl outside the CURL array out of the script and runs fetch over those lines with a stand-in curl. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game-free check over .github/workflows: actions pinned by full commit, top-level token permissions, checkouts without a persisted token (but the release record job's), no event data or inputs expanded inside a run: script, and no checkout in a pull_request_target workflow. Every workflow passes it as it is; the check's own cases show it catches each kind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ll local path RX_VK_DLSS_DIR is the one path a shared hookshot_env.txt may still set. A relative value (ngx, ., ../x) was taken and NGX resolved it against whatever folder the game was started from, and a .. step could lead out of the folder the value names. The file now takes only a folder named in full on this PC (C:\x on Windows, /x on Linux) with no .. step; shares and device paths stay refused. The process environment and HOS_ENV_FILE still take any value. tests/test_env_file.py checks a table of values for both systems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-hashes The AppImage's Python (scripts/linux/deps.sh) and the Windows builder's Python part (scripts/release_parts.py package --python-packages) took their pinned packages from PyPI by version only. Every other download a release makes is checked against a SHA-256. requirements-lock.txt, written by the new scripts/python_lock.py from requirements.txt's pins, lists each pin's wheels for Linux x86_64 and Windows x64 with their SHA-256, and both installers now use pip --require-hashes with it, so a wheel added to PyPI later under a pinned version is refused. The AppImage's dependency cache key and deps.sh's install mark include the lock. tests/test_python_lock.py checks the lock against the pins and that both installers use it; tests/test_release_prompt_packages.py checks the hashes reach pip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de it A game folder can come from elsewhere, and an update or a rebuild into it trusted what it found there: - .hookshot_romfs_heads (the damaged textures the extraction found) was joined to the romfs as it was, so a line with .., an absolute path or a linked folder chose a file outside the romfs to replace; - the stand-in was copied through a fixed <name>.zs.part, which could be a link to a file elsewhere; - the kept .hookshot_exefs was copied with its links followed, and the romfs links were made into an existing romfs folder that could hold a link. Each heads line now goes through the extraction's own path rule (no .., no absolute path, no link on the way, a .zs name); one that does not pass drops the list and every file is checked instead. A stand-in is copied into a new file of its own (mkstemp) and a damaged file that is a link is left alone. A link inside the kept exefs or the romfs being linked into stops the build and asks for an empty folder. New tests/test_game_folder_links.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ir name
The game rewrites hookshot_options.ini when an option changes and writes
its pipeline caches, state lists and their markers, the button-prompt
textures, mouse_sens.txt, language.txt and the moved-saves note by
itself. A game folder unpacked from an archive someone shared can hold
symbolic links (tar and unzip keep them on Linux), and a plain
fopen("w") through one emptied and rewrote the file it named.
host/hos_fopen_write.h opens these with O_NOFOLLOW, only as a regular
file (not a FIFO or a device), and empties it only once that is known.
Windows is unchanged: an archive's links are not made there without a
privilege. The program cache (gpu/vk/vk_shader.cpp) is left for later.
New tests/test_game_writes_no_links.py: the helper's behaviour, and every
write in those files goes through it but the switch-gated debugging ones.
The checks that cut those writers out of vk_draw.cpp now build them with
the helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the device draws with targets_ok let any power of two up to 64 through as a record's sample count, and the precompile handed it to the driver as the pipeline's rasterizationSamples. A shareable state list (or hookshot_pipelines.bin) with 16 or 64 on a GPU that draws with up to 8 then asked the driver for an invalid pipeline at every start. A record is now made only with a count in the device's framebuffer sample counts. tests/test_pipeline_state_counts.py gains the case; both state-list tests' stand-in device reports 1 to 8 samples. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es through a link On Linux the game moves the user/ folder it finds beside itself into $XDG_DATA_HOME/hookshot, entry by entry, at the start. When an entry could not be renamed (another file system), it was copied, and a link was followed: the files it pointed to, anywhere, were copied in among the saves, and links inside a copied folder likewise. A link by a full path is now moved as the link it is (a player's save folder kept elsewhere still works); a relative one, which would name something else from its new place, or one that cannot be moved, is left where it is with the saves staying in user/. A folder that has to be copied and holds a link is not copied: it stays too. delete_tree takes a link away without going into what it names. New tests/test_saves_move_links.py builds saves_dir over the posix layer's file functions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s size Updater.cs DownloadOnce compared the length only after the whole body was on disk; with no Content-Length it wrote whatever the server sent until it ended. It now stops as soon as the body passes GitHub's size of the file (or the answer's own length), as the Windows Python builder and the Linux updater already do. tests/updater_asset_digest_test.cs gains a case: a longer body with no Content-Length is refused before it is read to its end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st 4 GB pcache_file_read read mkw_vk_pipelines.bin (and the play caches beside it) whole into memory, then looked at its header. A large or sparse file under that name, such as another PC's, ran the game out of memory at the start. The header is now read and checked first, the rest only for this device's cache, and a file larger than 4 GB (NVIDIA's after a full preparation is about 1.1 GB) is not read, with a line in the log. New tests/test_pipeline_cache_file_read.py runs the reader with its memory held to 1 GB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a shared hookshot_env.txt A shared hookshot_env.txt may set SDL_ switches. Two kinds of them make the game write files where it was started, without end: the dummy and offscreen video drivers' *_SAVE_FRAMES (a picture each frame) and the disk audio driver (SDL_AUDIODRIVER / SDL_AUDIO_DRIVER=disk: the sound to sdlaudio.raw). The file now leaves those out, as it does the game's own dump switches; other SDL_ switches (SDL_VIDEODRIVER=x11 and the like) are taken as before. RX_VK_FRAME_TRACE, which adds a line to hookshot_frames.csv each frame, joins the switches it takes no value for. tests/test_env_file.py gains the cases; the readme says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_ switches hookshot_env.txt took every HOOKSHOT_ name without a path in its value. Most of them are the builder's (HOOKSHOT_MEM_LIMIT_GB, HOOKSHOT_JOBS, HOOKSHOT_LATEST_TAG, HOOKSHOT_KEEP_BUILD_FILES...), and the builder an update starts from the game inherits the game's environment, so a shared file could set how the update's build ran. HOOKSHOT_PREPARE_ONLY, set by --prepare-shaders, kept the game from ever playing. The file now takes only the game's own HOOKSHOT_NO_UPDATE_CHECK, HOOKSHOT_NO_AV1_NOTICE and HOOKSHOT_OCCLUSION_OFF; the process environment and HOS_ENV_FILE still set any. tests/test_env_file.py looks through the builder's sources for HOOKSHOT_ names and checks the file takes none but the game's; the readme says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot_env.txt set When the player says yes to an update, the game starts the builder, which inherits the game's environment, and with it every switch the game's shared hookshot_env.txt set. The builder reads some of the same names for the build it makes (RX_MARCH, RX_HOT_OPT, RX_VK_ONLY, RX_PGO...), so a file someone handed the player could change how the update's build ran. The game now notes the names that file set (hos_env_file_names), and starts the builder without them: on Linux through execve with an environment made before the fork, on Windows by clearing them before ShellExecuteW (and putting them back if it does not start). A HOS_ENV_FILE's switches are not noted: a test run brings those on purpose. A switch the list has no room for is not set at all, so none is set that the builder's start could not take back. tests/test_update_tracks.py checks the filter on its own and where it is used; tests/test_env_file.py checks which names are noted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An update builds into the game folder as it is, and the builder writes language.txt, hookshot_version.txt, hookshot.png, the seeded pipeline files, the shader list, prompts/ and bin/ there by name, and stages the game's files as <name>.new. A folder from elsewhere can hold a link at one of those names, and the builder wrote through it (a dangling .new link too: copy2 created what it named). Before anything is written, a link (a symbolic link, a junction or a WSL link) at one of the names it writes now stops the build and asks to remove it or choose an empty folder; links elsewhere in the folder (a cache moved to another drive) are left alone. A staged .new that is a dangling link is removed first. The check for links inside the kept exefs and the romfs (the commit before) now counts only symbolic links and junctions on Windows, not the reparse points OneDrive marks its files with: a game folder under a OneDrive-synced Documents or Desktop updates as before. tests/test_game_folder_links.py checks the refusal, and that every name make_game.py joins to the game folder is on the list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a trailing line break Updater.cs TagOk anchored its pattern with $, which in .NET also matches before a final line break, so "v2.1.0\n" passed the rule the other builders apply with a full match. The patterns for release and nightly versions had the same end. They now end in \z, and the release-candidate lookup also puts the tags it reads through TagOk. tests/updater_asset_digest_test.cs gains a tag-rule case that runs TagOk and TrackOf themselves on good and bad names (it fails on the rule before); tests/test_update_tracks.py, which compared the rule's text with Python's semantics, now requires the \z anchor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and not past 256 MB The button prompts' files in the game folder (prompts/<face>/textures.bin, ring.bin, hint.bin, External.bfotf) are a few MB, made by the builder. read_all allocated and read any file under those names whole before it looked at its magic, so a large or sparse one (a folder from elsewhere) ran the game out of memory at the start. The magic is now read first, and a file over 256 MB is not read: the game shows its own icons, as it does without the files. New tests/test_prompt_file_read.py counts what read_all asks malloc for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r junction too hos_fopen_write wrote through any name on Windows, on the grounds that an archive's links are not made there without a privilege. Archive tools do make them in Developer Mode, which many players turn on. It now refuses a name that is a symbolic link, a junction or WSL's link (by its reparse tag), and writes as before through other reparse points, which OneDrive puts on its files. The back end's object keys (nx2/build.py) now hold the host headers it includes, hos_fopen_write.h and hos_wide_size.h: a change to one of them rebuilt no vk object, so vk_draw.cpp kept the header's old code (seen here in the Windows build). tests/test_game_writes_no_links.py builds the Windows branch with a stand-in windows.h and checks each kind, and that every host header the back end includes is in its object keys; tests/test_shader_stamp_cache.py's stand-in tree has the two headers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…more host functions GP() turns a guest address into a host pointer without a check, so a size from the game walks a copy past the guest window into the host's own memory. nn::fs and the libc copy hooks were moved onto hos_guest_range before; these were not: - nn::audio AppendWaveBuffer took a wave buffer's data and ADPCM context as given, and the mixer read them for as long as the voice played (the host's memory into the sound, or a fault in the mixer); a buffer outside guest memory is now refused. AcquireVoiceSlot's coefficients likewise; - nn::os::GenerateRandomBytes wrote ARG(1) bytes from ARG(0); - nn::hid GetNpadStates wrote any count of states (now at most 16, as the console keeps) and GetSixAxisSensorStates its states unchecked; - the socket Poll stand-in cleared up to 4096 descriptors unchecked; - OutputDebugString and nn::diag's log lines copied up to 4 KB into the log from an unchecked pointer; - the event-name copies (hos_main.c) and the item key (oot_menu.c, whose own bound could wrap) wrote through pointers read from game objects. New tests/test_hle_guest_ranges.py runs wb_in_guest and checks the functions' text; the wave-buffer and input fixtures have hos_guest_range over their own guest memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y are regular files
The game reads hookshot_env.txt, hookshot_options.ini, language.txt and
mouse_sens.txt from its folder at the start. A folder unpacked from an
archive can hold a FIFO under one of those names (tar keeps them), and
fopen("r") on it waited forever, before the window opened, at every
start. New host/hos_fopen_read.h has hos_fopen_read: on Linux it opens
without waiting and takes only a regular file (a link to one is fine);
on Windows it is fopen. The reads of those files go through it, and
hos_fopen_read_binary takes the renderer's reads of its caches, notes,
state lists, preparation records and markers (gpu/vk/vk_draw.cpp) and the
button-prompt files (host/prompts.c) the same way, as do the renderer's
start-up reads of hookshot_options.ini and its skip marker
(gpu/vk/vk_main.cpp) and of the game's font files (vk_game_font.h). On
Linux, the copy that moves an old user/ folder to the saves folder reads
through it too, so a FIFO there fails the copy (the entry stays) instead
of holding the start. The helpers live in
host/hos_fopen_read.h, which needs no windows.h, so the checks that cut a
reader out of the sources still build on Windows beside their own
stand-ins; hos_fopen_write.h includes it, and the renderer's object keys
take it in.
tests/test_game_writes_no_links.py checks the helper (a FIFO and a folder
refused at once, a file and a link to one read) and that each read goes
through it; the checks that cut those readers out include the header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mouse_setup (host/nn_hid.c) reads the sensitivity kept in <user>/mouse_sens.txt, and HOS_MOUSE_SENS, and any positive number went on: "inf" or "1e999" reached lround() when the value moved into the menu's option, an undefined result kept in hookshot_options.ini. Saves can come from elsewhere. A value is now taken only when finite, and kept to the menu's 0.1-5 (10-500%). New tests/test_mouse_sens_file.py cuts mouse_setup out of the source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release_rc.sh and the setup lists' code changed on main since this branch began; the next fixes are to them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f it release_rc.sh copies the newest release candidate's AppImage and .zsync into the rc release. A file without GitHub's SHA-256 digest was downloaded and uploaded unchecked. Now every file must have a well-formed sha256 digest, checked before the tag moves or anything is uploaded; the download is then always compared with it. tests/test_release_scripts.py: a .zsync without a digest leaves the tag, the releases and the uploads as they were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The setup list a release ships names each part's checksum and link. The checksums pinned the bytes, but the links could name any https host. The Windows setup (builder/setup/setup.c), the Python builder (setup_parts.py) and the Linux updater (linux_update.py) now take a part only from this project's GitHub releases (and, for the Windows lists, llvm-mingw's, the one part that is not ours), with no ".." step, escape, query or other host in the rest of the link. A list given for testing (HOOKSHOT_SETUP_LIST), and the Linux updater's lists from this PC, take any https (or file) link as before. tests/test_setup_list_names.py checks both readers on Windows' side (the C rule compiled from setup.c); tests/test_linux_update.py the Linux one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ldown Dependabot proposed only the actions. The Python packages the release's Python carries (requirements.txt, nx2/prompt_requirements.txt) are pinned and installed with --require-hashes, so a fixed release of one was never proposed. They are now watched weekly with the actions' 7-day cooldown, in one group. A proposal changes the pins; scripts/python_lock.py writes the lock again (tests/test_python_lock.py fails on a stale lock). The builder's .NET project has no NuGet packages, so nothing is added there. tests/test_workflow_hardening.py checks the entries and their cooldowns, and asks for a NuGet entry once the builder has a package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…strides and divisors Vulkan does A note in hookshot_pipelines.bin and a record of the shareable state list carry the draw's vertex stream strides and divisors, and any number reached vkCreateGraphicsPipelines. streams_ok now refuses a stride past 2048 (Vulkan's least maxVertexInputBindingStride) or a divisor past 65535; the game's own are under 100 and 0-1. note_sane and both versions' record readers use it, as targets_ok and formats_ok. tests/test_pipeline_state_counts.py: a record with a stride of 4096 or a divisor of 65536 is left out, one at the limits kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, merges or ships them The pipeline state lists contributors add (assets/pipeline_states) are merged into the list every release ships, and the game builds each record's pipeline at the first start. pipeline_states.py checked a list's header and length only. Now add, tour and check (which CI runs) stop on a record with a value no pipeline has: counts past their arrays (targets, attributes, answers, questions), a sample count or topology Vulkan does not have, a yes/no value other than 0 or 1, non-zero padding, or a vertex stride or divisor past Vulkan's limits, saying which record and why. An xz list is unpacked only up to 64 MB: a contributed file of a few KB could unpack to gigabytes in CI. Every shipped list passes. tests/test_pipeline_state_sources.py: such records are refused by add and check, and an xz list of 128 MB is refused unread; the fixtures' records have one sample. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nvnDeviceGetProcAddress and nvnBootstrapLoader read the name the game passes in place, to its end: a pointer near the guest window's end, or a name with no end, read past the guest's memory into the host's. A new guest_name copies at most 255 characters, only from inside the window, and gives "" when there is no string there (which asks for no proc). tests/test_hle_guest_ranges.py checks both callers and runs guest_name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hos_fopen_write refused a link or a non-file at the name silently, so a setting that did not stick (hookshot_options.ini a link from an archive) left no reason. The first refusal in each source file now writes "files: <path> is a link or not a plain file: the game does not write through it" on stderr, which is the game's log; on Linux only for a link or a non-file (ELOOP, ENXIO, not S_ISREG), not for a missing folder. tests/test_game_writes_no_links.py checks the line is said once, naming the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ged-texture list stand_in_damaged_files takes the extraction's list of damaged textures (.hookshot_romfs_heads) only when each line passes nup.extraction_path, which refused any reparse point on the way: in a folder OneDrive keeps, whose files carry OneDrive's own reparse points, the list was dropped and all ~100,000 files were opened again (minutes). extraction_path takes links_only, which counts only the reparse points that are links (a symbolic link, a junction, WSL's link); the read-back uses it, and the unpack itself still refuses any. tests/test_game_folder_links.py: with OneDrive's tag the list is taken; with a symbolic link's it is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Conflicts resolved: - host/hos_update.c: "Skip this version" is kept with the saves (hos_user_dir, NotRexed#2283); write_skip writes there, still never through a link at the file's name. The saves folder itself may be a link (a player keeps it elsewhere). - host/hos_main.c: the environment goes over hookshot_env.txt first; only a name the file actually sets is noted for the builder's start, so a switch the player set in the environment stays in the builder's environment. - nx2/make_game.py: write_stamp (NotRexed#2246) with the checked damaged-texture list. - gpu/vk/vk_draw.cpp, host/nn_audio.c, host/nn_hid.c, host/prompts.c: both includes; env_on with hos_fopen_read_binary. - readme.md: the env file's rules here, without TOTK_, and main's paragraph on the environment going first. After the merge: - gpu/vk/vk_draw.cpp: main's new writes (driver faults note, predicted pipelines' marker and sample) go through hos_fopen_write. - tests: fixtures that compile code from both sides get hos_fopen_write.h, hookshot_env.h, hos_env_file_names and the skip file's new place; test_env_file.py finds HOOKSHOT_ names read with env_on too. Co-authored-by: Cursor <cursoragent@cursor.com>
… (HOS_CONTROL, HOS_CONTROL_MEM) HOS_CONTROL's value is a socket path, and a bare name passed the file's no-path rule (a socket in the current folder). HOS_CONTROL_MEM opens guest memory reads and writes over it. Both are refused by name, like the other switches that name a file; tests/test_env_file.py found HOS_CONTROL. Co-authored-by: Cursor <cursoragent@cursor.com>
…t the header's windows.h (Sleep clashed on Windows) Co-authored-by: Cursor <cursoragent@cursor.com>
hckrman101
left a comment
There was a problem hiding this comment.
Thanks for picking this up from #2326 and for the clear takeover notes. I went through the update offer, the builders' download and list checks, the env file rules, hos_fopen_write/hos_fopen_read, the saves move, the builder's folder-link checks and the HLE ranges. Mostly this does what it says. A few things below.
What holds up
- Update offer:
builder_okmatches what the three builders actually write (Runner.csProcessPath,paths.launcher_command()withlist2cmdline(["-I", start]), AppRun's$APPIMAGE/$HERE/AppRun). The env hand-off is correct on both sides: Windows_putenvupdates the process blockShellExecuteWpasses on, and Linux buildsenvpbefore the fork. - Downloads: every redirect hop is checked now, not just the final URL. The size cap applies while reading, before anything is written, and the part URL prefix plus no
../%/?/@/:closes the "any https host" gap.release_rc.shchecks every digest before the tag moves. hos_fopen_writeon Linux:O_NOFOLLOW|O_NONBLOCK, thenfstatfor a regular file, then truncate. That order closes the link and FIFO cases. The pipeline caches still go.tmpthen rename, so a link at the final name gets replaced, never written through.- HLE: the new
hos_guest_rangechecks work like the existing fs/libc ones, and the wave-buffer mixer already bounds each read byw->size, so checking[buffer, size)up front is the right span. I didn't find a check that would reject a valid call. - Every new test is game-free, and the network ones only use localhost. Each one has a
RUN_ALL_SKIPwhere it needs one.
Worth a look
- Windows "Skip this version" refuses a
userfolder that is a junction (inline). That's where Windows keeps the saves, and a junction is the usual way to move them to another drive. In that case the skip silently fails and the offer comes back every start. On Linux the takeover deliberately allows a linked saves folder. I'd keep only the file-name check on Windows too. - A related trade-off on Linux: a player who deliberately symlinks
hookshot_options.ini,mouse_sens.txtor a pipeline cache (dotfile managers, a cache on another disk) now gets no write, just one log line. It's defensible for archive-borne links, but a line in the readme's file section would save a confused report. - Overlap with open PRs. The order needs deciding:
- #2367 (portable Windows builder) records a relative
builder_portable, whichbuilder_okwould refuse. Arkex-ai already raised it on #2326 and it isn't settled yet. - #1414 makes the setup and updaters follow the repo they came from, while
part_url_ok/PART_SOURCEShardcodeNotRexed/Hookshot. Whichever lands second has to reconcile the two. - #2317 (per-user data folder) touches 22 of these files, including the paths the new write/read helpers wrap.
- #2367 (portable Windows builder) records a relative
CI: the only red job is Windows part 1/2, on test_native_astc_texture_count.py (scanf deprecated under -Werror). That's main's: #2397 fixed it, and main's source checks are green since. A merge with main should clear it. GitHub reports no conflicts right now.
Splitting: the commits are already split by area, so I wouldn't force a split. If @NotRexed would rather take it in pieces, the natural cut is (a) release/CI/lock/dependabot, (b) builders and update offer, (c) game file I/O and env file, (d) HLE ranges and pipeline record limits. Only (b) collides with #2367/#1414.
Not run on Windows yet: the update offer with a real builder, the junction check and the hos_fopen_write Windows branch. A Windows run of the update offer before merge would be good.
| /* (not through a link or a junction: the user folder or the file put there in its place would have the write | ||
| land somewhere else) */ | ||
| const DWORD ua = GetFileAttributesW(user), fa = GetFileAttributesW(file); | ||
| FILE *f = ua != INVALID_FILE_ATTRIBUTES && (ua & FILE_ATTRIBUTE_DIRECTORY) && !is_link(user, ua) && |
There was a problem hiding this comment.
!is_link(user, ua) refuses a junctioned <game>\user, which is where Windows keeps the saves and the usual way to move them to another drive. In that case the skip is silently not written and the offer comes back every start. The Linux write_skip deliberately allows a linked saves folder and only refuses a link at the file's own name. Dropping the folder check here and keeping !is_link(file, fa) would match that.
…sses Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # nx2/make_game.py
hckrman101
left a comment
There was a problem hiding this comment.
Thanks for the merge. I compared the PR's patch at 348d26c with the one at d827111 file by file: all 84 files are line-for-line the same, so this push is the main merge only.
The make_game.py resolution is right. stand_in_damaged_files keeps all of #2096's flow (the resolved-v1 checked stamp and removing an old one, the note written before the stop, the fail on unresolved files, the stamp and heads removal only after that) and wraps the PR's checks around the heads list: .zs only, nup.extraction_path(..., links_only=True) for each entry, falling back to opening every file if one doesn't pass. Nothing from either side is dropped.
Earlier points, none changed by this push:
- Windows "Skip this version" still refuses a junctioned
userfolder (!is_link(user, ua)inhost/hos_update.c, inline thread above). Still open; I'd keep only the file-name check, as on Linux. - The readme line for players who symlink
hookshot_options.ini,mouse_sens.txtor a pipeline cache on purpose isn't there yet. Small, but still open. - Overlaps: #2367, #1414 and #2317 are all still open, and the
builder_okvs relativebuilder_portablequestion with #2367 is still unsettled. Nothing to fix here on its own, but the order needs deciding with @NotRexed. - Still no Windows run of the update offer, the junction check or the
hos_fopen_writeWindows branch. The 414 local checks are a good sign, but they don't cover those.
CI: the new head's runs are only queued so far. Main now has #2397, so the Windows test_native_astc_texture_count.py failure from last time should be gone; worth a look once they finish.
Nothing new from the merge. Point 1 and a Windows run are what I'd still want before merge.
|
This now conflicts with main in |
) **What this is for:** contributors rebuilding from source should not spend another minute drawing the same button prompts. **What is measured:** on an Intel Core i9-13900K running Arch Linux, the finish stage at main `c544828f` took 67.707 and 66.898 seconds; two unchanged runs took 0.603 and 0.651 seconds, and the final head took 0.616 seconds, clearing the run-time files as well. Other work was running throughout. **Not measured:** Windows, a packaged Builder update, a complete compile/lift, or gameplay performance. Refs #2636. ## Problem `nx2/make_game.py` removes the game folder's `prompts` directory and runs `prompt_assets.py` on every build when fontTools is available. Most source edits do not change anything those assets depend on. This avoids drawing unchanged assets; it does not make the drawing itself faster. #2636's A1 plans to measure the finish stage, but its A1–A10 items do not already propose this prompt cache. It complements #2674 and #2675, which address unnecessary retranslation rather than prompt generation. ## What changed - **`nx2/make_game.py`:** source-checkout builds request incremental prompt generation. Packaged builders still regenerate every time, using the finish stage's existing launcher distinction. `RX_RELEASE` cannot distinguish them: make_game sets it for source builds too. Missing fontTools still removes the replacement prompts, keeping the existing fallback to the game's icons. - **`nx2/prompt_assets.py`:** fingerprint the inputs before generating, draw into a fresh sibling directory, and record the successful outputs only if a second fingerprint still matches. Reuse requires both matching inputs and every recorded output's size and SHA-256. **A failed generation still removes the prompts, as before**, so the game shows its own icons. An interrupted builder can leave the previous complete set without a stamp; the next build regenerates it. The drawing, encoding and asset formats are unchanged. - **`tests/test_prompt_skip.py`:** game-free fixtures run the actual finish-stage prompt call and production cache/publication functions, with small authored files and a stand-in for expensive drawing. - **`docs/development.md`:** explain reuse, its limits, and `HOOKSHOT_FORCE_PROMPTS=1`. The force switch follows `env_on`: unset, empty, `0`, `false`, `off` and `no` are off, ignoring case and surrounding spaces. ### What prevents a wrong skip The input record covers: - The four prompt source modules, including controller, keyboard and binding tables. - The game's icon font and optional bold label font, compression dictionary, common texture/layout archives, and matching resource-size table names and contents. - The repository's controller and keyboard fonts, and added, changed or removed PNG overrides. - Python/platform; fontTools, Pillow, ASTC encoder, texture decoder, NumPy, zstandard and archspec availability and versions; Pillow's native library versions and the selected ASTC backend. - Generation parameters and relevant library switches, including `SOURCE_DATE_EPOCH`, `FREETYPE_PROPERTIES` and the encoder's custom CPU definitions. Languages and saved bindings are not build inputs here: the generator uses its source tables, and the runtime applies the selected controls. The output record includes files **and directories**: the keyboard directory itself is a runtime marker. Legitimately absent optional outputs stay absent; a missing, truncated or same-size-corrupted recorded file forces regeneration. The runtime's own compressed copies and control-specific keys are excluded before hashing, not mistaken for generator output. **The files the game derives at run time are still cleared by every build**, including an unchanged hit: their own keys do not cover changes to `host/prompts.c`. Only the known runtime filenames are removed; if one cannot be cleared, the build regenerates instead of keeping on doubt. An unreadable input cannot authorize reuse. The log gives the reason, even if a forced regeneration subsequently fails. A source edited during generation leaves no stamp, addressing the stale-input certification problem discussed in [#2674's review](#2674 (comment)). ## How it was tested 2026-11-12; Arch Linux, kernel `7.2.15-1-omarchy`, x86-64, Intel Core i9-13900K, 32 logical CPUs, Python 3.12.16. Base `c544828ff`; final head `2ec89de8`. No game or GPU program was launched for these checks. ### Finish-stage measurements These execute the real finish-stage statements through completion, including binary/image copies, the unchanged shader-list check, pipeline-list work and prompt generation, in a disposable copy of an existing built game folder. Compilation, translation and shader preparation are not included. They are **not whole rebuild times**. All optional prompt packages were present: fontTools 4.68.0, Pillow 13.0.0, astc-encoder-py 0.1.12, texture2ddecoder 1.0.6, archspec 0.2.7, NumPy 2.5.4 and zstandard 0.25.0. | Run | Source | Finish time | 1-minute load before → after | Top other work | |---|---|---:|---:|---| | Before 1 | `c544828f` | 67.707 s | 7.13 → 7.40 | Existing game 316 → 315% CPU; file indexing 66 → 79%; relay check also active | | Before 2 | `c544828f` | 66.898 s | 7.06 → 8.24 | Existing game 315 → 318%; terminal process about 41% | | First generation with cache | `63d5c965` | 66.400 s | 8.24 → 8.19 | Existing game 318 → 320%; terminal process about 42% | | Unchanged 1 | `63d5c965` | 0.603 s | 8.19 → 8.19 | Existing game 320%; terminal process about 42% | | Unchanged 2 | `63d5c965` | 0.651 s | 8.19 → 8.19 | Existing game 320%; terminal process about 42% | | Changed table source | `681208dd` | 63.147 s | 6.99 → 6.95 | Existing game 320 → 321%; terminal process about 42% | | Regenerate, final head | `2ec89de8` | 65.007 s | 5.77 → 5.86 | Existing game 317 → 316%; terminal process about 43% | | Unchanged, final head, with run-time files present | `2ec89de8` | 0.616 s | 5.79 → 5.79 | Existing game 316%; terminal process about 43% | Other work was running; none was a quiet measurement. CPU percentages are lifetime averages from snapshots, not interval measurements. The first generation is included to show that this is a skip, not a faster drawing algorithm. The unchanged runs print: ```text prompts: unchanged, kept ``` Adding a harmless comment to the button-table source forces a fresh generation: ```text prompts: regenerated (table changed) ``` The comment was removed afterwards. A timestamp-only touch does not regenerate: the key checks content. An additional run on the final head verifies an unchanged hit with all six runtime-derived files present: all six are removed, while the 16 generated originals and the stamp retain their sizes and SHA-256 values. The focused fixture also compares the retained originals byte for byte. The final timing rows above include that cleanup. **Asset parity:** all 16 regenerated files, 274,704,680 bytes, compare byte for byte with a full generation by main `c544828f`. Only the new stamp is excluded. Both sides used the same `SOURCE_DATE_EPOCH` because fontTools otherwise writes the current time into the OpenType header. This comparison setting is not introduced into normal builds. A development cache hit retains the earlier font timestamp; packaged builds retain their old full-generation behavior. ### Game-free checks - **23 focused tests pass** on the final change. They cover unchanged reuse, every input kind, package versions/presence, switch/parameter changes, missing/truncated/same-size-corrupted output, empty directories, clearing runtime derivatives while retaining byte-identical originals, regeneration when runtime cleanup fails, interruption, mid-generation edits, unreadable input, force-switch parsing, incomplete output, publication failure, and the packaged/no-fontTools paths. A non-zero generator result removes prompts for both source and packaged builds; interruption before that result leaves the old set unstamped. - The same unchanged-build test against main's finish stage fails behaviorally: `2 != 1: unchanged finish must not draw the prompts twice`. - Removing each of **18 guards/key components** in memory makes its corresponding behavior test fail on the final change. Three later corrections also fail against the earlier version of this patch: returned failure retained old prompts in both build paths, runtime files survived a hit, and an uncleared runtime file did not force regeneration. The unreadable-input diagnostic has a separate before-fails/after-passes proof. - The whole suite one commit earlier, at `f20ea58b` (before a failed generation was made to remove the prompts again and the run-time files to be cleared), `python -I -B tests/run_all.py --jobs 8`: ```text 472 passed, 0 failed, 20 skipped in 133 s (linux, 8 at once) ``` Load was 7.37 → 19.48. The first full run, at 32 checks at once, was **471 passed, 1 failed, 20 skipped**: a clang++ child compiling `texture_pass.cpp` for `nx2/test_depth_sample_types.py` died with SIGKILL. That fixture and its input trees are unchanged from main. No kernel OOM line was found, so the cause is not established. The complete run with eight workers passes, including that check. The failed run is retained, not counted as a pass. On the final head `2ec89de8` the focused checks, the baseline failure and all guard mutations were rerun, and the whole suite once more with eight workers: **471 passed, 1 failed, 20 skipped in 136 s** (load 11.14 → 22.62). The one failure is `tests/test_release_scripts.py`, `test_nightly_two_runs_at_once_end_at_the_newer_commit`, the intermittent race reported in #2626; this change touches none of its files. - No core dumps were recorded. Nothing generated from the game is in this change. ## Not tested - Windows filesystem behavior or a Windows build. - The packaged Windows Builder or Linux AppImage build/update end to end. Their always-regenerate selection is covered by a game-free test only. - A full compile/lift or a new gameplay run, other CPUs, or gameplay/frame-rate benefits. No screenshots: the regenerated asset bytes match, and no visual change is intended. ## Risks The input list must grow when the generator gains new file dependencies or outputs outside the prompt directory. A regression flags new local module imports, but it does not replace reviewing new file reads. Generation temporarily needs room for both old and new assets. Publication uses completed directories and rollback on a rename error; interruption cannot create a stamped half-set, though a process killed between the two directory renames may require regeneration on the next build. ## Overlaps The open-PR scan found 47 PRs touching one of these files. The actual conflict hunks were read: - **#2151:** adjacent item-button generation conflicts with the finish-stage prompt call/error branch. Preserve both steps and account for added prompt dependencies. - **#2261:** substantive overlap. It adds `--wheel-icons`, another input folder and output outside `prompts`. Keep that work outside this skip or extend the input/output proof and staging; accepting both hunks mechanically is not enough. - #1872, #1958, #2304, #2317, #2430, #2465, #2474, #2478, #2608, #2673, #2675 and #2686 share code files but merged cleanly in the scan. That is not proof that any newly added generator dependency is covered. Assisted-by: GPT-6 Astra Co-Authored-By: GPT-6 Astra <noreply@openai.com> Co-authored-by: GPT-6 Astra <noreply@openai.com>
Takes over #2326 from @hckrman101, who asked for someone to pick it up. His 34 commits are unchanged; this adds a merge with main and three fixes on top. His description follows below the line.
Added in the takeover
tests/test_oot_menu_mouse.py(Windows only) failed to build because its stand-inhos.hhad nohos_guest_range, whichhost/oot_menu.cnow calls. The stand-in now has it.5cbfead0). Ten files conflicted. The two that needed a decision:host/hos_update.c: since fix(update): on Linux "Skip this version" is kept, and no longer costs the game its saves #2283, "Skip this version" is kept with the saves (hos_user_dir), not in<game>/user/.write_skipnow writes there and still never through a link at the file's name. The saves folder itself may be a link, because a player can keep it elsewhere.tests/test_update_skip_file.pycovers that case now.host/hos_main.c: main made the environment go overhookshot_env.txt. A name is now noted for the builder's start only when the file actually set it. Otherwise a switch the player set in their own environment would be taken from the builder.hos_fopen_write: the driver faults note, and the predicted pipelines' marker and sample ingpu/vk/vk_draw.cpp.tests/test_game_writes_no_links.pyfound them.HOS_CONTROLandHOS_CONTROL_MEMare refused in a sharedhookshot_env.txt. The control bridge (feat(control): drive a running game over a local socket, with a CLI #926) landed after this PR started. Its socket name passed the file's no-path rule as a bare name, andHOS_CONTROL_MEM=rwopens guest memory writes.tests/test_env_file.pyfound it.hos_fopen_write.h,hookshot_env.handhos_env_file_names.test_env_file.pyalso findsHOOKSHOT_names read withenv_on.Tested (takeover)
Linux:
python -I -B tests/run_all.py: 367 passed, 0 failed, 53 skipped. The skips are Vulkan/SPIR-V headers, dotnet and Windows-only tests, which are not installed here.host/hos_update.cpasses a gcc-Wall -Wextrasyntax check.Not tested (takeover)
test_oot_menu_mouse.pywas not run: there is no MinGW here. A clang syntax check ofoot_menu.cagainst the new stand-in no longer reports thehos_guest_rangeerrors.Supersedes #2326.
Some files that players run, download or pass around are still trusted more than they should be. A game folder's text file can pick the program the update offer starts. A shared
hookshot_env.txtcan still set switches that write files or change how an update builds. A game folder unpacked from an archive can hold links that the game or the builder writes through. This PR closes those gaps and adds tests so they stay closed. None of it changes what a normal player sees.What changes
Update offer and builders
Update offer (host/hos_update.c). The update offer now uses
builder=/builder_args=fromhookshot_version.txtonly when they look like what our builders write:HookshotBuilder.exe, or topython(w).exewith-I <path>\hookshot_builder_start.py;AppRun.Anything else counts as "no builder", and the offer opens the download page instead. Only the shape of the path is checked, not where it is, so a builder unpacked anywhere still works.
"Skip this version" no longer writes through a
userfolder or skip file that is a link. On Linux it uses lstat and O_NOFOLLOW. On Windows it refuses symbolic links and junctions but still allows OneDrive's own reparse points.The builder an update starts no longer gets the switches that the game's
hookshot_env.txtset. It inherits the rest of the game's environment, but the builder reads some of the same names for its build (RX_MARCH,RX_VK_ONLYand others). On Linux the builder is started withexecveand an environment built before the fork. On Windows those switches are cleared beforeShellExecuteW, and put back if the builder does not start.One tag rule for every builder. A tag may use
[0-9A-Za-z._+-], be at most 128 characters, and not be.or... The WPF, Python and Linux builders apply it to--tagand to every tag they read from GitHub. Before, the WPF builder's nightly lookup passed the list's tag line, unchecked, into a release API link. The WPF builder's patterns end in\z, because .NET's$also matches before a final line break.Windows Python builder downloads (setup_parts.py, updater.py).
Setup lists' parts (builder/setup/setup.c, setup_parts.py, builder/linux/linux_update.py). A part's link must now be under this project's GitHub releases, or llvm-mingw's for the Windows lists (the one part that is not ours). The rest of the link must have no
..step, escape, query or other host. Before, the checksums pinned each part's bytes, but a link could name any https host. A list given for testing (HOOKSHOT_SETUP_LIST) and the Linux updater's lists from this PC still take any https (or file) link.WPF builder downloads (Updater.cs). A download now stops as soon as it passes GitHub's size for the file. Before, a body with no Content-Length was written to disk in full and only then compared.
Shared
hookshot_env.txt(host/hos_main.c)_DUMPin its name is now refused as a group.RX_SCREENSHOT,RX_FLASH_LOG,HOS_SCAN_DTandRX_VK_FRAME_TRACEare refused by name.*_SAVE_FRAMESswitches (a picture each frame) and thediskaudio driver. OtherSDL_switches still work.HOOKSHOT_names: only the game's own are taken:HOOKSHOT_NO_UPDATE_CHECK,HOOKSHOT_NO_AV1_NOTICEandHOOKSHOT_OCCLUSION_OFF. The rest are the builder's.RX_VK_DLSS_DIRis still the one path the file may set, but only as a full local path (C:\...or/...) with no..step. Before, a relative value was resolved against whatever folder the game was started from.tests/test_env_file.pyscanshost/andgpu/for switches whose value is used as a path, and the builder's sources forHOOKSHOT_names. It fails if the shared file could set one of them. The readme says the same.HOS_ENV_FILEstill set anything.Files the game writes, and game folders from elsewhere
Files the game writes by itself (new host/hos_fopen_write.h). This covers
hookshot_options.ini, the pipeline caches, the state lists and their markers, the button-prompt textures,mouse_sens.txt,language.txtand the moved-saves note. A game folder unpacked from an archive can hold links, and before, the game wrote through a link at one of these names, emptying the file it pointed to.Files the game reads from its folder at start-up. These are now read only when they are regular files. Before, a FIFO under one of their names (an archive can carry one) made the game wait forever before its window opened. The files covered:
hookshot_env.txt,hookshot_options.ini,language.txtandmouse_sens.txt, through the newhos_fopen_read;hos_fopen_read_binary;This applies on Linux. On Windows the read is plain
fopen.Mouse sensitivity (host/nn_hid.c). A value from
mouse_sens.txtorHOS_MOUSE_SENSis taken only when it is a finite number, and it is kept to the menu's 10-500%. Before, a value likeinfwent into the menu's option as an undefined number.Saves moved from an old
user/folder (Linux). The game movesuser/into$XDG_DATA_HOME/hookshotand copies an entry when it cannot rename it. Moving, copying and the clean-up afterwards no longer go through a link. A link with a full path is moved as a link, so a save folder kept elsewhere still works. A relative link, a link that cannot be moved, or a folder that holds a link or a FIFO stays where it is, and the saves stay inuser/.Rebuilding or updating an existing game folder (nx2/make_game.py, nx2/nup.py).
.hookshot_romfs_headshas to name a.zsfile inside the romfs, under the same path rule the extraction uses. If any line does not, the list is dropped and every file is checked instead.<name>.part..hookshot_exefs, or inside the romfs the build hard-links into, stops the build and asks for an empty folder. On Windows only symbolic links, junctions and WSL links count, so a folder under OneDrive still updates.language.txt,hookshot_version.txt,bin,prompts, the staged.newfiles and so on) stops the build before anything is written. Links at other names, such as a cache moved to another drive, are left alone. A test checks that every name the builder writes is on the list.Pipeline cache (gpu/vk/vk_draw.cpp).
mkw_vk_pipelines.binand the play caches are now checked by their header before the rest is read, and a file over 4 GB is not read. Before, the whole file was loaded into memory first.Button-prompt files (host/prompts.c). A file is read only when its magic is right, and never past 256 MB. Before, the whole file was loaded into memory first.
Pipeline notes and state lists (gpu/vk/vk_draw.cpp). A record is used only with a sample count the GPU supports, vertex strides up to 2048 (Vulkan's least limit) and divisors up to 65535. Before, any power of two up to 64 and any stride or divisor were handed to the driver.
Contributed pipeline state lists (nx2/pipeline_states.py).
add,tourandcheck(which CI runs) now stop on a record with a value no pipeline has: a count past its array, a sample count or topology Vulkan does not have, a yes/no value other than 0 or 1, non-zero padding, or a stride or divisor past those limits. They name the record and the reason.A refused write is logged. The first time a file stops at a link or a non-file in
hos_fopen_write, it writesfiles: <path> is a link or not a plain file: the game does not write through itto the game's log. That gives a setting that does not stick a visible reason.Host functions the game calls (HLE)
GP()turns a guest address into a host pointer without checking it. Before, a size the game passed could carry a copy past the guest window into the host's own memory. These functions now usehos_guest_rangeon the whole span, as nn::fs and the libc copy hooks already do:hos_main.candoot_menu.c.nvnDeviceGetProcAddressandnvnBootstrapLoadernow copy the name the game passes, at most 255 characters and only from inside guest memory. Before, they read it in place to its end.Release and CI
--require-hashes, from a newrequirements-lock.txt.python -I scripts/python_lock.pywrites it after a pin changes.tests/test_python_lock.pyfails when the lock and the pins differ.<name>.part. Each file is hashed while it downloads, and a download stops past 1 GB. The pinned commits and SHA-256s are unchanged..zsyncinto thercrelease only when GitHub gives a SHA-256 digest for each file. This is checked before the tag moves or anything uploads. Before, a file without a digest was copied unchecked.requirements.txt,nx2/prompt_requirements.txt) weekly, with the same 7-day cooldown as the actions. After it changes a pin,scripts/python_lock.pymust write the lock again;tests/test_python_lock.pyfails until it does. The builder's .NET project has no NuGet packages, so there is no NuGet entry.fetchwith SHA-256 pins, like every other download there.persist-credentials: false(except the release record job's);${{ }}of event data or inputs inside arun:script;pull_request_targetworkflow.Tested
Linux, RTX 3090. The branch merges main@origin
ebbba2ea, which carriesscripts/release_rc.shand the setup-list code changed here; the merge had no conflicts. Testing used merges with main as it moved (e19d9693,d7a73062,ebbba2ea,49f75dc8), all without conflicts. The link and env game runs used thee19d9693merge, the FIFO run and the last game build thed7a73062merge, and the last full suite the49f75dc8merge.python -I -B tests/run_all.py). 373 passed, 1 failed, 12 skipped. The failure,tests/test_wayland_libdecor_hint.py, also fails on main on this PC.--require-hashesfor Linux x86_64 / Python 3.12 (with dependencies) and for Windows x64 / Python 3.12, 3.13 and 3.14 (as the Windows part installs them): 10 of 10 each time.host/hos_update.ccompiles with no warnings (-Wall -Wextra) under gcc (Linux) and llvm-mingw clang (Windows).hookshot_vk.dll) cross-built with llvm-mingw before the last round. After it,vk_draw.cppandvk_main.cpppass a syntax check for both Linux and Windows.host/nvn_native.c,builder/setup/setup.cand the host files that includehos_fopen_write.hpass one for both too.language.txtlink planted in the folder, the build stopped at the start and named it, and the link's target was left as it was.HOS_HEADLESS=1 HOS_NO_SDL=1, separate data folder, links planted in the game folder; final run on the branch head):hookshot_options.inior at the pipeline marker;user/was moved to the saves folder, and itsuser/savelink was moved as a link with its target left alone;RX_VK_DLSS_DIRwas left out ofhookshot_env.txt;RX_VK_FRAME_TRACE,HOOKSHOT_MEM_LIMIT_GB,HOOKSHOT_JOBSandHOOKSHOT_PREPARE_ONLYwere left out, and nosdlaudio.raworhookshot_frames.csvappeared;language.txt,hookshot_env.txtandhookshot_options.ini. On a build that had moved only the host's reads, the game hung after listing the GPUs, on the renderer's own read ofhookshot_options.ini. With every start-up read moved over, it started and ran as usual and never waited on the FIFOs.tests/test_updater_asset_digest.pybuilds the wholeUpdater.cswith its fixture: 22 of 22 cases pass. Its two new cases fail on the code before the fix.tests/test_builder_dlss_update.pypasses (57 checks).EnableWindowsTargeting=true, with no warnings.Not tested
hos_fopen_write, which was only tested with a stand-inwindows.h. The Windows host files were only syntax-checked.