Skip to content

feat(builder): add relocatable Windows portable installations - #2367

Merged
hckrman101 merged 9 commits into
NotRexed:mainfrom
Arkex-ai:feat/windows-portable-builder
Nov 11, 2026
Merged

hckrman101 merged 9 commits into
NotRexed:mainfrom
Arkex-ai:feat/windows-portable-builder

Conversation

@Arkex-ai

@Arkex-ai Arkex-ai commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

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 --portable setup with one marked, relocatable Builder home. Standard installs retain their existing location.

Fixes #2258. Claim.

Technical changes

  • Share the home across setup, WPF settings/work/SDKs, child builds and self-updates, using process-scoped context.
  • Keep same-volume paths and the game's Builder association relative. Invalid portable associations fail instead of selecting AppData.
  • Stage setup/ZIP repair publication, retain installed parts until success, and reject truncated native paths before file operations.
  • Add source-owned native, .NET, archive and published-app regressions. The startup fixture now compares directory identity, including DOS aliases, and executes the shipped self-contained Release package.

Validation

Current tested head f75c156f, rebased onto main 355453de. 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 main 7759a6f3:

  • All eight focused portable checks pass on Windows/Python 3.12, plus the worker fixture's 104 assertions.
  • Published Release app: seven startup/child/relocation/error scenarios, including an owned DOS alias, with installed-runtime discovery isolated. Restoring the old textual path comparison reproduces the hosted false failure; the rejected-home startup mutation is still caught.
  • Fresh hosted checks will run on this head. Earlier hosted failures include the missing env_on fixtures 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 64856415 on b744b1ec: Windows 359 passed / 5 failed / 31 skipped; Linux 362 passed / 2 failed / 31 skipped. Both hit the two missing-env_on fixtures; 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.

Portable folders in an isolated preview, captured at 9e3aa287 and cropped without changing pixels

Current-head workflow Result
Game-free source regressions Queued
Linux AppImage Queued
PR title Queued
Vulkan back end with NVIDIA DLSS Queued
Windows builder Queued

@Arkex-ai
Arkex-ai force-pushed the feat/windows-portable-builder branch 3 times, most recently from f1bf16c to 6485641 Compare November 11, 2026 16:50
@Arkex-ai

Copy link
Copy Markdown
Contributor Author

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 64856415 + launcher 6adbe93b reports conflicts in MainWindow.xaml.cs, Storage.cs and setup.c. Whichever lands second needs to preserve the marked home, relative game association and process-only child context, including the launcher's new --engine --tag route. I'll handle the portable rebase if the launcher lands first. No launcher UI is included here.

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_home resolves to %LOCALAPPDATA%\HookshotBuilder as before;
  • publish_install is the old MoveFileW, reuse_part still moves, and rmtree(final_dir) is unchanged;
  • home_path only fails on truncation for portable homes;
  • make_game.py's --work default, HOOKSHOT_SETUP_BASE or else LOCALAPPDATA, 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
@Arkex-ai
Arkex-ai force-pushed the feat/windows-portable-builder branch from eb85017 to f75c156 Compare November 11, 2026 20:22
@Arkex-ai

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Fixed in f75c156f on main 355453de: a successful standard rebuild removes the old portable association before writing standard Builder metadata. The new transition case fails against the previous source and passes now, including a second standard build and denied removal preserving the old metadata. All eight focused Windows fixtures pass again.

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.

LNDF added a commit to LNDF/Hookshot that referenced this pull request Nov 11, 2026
…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 Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@hckrman101
hckrman101 merged commit 5a3fcb9 into NotRexed:main Nov 11, 2026
hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
## 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.

![Controlled incomplete-runtime
diagnostic](https://raw.githubusercontent.com/Arkex-ai/Hookshot/66f5ba8d8c89ba14330f8fbf118ff0d9b7ff5f47/docs/images/setup-runtime-incomplete.png)

<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 -->
madebyjordan added a commit to madebyjordan/Longshot that referenced this pull request Nov 11, 2026
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>
hckrman101 pushed a commit that referenced this pull request Nov 12, 2026
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>
Fanatrick pushed a commit to Fanatrick/Hookshot that referenced this pull request Nov 12, 2026
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
Joey-Magnus added a commit to Joey-Magnus/Hookshot that referenced this pull request Nov 12, 2026
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>
Joey-Magnus added a commit to Joey-Magnus/Hookshot that referenced this pull request Nov 12, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

How do I make Hookshot portable?

3 participants