Repository navigation
feat(builder): add relocatable Windows portable installations - #2367
Conversation
f1bf16c to
6485641
Compare
|
Integration note for #2373: the launcher and this opt-in portable-home package have distinct behavior but share setup/storage paths. A merge-tree check at |
6485641 to
6affe74
Compare
30b8537 to
eb85017
Compare
Joey-Magnus
left a comment
There was a problem hiding this comment.
This is a well-contained feature, and I couldn't find a regression for standard installs, which is what most players run. I focused on the native setup (portable_home.h, setup.c), the game-side association (make_game.py, host/hos_update.c) and BuilderHome/PortableArchive. For a standard install:
init_homeresolves to%LOCALAPPDATA%\HookshotBuilderas before;publish_installis the oldMoveFileW,reuse_partstill moves, andrmtree(final_dir)is unchanged;home_pathonly fails on truncation for portable homes;make_game.py's--workdefault,HOOKSHOT_SETUP_BASEor elseLOCALAPPDATA, resolves to the same folder.
On the game side, local_builder runs <game>\hookshot.exe, which is the setup launcher and not the game, because hos_game_path_w maps bin\hookshot.exe back to the game folder. The relative association is accepted only when it resolves to a marked home, so it can't point anywhere a plain builder= line couldn't already. All checks pass on this head, including Hookshot Builder package and setup.
should fix nx2/make_game.py:1469-1472 with builder/setup/portable_home.h:110-133: the association is written when the build is portable, but never removed when it isn't. If a game folder built by a portable Builder is later built again (or updated) by a standard one, hookshot_version.txt gets builder= and no builder_local=1, while the old .hookshot_builder_home stays. hb_home_game reads that file first, without looking at builder_local, so the game's launcher keeps opening the portable Builder's Play/Update window while hos_update_check hands updates to the standard one. If the portable home has since been moved or deleted, hb_home_game returns -1, g_home_ok is 0, and the launcher silently skips the Builder altogether. The simplest fix is for write_game_home(out, None) to delete an existing .hookshot_builder_home. Alternatively (or as well), hb_home_game could honour the file only when hookshot_version.txt has builder_local=1. A case for this in test_portable_game_home.py would pin it down.
question builder/HookshotBuilder/PortableArchive.cs:23: an existing versions\<tag> folder is reused whenever it has HookshotBuilder.exe and a matching tools\version.txt. That means the ZIP fallback can never repair a version that antivirus has partly removed: it validates and starts the same broken folder. I understand not overwriting a possibly running installation, and the ZIP route is only the fallback for releases without the setup's list, so its reach is small. Is the setup route (MainWindow.xaml.cs's repair through HookshotBuilder-Setup.exe) the intended way to repair in portable mode? If so, a line about it in docs/windows-portable-builder.md would help.
minor: --portable is a player-facing option, but it's only documented in docs/windows-portable-builder.md, which is linked from CONTRIBUTING.md. A sentence and a link in the readme's Windows install section would let players who want an external-SSD install find it.
Not covered by me: the WPF startup fixture and the .NET tests beyond reading them (they need Windows; CI ran them), and real removable-drive moves, which the PR says are untested. The heads-up about #2373 overlapping in setup.c, Storage.cs and MainWindow.xaml.cs is useful. Whichever lands second needs to keep BuilderHome.PassTo on the launcher's new child processes.
Share a marked installation home across setup, WPF, child builds and game updates. Preserve portable state after relocation and keep installed parts intact until staged repair publication. Assisted-by: OpenAI Codex
Prevent WPF from constructing MainWindow after a rejected portable root; observed with the published CLI executable. Assisted-by: OpenAI Codex
Publish the actual WPF app and verify relocation, child context and clean rejection of invalid homes without a game or GUI mode. Assisted-by: OpenAI Codex
Reject a denied marker write without falling back to AppData; document the verified portable folder display with a pixel-preserving crop. Assisted-by: OpenAI Codex
Check derived native paths before any file operation instead of validating only the home itself; preserve standard formatting behavior and exercise the exact MAX_PATH boundary. Assisted-by: OpenAI Codex
Compile the real home dependency in the standalone worker project, isolate its process-scoped root, and preserve startup failure diagnostics on legacy-codepage CI consoles. Assisted-by: OpenAI Codex
Publish Release and verify startup with runtime discovery isolated from installed Desktop runtimes. Assisted-by: OpenAI Codex
Exercise an owned DOS alias so runneradmin and RUNNER~1 spellings cannot produce a false failure. Assisted-by: OpenAI Codex
Assisted-by: OpenAI Codex
eb85017 to
f75c156
Compare
|
Thanks for the review. Fixed in I also linked portable setup from the readme and clarified repair: the packaged setup is the repair route; the legacy ZIP fallback reuses a matching installed version and does not audit all its files. Fresh CI is running. |
…aves, portable data folder - A link in the saves (a symbolic link or a Windows junction), or a folder holding one, is never moved: on the same drive it was renamed into the data folder, where a relative link names nothing. The saves stay with it. - When a game folder's saves cannot move (a second game folder beside the first), the data folder stays with them in <game>/user, so it keeps its own saves and settings instead of the first folder's settings. - The log says when a hookshot_options.ini or hookshot_env.txt beside the game is not read. - Rollback: what a copy left of an original is deleted before the whole copy is moved back. - A portable install (.hookshot_builder_home, NotRexed#2367) keeps its data folder in <game>/user; only its settings move into it. - The shipped pipeline notes are no longer a change of their own: make_game.py seeds cache/hookshot_pipelines.bin as main seeds the game folder's. - The Linux builder's DLSS SDK move is taken out into its own PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Joey-Magnus
left a comment
There was a problem hiding this comment.
write_game_home(out, None) removes a stale .hookshot_builder_home on a standard rebuild, and it's tested. Standard installs resolve HOOKSHOT_SETUP_BASE to the same %LOCALAPPDATA%\HookshotBuilder as before. The new deletions are portable-only and scoped: rmtree(to) touches only this attempt's staging folder, the publish_install backup is restored on failure, and the tag is checked against an allow-list before it's used in a path.
Merge order: this before #2373. They share setup.c, Storage.cs, MainWindow.xaml.cs, Updater.cs and App.xaml.cs, and #2373 then needs BuilderHome.PassTo on its --engine setup and the portable home in FreeAfterBuild.
## Summary Windows setup can accept a Builder executable and installed marker even when its private .NET runtime is missing. Incomplete installations now enter recovery, and incomplete staging stops before publication. The diagnostic keeps the affected paths and possible antivirus quarantine cause so retrying setup is actionable; the original cause in #2423 remains unconfirmed. ## Technical changes - Use one startup-file check before installed reuse, staged publication and fast launch; reject missing/empty components and directories in place of files. - Preserve the accepted portable-home paths and rollback publication from #2367. Restore the quarantine/history hint, executable path and staging folder in the failure message. - Execute the actual recovery and portable-startup boundaries with authored files, including missing-runtime refusal followed by recovery. Keep checked downloads, explicit repair and the direct-game fallback. ## Validation Head `66f5ba8d8c89ba14330f8fbf118ff0d9b7ff5f47`, rebased on main `6f4f28ac`. - 56 real-Win32 file cases pass. Three independently removed entry guards are rejected by causal regressions; 14 portable-startup scenarios pass, including a missing runtime that cannot start a process and a restored runtime that can. - The complete Windows setup compiles and links with llvm-mingw. The refreshed native diagnostic below uses the actual readiness guard and message function with fictitious paths; no installation or game is involved. - Full Windows suite: **408 / 2 / 32** versus exact base **406 / 2 / 32** (passed / failed / skipped). Failing check and case identities match: NUP link tests and the VRAM-row fixture. The earlier portable fixture integration failure was corrected before this run. This is not an all-green local suite. - Current-head hosted CI is pending. Current Linux GPU/game, complete repair on the rebased source and the reporter's installation history are not validated. This checks the directory-based WPF startup contract, not every optional assembly or installed-file checksum.  <details><summary>Earlier-head validation</summary> On `4171896d`, all applicable hosted checks passed. A disposable controlled installation with EXE/marker present and coreclr.dll missing recovered through actual downloads, verified/unpacked pinned official v2.1.0, and ran its private .NET8.0.33 managed CLI help with exit0; original source files stayed unchanged. This was not the reporter's unconfirmed missing-file cause or a full game test, and has not been repeated on the rebased head. Full suites on `dead3fcd` versus base `3e95ca68`: Windows378/3/31 versus376/3/31; Linux385/2/25 versus385/2/23, with matching failing check identities. These are historical results, not checks of the current head. </details> <!-- pr-ci-status --> | Current-head workflow | Result | |---|---| | [Game-free source regressions](https://github.com/NotRexed/Hookshot/actions/runs/43108434405) | Queued | | [Linux AppImage](https://github.com/NotRexed/Hookshot/actions/runs/43108434577) | Running | | [PR title](https://github.com/NotRexed/Hookshot/actions/runs/43108448286) | Queued | | [Windows builder](https://github.com/NotRexed/Hookshot/actions/runs/43108434381) | Queued | <!-- /pr-ci-status -->
Conflicts with NotRexed#2141, NotRexed#2160, NotRexed#2367 and NotRexed#2447, resolved for the launcher: - NotRexed#2367 (portable homes): Storage.Base is BuilderHome.Base. The launcher's hidden --engine setup gets the home (BuilderHome.PassTo), and the setup's engine mode opens it before it installs (it ran before init_home and would have stopped on every install). In engine mode a reused part is linked, in a portable home too, so the version in use stays whole; otherwise a part is moved, or copied in a portable home, as NotRexed#2367 has it. The post-build clean-up is Storage's, so it already uses the portable home. A portable home makes no shortcuts by default, as NotRexed#2367's portable install. - NotRexed#2447 (Save my pipeline list): its button was on the launch page the launcher replaces. It is a row of the menu's Settings now, on Windows and Linux, with the short of what it did under it and the whole in its tooltip. - NotRexed#2141 (NotRexed#2129, back from an update that stopped): the launcher's one way back is BackFromBuild, for Back and for a cancel alike: to the launcher of the game, which lets the update go and offers it again, else to the build's choices. test_builder_update_back.py's Windows half now checks that. - NotRexed#2160 (Cancel during a Linux update download): its checks are in the launcher's message_in, which pump hands each line to; test_linux_update_cancel.py's stand-in compiles both. The hard-linked parts (link_tree): nothing writes a part's file in place (compilers, Python, headers and libraries are read; Python's bytecode caches are written to a new file and renamed over), now said where the links are made. test_portable_setup_transaction.py also runs the engine mode's reuse: linked, in a standard and a portable home, the version in use whole. test_portable_builder_home.py's fixture compiles LaunchList.cs with Storage.cs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The worklist was behind the forum and the merges. Since v2.1.0, many fix PRs have merged, several rows already shipped, and the most-reported problems in Discord #bug-reporting had no row. This PR brings `docs/worklist.md` back in line, following the rules in the worklist and in `docs/workflow.md`. ## Method - **Forum snapshot.** All 409 #bug-reporting posts as of Nov 11, ~11:10 PM ET (Nov 7-11). Each post is dated by its thread ID, so the submit time is in ET. - **How dates were used.** - v2.1.0 was cut from `main` at about 8:32 PM ET on Nov 10 and published at 9:29 PM ET. A PR merged before the cut counts as shipped; I checked this with `git merge-base --is-ancestor <merge commit> v2.1.0`. - A report sent before a fix shipped and not repeated after it counts as fixed. - A report sent after v2.1.0, or before a fix that merged later, counts as still open or waiting for a release. - **Score.** Severity x reach x confidence. - Reach comes from workflow.md's 2026-11-10 reporter shares. Where the affected subset is unknown, it is marked as an upper bound. - Confidence is 0.7 only where the GitHub tracker has logs from several commenters; otherwise it is 0.4. - **Owners.** New rows have no owner. Existing owners are kept. ## Changes - **Added 20 rows (18 Open, 2 Fixed-waiting).** - Open: RAM/VRAM fill-up to OOM (#993, 5.6; 13 threads after v2.1.0), shader preparation stalls (3.2), AMD Windows crashes (#996, 2.9), CPU-bound fps in Castle Town and Hyrule Field (#995), invisible objects and doors (#1002), build.py stopped on v2.1.0, cutscene audio desync, shadow artifacts, coloured lighting, HDR black screen and flicker, frame generation stutter, the block grid around lights and fog, the v2.1.0 update failing, AF outlines, wrong button glyphs, Steam Deck at 6 fps, Linux AMD slower than Windows, and the FSR 4 tick not rebuilding. - Fixed-waiting: the pick-up counter above 60 fps (#2006) and RADV device lost after the intro (#2362). - **Updated 6 rows.** - The Link-jump row is now the "above a 60 fps cap" row (#1742), covering the Scarecrow's Song softlock, Gohma and the item hanging above Link's head. It goes from S2 to S1 and is re-scored from 1.2 to 3.5. - New forum links or scores on the antivirus, Intel black screen (0.11 to 0.2, #999 has logs), fairy grotto, voices (#2506) and RX 580 (#2343) rows. - **Moved 10 rows to Fixed, waiting for a release.** #2545 (ground garbling), #2451 (RTX 30 crash), #2286, #2275, #2449, #2096, #2406, #2515, #1954 and #2367. - **Deleted 8 rows** whose fix shipped in v2.1.0: #1792, #1922, #1819, #1863, #1447, #1451, #1769-#1810 and the HDR10 overlay. ## Not done - PRs listed as "candidate" fixes are matched by title and description only. None is confirmed against the reports. - Long forum threads were judged from their summaries. - `tests/run_all.py` was not run (docs-only change). Co-authored-by: Joey Magnus <joey.magnus@cxalloy.com>
198 commits behind: the portable installations (NotRexed#2367), the update guard for a folder newer than the latest release (NotRexed#2519) and the builder's update fixes since (NotRexed#2141, NotRexed#2449, NotRexed#2528) all touch the files this branch changes. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DBGH7x7hn8ibuEU45oPKo2
Eight files conflicted; both sides are kept in each. - builder/linux/hookshot_builder.py: --tag is still checked with linux_update.tag_ok before the builder starts, and main's --allow-downgrade-from is passed on as main does. - builder/setup/setup.c: this branch's part_url_ok and main's builder_ready (NotRexed#2367's startup check) sit side by side. - gpu/vk/vk_draw.cpp: main's pcache_file_read reads the 32-byte header before seeking to the end. It keeps that order and now opens the file with hos_fopen_read_binary and refuses a file past kPcacheMax (logged), as this branch did. - host/hos_main.c: main's saves move (NotRexed#2282) makes every link in user/ again in the new place, by the full path it names, and looks at every entry before moving any. That replaces this branch's own link handling in saves_dir (move a full-path link as it is, keep a relative one). copy_tree no longer fails on a link inside a folder, since copy_link now makes it again instead of following it. The file copy still goes through hos_fopen_read_binary / hos_fopen_write, so a FIFO fails the copy and nothing is written through a link. tests/test_saves_move_links.py now expects the links to be made again (a relative one by its full path) and still checks that nothing they name is copied or deleted, and that a FIFO fails the copy. tests/test_linux_saves_move.py gets hos_fopen_write.h, which copy_tree calls. - host/hos_update.c: main's builder_local (NotRexed#2367, a hookshot.exe in the game folder) is taken as main takes it. Otherwise builder= still has to pass builder_ok with builder_args=, and must be a file. The builder_args read moved up before the offer, where builder_ok needs it, so main's second read after the answer is dropped. is_link and local_builder are both kept. - nx2/nup.py: main split out package_parts, is_link and Destination. is_link takes links_only, so make_game's read-back of a OneDrive folder still counts only symlinks, junctions and WSL links, through extraction_path(..., links_only=True). LINK_TAGS is kept. - scripts/ngx_sdk.py: main's loop over RUNTIMES (DLSS and Frame Generation) copies each runtime through _part, a new temp file beside it, rather than a fixed <name>.part. - tests/test_bound_keys.py: main's longer options file, and the fixture still includes hos_fopen_read.h. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
…der_home is a folder write Checks that came with main cut code this branch changed and compile or run it with stand-ins of their own: - test_audio_biquad (nn_audio.c) and test_teleport_minimap_flag (oot_menu.c) get a hos_guest_range for their guest memory. - test_ocarina_notes and test_ocarina_note_bindings include hos_fopen_read.h, as test_bound_keys does. - test_menu_keys finds menu_init's options read by hos_fopen_read and includes its header. - test_pcache_header_read gets hos_fopen_read.h, kPcacheMax from vk_draw.cpp and a logf. - test_linux_downgrade's main() runs with linux_update, which checks --tag now. The builder writes .hookshot_builder_home in the game folder since NotRexed#2367 (a portable game's Builder). It is on FOLDER_WRITES now, so a link at that name stops the build like the others; test_game_folder_links found it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Windows Builder could not stay portable on an external SSD: setup, app storage and the game updater selected different locations, while saved absolute paths broke after a move. This adds opt-in
--portablesetup with one marked, relocatable Builder home. Standard installs retain their existing location.Fixes #2258. Claim.
Technical changes
Validation
Current tested head
f75c156f, rebased onto main355453de. A successful standard rebuild now removes the former portable association before publishing standard Builder metadata. The transition regression rejects the previous production source; it also checks repeated standard builds and denied removal preserving old metadata. All eight focused Windows fixtures pass, including native setup/transactions, archive/relocation and actual published self-contained WPF CLI startup. ZIP fallback repair limits and portable setup discovery are documented. Physical removable-drive relocation and a complete public-release/game repair remain untested. Fresh hosted checks are separate exact-head evidence.Earlier validation evidence and limits
Earlier tested head
6affe74b, rebased onto main7759a6f3:env_onfixtures now fixed on main by test: test_vcore_fs_open and test_vcore_video_acquire build again with env_on #2380; the old WPF failure was a short/long path assertion, not selection of a different home.At
64856415onb744b1ec: Windows 359 passed / 5 failed / 31 skipped; Linux 362 passed / 2 failed / 31 skipped. Both hit the two missing-env_onfixtures; Windows also retained the documented recorder negative control, NUP link-privilege and compute-guard fixture failures. These are earlier-head results, not current-head green checks.At
9e3aa287: complete native setup/host-update compilation and self-contained WPF publish passed; ten broken variants were rejected. Full Linux was 351/0/31; Windows349/3/30 had the same failure identities as its exact main baseline.Tests use authored packages/files, with no game data, downloaded SDKs or built game. Actual removable-media/drive-letter changes, live release download/repair and gameplay remain untested. #2373's new launcher shares setup/storage paths and needs the portable contract preserved whichever lands second.