Skip to content

feat(builder): hand a 2.x install over to the Hookshot Launcher - #2722

Draft
hckrman101 wants to merge 2 commits into
NotRexed:mainfrom
hckrman101:builder/launcher-handover
Draft

hckrman101 wants to merge 2 commits into
NotRexed:mainfrom
hckrman101:builder/launcher-handover

Conversation

@hckrman101

@hckrman101 hckrman101 commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Hookshot 3 replaces the Builder with a new launcher, and a player on 2.x has no way to get there from the Builder they already have. With this PR, the 2.2/2.3 Builder offers the move when a release ships the launcher: one click installs it and hands the game folder over, with the player's build settings.

This is the second of three PRs: #2733 (nightly naming), then this one, then the launcher itself (#2739). It doesn't depend on the launcher's code. The two sides agree through a written contract (docs/launcher-upgrade.md) and sample files that both test against.

What changed

The contract (docs/launcher-upgrade.md, tests/fixtures/launcher/):

  • A release that ships the launcher carries one more file, hookshot-launcher.txt, with the launcher's SHA-256, size and https link for each platform. A 404 means a Builder release, which updates as it does today. The version name is never used to decide, so the nightly renaming to nightly-<count>-<sha8> doesn't affect it. A tag is only used to find its release (release_of, the same rule the updater uses).
  • Where the launcher lives (%LOCALAPPDATA%\HookshotLauncher, ~/.local/share/hookshot/launcher), and how it is installed whole.
  • The import file dropped for the game folder, and which Builder setting fills each key on each platform.
  • What changes in the folder's hookshot_version.txt: only builder=. builder_args= goes, and a portable folder (builder_local=1) is left alone.
  • The bridge for Builders older than 2.2, including what its payload must still contain on each platform.

The upgrader (builder/HookshotBuilder/Upgrader.cs, builder/linux/linux_upgrade.py, and the two Builder windows):

  • Before an update, the Builder checks whether the target release ships the launcher. If it does, Update becomes Upgrade. A screen explains what will happen, with Upgrade and Not now, and nothing changes before Upgrade.
  • Upgrade downloads the launcher and checks its size and SHA-256 before anything is unpacked or run, then installs it:
    • Windows unpacks to app.part and swaps it in by renames. Zip entries that would land outside app.part are refused. The old app is renamed aside rather than deleted, so a file in use can't leave half a launcher behind.
    • Linux writes Hookshot.AppImage.part and renames it over the old file.
  • After installing, it writes the import file, points the folder's stamp at the launcher, starts the launcher with --launch <folder>, and closes.
  • Once the launcher is installed, the Builder passes --launch and --update on to it. A portable Windows Builder doesn't. This means the game folder's own launcher and the game's update offer keep working.
  • On Linux, host/hos_update.c prefers an installed launcher over the installed 2.x Builder.
  • The bridge: a Builder without its build tools opens straight on the hand-over screen (AppRun on Linux, Tools.Find() on Windows).
  • New command-line flag, Linux only: hookshot_builder.py --handover, used internally by AppRun. docs/versioning.md describes the upgrade.

Failure handling:

  • A failed, wrong-size, wrong-checksum or cancelled download leaves the old launcher (if there was one), the game folder and its stamp exactly as they were. No .part file is left behind.
  • On Windows, the upgrade won't start while the launcher is running. It asks the player to close it first.
  • If writing the import file or the stamp fails, the launcher is still installed and imports the Builder's settings itself on first start.

How it was tested

  • tests/test_linux_upgrade.py (30 tests) covers:
    • parsing the sample manifest, and the bad ones it must refuse
    • how a 404 is handled
    • the import file matching the sample, and the stamp rewrite
    • install: whole, wrong size or checksum, a cancel as the download ends, and a failed download that changes nothing
    • forwarding, and the AppRun bridge, run with a fake AppDir
    • the Tk Builder's begin_update, apply_mode and main, compiled from source with no Tk
    • hos_update.c's installed_launcher, compiled with cc
  • tests/test_builder_upgrade.py compiles the real Upgrader.cs, Updater.cs, BuilderSettings.cs, BuilderHome.cs and Strings.cs with a fake window (52 checks). It covers:
    • the same manifest, import, stamp and forwarding cases against the shared samples
    • install: a zip with a ../ entry, a zip without HookshotLauncher.exe, a cancelled install, and a replacement that leaves no stale files
    • a UTF-8 path
  • No test uses the network or game files.
  • python -I -B tests/run_all.py on Linux: 423 passed, 67 skipped, 3 failed.
    • test_bake_portability and test_builder_log_read_only need zstandard and libtk, which this PC doesn't have.
    • test_release_scripts hit a timing failure in its two-runs-at-once nightly case under the parallel run. It passes on its own, and this PR doesn't touch the release scripts.

What was not tested

  • Nothing ran on Windows. The WPF hand-over screen, MainWindow.xaml.cs's mode logic, the running-launcher check and the rename swap with real file locks are untested. Upgrader.cs was built and checked on Linux with the .NET 10 SDK, not on Windows with .NET 8.
  • The Tk screens never ran, because this PC lacks libtk. The Linux hand-over screen, its Cancel button and the error screen were only exercised as functions compiled from the source.
  • No real upgrade ran end to end. No release ships hookshot-launcher.txt yet, and the launcher is the next PR.

Screenshots: none. Neither the WPF nor the Tk hand-over screen could be run on this PC, so there's nothing real to show yet. I'll add them once the screens run on Windows and on a Linux setup with Tk.

Follow-ups

  • A portable Windows Builder can still offer Upgrade, which installs the launcher in %LOCALAPPDATA% rather than beside it. The folder's stamp is left alone, and the portable Builder no longer forwards to the launcher, so nothing breaks. A portable launcher home would be its own change.
  • The release workflow has to publish hookshot-launcher.txt and the bridge payloads described in section 5 of the contract. That belongs with the launcher's release.
  • The hand-over texts are English only, like the update's.

docs/launcher-upgrade.md is the contract between a 2.2/2.3 Builder that hands an
install over to the Hookshot Launcher (3.0) and the launcher that takes it: how a
launcher release is told from a Builder release (hookshot-launcher.txt, never the
version name), where the launcher is installed and how it is swapped in whole, the
import file dropped for the game folder, what changes in the folder's stamp, and the
bridge that brings Builders older than 2.2 to the same screen in one update.

tests/fixtures/launcher/ holds sample files that both sides test against, so the
upgrader and the launcher read and write the same thing.
The 2.x Builder, on Windows and Linux, now asks whether the release it would update
to ships the launcher (hookshot-launcher.txt, docs/launcher-upgrade.md). When it does,
Update becomes Upgrade: a screen says what will happen, and Upgrade downloads the
launcher, checks its size and SHA-256, installs it whole into the launcher home, drops
the import file, points the folder's stamp at it, starts it with --launch and closes.
Without this, a 2.x player has no way from the Builder to 3.0.

- An installed launcher gets --launch and --update from the Builder (not from a
  portable Windows Builder).
- Windows swaps the unpacked app in by renames, so a failure or a running launcher
  never leaves half of one; it asks to close a running launcher first.
- The bridge (section 5): a Builder without its build tools opens on the hand-over
  screen (AppRun on Linux, Tools.Find() on Windows).
- host/hos_update.c on Linux starts an installed launcher before the 2.x Builder.
- Tests: tests/test_linux_upgrade.py, tests/test_builder_upgrade.py (+ .cs fixture),
  both against tests/fixtures/launcher/; no network, no game.
@madebyjordan

Copy link
Copy Markdown
Contributor

Thanks for writing this as a contract with shared sample files. It made it easy to build the other side against. I've integrated it with the standalone launcher I've been working on for 3.0 (Avalonia, Windows and Linux), and ran both sides together. Branch: https://github.com/madebyjordan/Longshot/tree/launcher/avalonia

The launcher side now meets docs/launcher-upgrade.md:

  • HookshotLauncher.exe at the zip's root, installed in %LOCALAPPDATA%\HookshotLauncher\app (its own installer uses the same place, so a PC has one launcher whichever way it came).
  • <home>/import/*.json taken in at every start, before the window shows; on Linux, a first start with nothing dropped reads builder.ini.
  • The 2.x command line: --launch, --update <folder> [--tag] [--fetched], --play [<folder>].
  • hookshot-launcher.txt written by scripts/launcher/manifest.py and uploaded by the launcher workflow, behind a repository variable (CI_LAUNCHER_HANDOVER) so no release starts moving players until that's decided.
  • Its tests read your tests/fixtures/launcher/ files, and one also reads an import written by your own linux_upgrade.write_import.

One difference to discuss: the launcher keeps its builds in the Builder's home (HookshotBuilder, hookshot/builder), where it already installs and drives each release's builder, rather than its own builds.json. So nothing needs moving at the hand-over, but a 2.x Builder's "Free up space" could still touch those versions. I'm happy to move them into the launcher's home if you'd rather it own them.

Tested end to end

  • Linux (Ubuntu 22.04 VM): your Tk hand-over screen, reached from the game's update offer (--update <folder> --tag v3.0.0) with HOOKSHOT_LAUNCHER_LIST pointing at a freshly published launcher AppImage. Upgrade installed it, wrote the import, rewrote the stamp (builder= to the launcher, builder_args= gone), started the launcher on the folder and closed. The launcher took the import in with the builder.ini settings. As far as I know this is the first time the Tk screen has run.
  • Windows: your real Upgrader.Install, ImportFor, WriteImport and RewriteStamp on the published zip, then the installed launcher started with --launch: same result. tests/test_builder_upgrade.py passes on Windows with .NET 8 (52 of 52), which covers your note that Upgrader.cs wasn't built on Windows.

Two things I found in this PR

  • tests/test_linux_upgrade.py passes on Linux but fails 6 tests plus 1 error on Windows (path separators, file modes, CRLF, and running AppRun). It has no RUN_ALL_SKIP for Windows, so run_all.py on a Windows shard will run it.
  • The Windows Upgrader.Probe has no local-list override like Linux's HOOKSHOT_LAUNCHER_LIST, so the WPF hand-over screen can't be clicked through without a real release. Honouring the same variable on Windows would make it testable.

Overlap with #2373: this PR conflicts with #2373 in MainWindow.xaml, MainWindow.xaml.cs, builder/linux/hookshot_builder.py and tests/test_linux_update_cancel.py. In the Tk Builder both PRs use self.launcher and the message kind "launcher" for different things (your offered release, and #2373's launcher screen). In my merge I renamed yours to self.offer and "offer", put the WPF hand-over screen over #2373's window, and sent #2373's Update and release rows through it. Whichever lands second can reuse that resolution from the branch above.

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.

2 participants