Skip to content

Title-app mode: one game as its own local AppImage / dmg / portable exe - #14

Merged
TechnicallyComputers merged 7 commits into
mainfrom
feat/title-app
Sep 27, 2026
Merged

TechnicallyComputers merged 7 commits into
mainfrom
feat/title-app

Conversation

@TechnicallyComputers

@TechnicallyComputers TechnicallyComputers commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Title-app mode: retro-hub can run as one game's own app, and a packaging kit turns it into a local AppImage, .dmg or single portable .exe. The contract is n64lle docs/TITLE-APP.md; its §9 records every deviation and every verification.

Base: main. feat/direct-home (#13) and the runtime overlay (#15) landed there first. 157ff67 merges main in; CMakeLists.txt conflicted, and retro-hub now gets both hub_title.cpp and retro_overlay. After the merge, ctest passes 15/15, and the Pokemon Stadium app was rebuilt on it with every gate passing and relaunched.

The hub:

  • Where it finds the game: title/title.json beside the exe, Resources/title/ on macOS, or --title.
  • New flags:
    • --core, --package: override the bundled title's.
    • --runner: beats RETRO_CORE_RUNNER and the lookup.
    • --hub <p>: re-exec with that hub, passing along the bundled title and runner.
    • --rom.
    • --check-title: resolve everything and print it, with no window.
  • --version: direct_mode 4, title_app 1.
  • ROM: found from --rom, else the remembered path, else a file beside the app, else the home-page picker. It is checked against rom.size/rom.sha256, a mismatch is refused, and the choice is remembered.
  • State: saves, sessions, settings and mods.toml go to <id>-data/ beside the AppImage, the portable exe or the .app. The directory is probed for writability; if it isn't writable, the user data dir is used. The runner child gets RETRO_TITLE_STATE_DIR.

packaging/title/ kit:

  • It runs from any flat hub prefix, and the bare hub archive now ships it.
  • The launcher's own AppImage, .app and Windows scripts now share packaging/common/ with it.

Kit gates:

  • Payload allowlist: MANIFEST exact, no symlinks, no ROM extension, no N64 header magic.
  • Versions: hub and runner --version are read back out of the artifact.
  • Self-check: --check-title runs from a clean directory.
  • Linux imports: every import resolves (a soname shipped in the payload counts).
  • Payload untouched: title/ must be byte-identical in the artifact.

scripts/build-local.sh / .ps1:

  • Builds a flat hub prefix into out/local/<platform>/, including the kit.
  • --runtime <Retro-Runtime checkout> uses that checkout's runner.
  • The last line printed is RETRO_HUB=<path>.

Verified on linux-x86_64:

  • ctest 13/13.
  • A fake payload exercised every refusal case.
  • The real Pokemon Stadium AppImage was built through n64lle's pokemonstadium-app; every gate passed.
  • That AppImage was run from a clean folder:
    • A wrong ROM was refused, and the real one was accepted and remembered.
    • It drew the title screen, and its data landed beside the AppImage.
    • --core, --runner and --hub each took effect.

Not run: macOS (the kit's dmg path, build-app.sh after the refactor), Windows (build-title-app.ps1, build-local.ps1, package.ps1; only the portable stub ran, under wine), and the file picker used by a person.

Retro-Runtime pin: d5146fa, the pin on main.

🤖 Generated with Claude Code

…_app 1)

A port's framework stages a title payload (title.json schema 1, the core,
the game package) and the hub finds it with --title, or beside itself in
<exe_dir>/title/ (macOS also ../Resources/title/). It then opens that
title's home page instead of the library. --run-core/--core without --title
keeps Direct mode exactly as it was.

- Flags (revision 4): --title, --core (alias of --run-core, overriding the
  title's), --runner (beats RETRO_CORE_RUNNER and the lookup), --hub,
  --check-title. Every path on the command line is made absolute against
  the launch directory before anything else, in Direct mode too.
- The ROM: --rom, the remembered one, rom.file_names beside the app, then in
  <data dir>/roms/; otherwise the home page shows "Choose ROM..." (SDL3
  SDL_ShowOpenFileDialog; the callback only stores the answer, the frame
  loop takes it) and Play stays disabled. rom.size / rom.sha256 are checked;
  a mismatch is refused with expected vs got. A good ROM is remembered.
- State: <dir of the app>/<id>-data/, the app being $RETRO_HUB_APP (a --hub
  re-exec), $APPIMAGE, $RETCOMM_PORTABLE_EXE, the .app bundle, else the hub.
  Probed by writing; <user data>/<id>/ when not writable, logged. hub.paths
  point there, so config.json, sessions, saves, settings, the describe cache
  and rom.json land in it. The runtime updater is off in a title app.
- mods.toml: the n64lle selection is read from and written to
  <data dir>/mods.toml (ModScanResult::selection), and the runner child gets
  RETRO_TITLE_STATE_DIR=<data dir> (PlayArgs::env -> LaunchSpec::env).
- --check-title prints title, id, core, package, title_dir, runner,
  runner_version, rom (path | none | refused) and data_dir; exit 0 when
  title/core/package/runner resolve. It creates nothing.
- --hub <p> re-executes that hub with the same arguments minus --hub, plus
  --title and --runner, and RETRO_HUB_APP; not when <p> is this hub, and
  never twice (RETRO_HUB_REEXEC). A hub from before title-app mode is given
  the title as a Direct-mode command line when it has direct_mode >= 2 and
  a ROM resolves, else refused (exit 2).

Tests: retro-hub-title-test (title.json, anchors and the data dir incl. the
read-only fallback, ROM checks and resolution order, mods.toml in the state
dir, flat title paths). Checked by hand on Linux with the Retro-Runtime fake
package core: --check-title with/without --rom, a wrong-sha ROM refused, --hub
to a copy, to itself, under RETRO_HUB_REEXEC, and to the feat/direct-home hub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mmon/

build-title-app.sh (AppImage on Linux; .app in a .dmg on macOS) and
build-title-app.ps1 (Windows: one portable .exe) package a title payload with
retro-hub in title-app mode and a runner beside it. The kit runs from a flat
hub prefix, <hub dir>/packaging/title/, and needs nothing from this tree; the
last stdout line is `app <absolute path>`.

Gates, each failing the build: the payload allowlist (MANIFEST.txt exact --
it lists every file but itself and title.json -- no symlinks, no ROM/disc
extension, no N64 ROM header magic, and the magic scan over the whole app);
the artifact's title/ byte-identical to the payload; on Linux every library a
payload ELF imports is in the AppImage, a system library, or shipped in the
payload itself (the game package imports the generic core by soname, which
the runner has already loaded); hub and runner --version read back out of
the extracted AppImage / mounted dmg / unpacked exe; `retro-hub
--check-title` from the artifact in an empty directory with a clean
environment, data dir beside the AppImage / exe, never in the dmg. linuxdeploy
is only pointed at retro-hub and the runner, never at the payload.

Shared with the launcher's own packaging instead of copied (packaging/common/):
appimage.sh (linuxdeploy fetch/run with RETRO_HUB_LINUXDEPLOY for an offline
copy, AppImage extraction), AppRun.in (moved from linux/), macos.sh
(Info.plist, icns, DMG, moved out of build-app.sh), Info.plist.in (moved,
@name@/@BUNDLE_ID@), bundle_dylibs.sh (moved; macos/bundle_dylibs.sh forwards
to it), portable.ps1 (signing and the stub + zip + RCM1 exe, out of
package.ps1; the zip now always uses '/' entry names). The kit caches
linuxdeploy in ${XDG_CACHE_HOME:-~/.cache}/retro-hub/tools.

The portable stub recognises a title payload (title/title.json in the zip):
it unpacks into <exe dir>/<id>-data/app/ (or %LOCALAPPDATA%\<id>\app\) when the
payload's fingerprint changes, leaves RETCOMM_HOME unset, and starts the hub
in the caller's working directory; arguments are now quoted as
CommandLineToArgvW reads them back.

CMake installs the kit to share/retcomm/packaging/{title,common}; the bare hub
archive carries it at packaging/ (scripts/flat_hub_layout.sh, one file list
shared with scripts/build-local.sh) and checks it runs from the archive.

Linux: run end to end on a fake payload (Retro-Runtime's fake_pkg_core, a
random-bytes ROM); the launcher's own AppImage and the bare hub archive were
rebuilt through the refactored scripts. The stub was cross-built with mingw
and run under wine on a fake title exe. macOS (build-title-app.sh --format
dmg, build-app.sh) and Windows (build-title-app.ps1, package.ps1) are written
and parse (bash -n, shellcheck, pwsh parser) but have not been run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For a port's framework (n64lle) to run and package a title app. Builds
retro-hub and a retro-core-runner -- the submodule's, or one from
--runtime <Retro-Runtime checkout> (its scripts/build-local.sh when present,
else cmake) -- into out/local/<platform>/, laid out like the bare hub archive
(scripts/flat_hub_layout.sh) plus the runner, the title-app kit and, on
Windows, the portable stub. The runner is installed with an $ORIGIN RUNPATH
and finds the SDL3 copied beside the hub.

SDL3: found, else built from source into .cache/sdl3 (scripts/sdl3_local.sh,
now shared with packaging/linux/build-local-appimage.sh); Homebrew on macOS;
vcpkg at the CI baseline on Windows. Flags --debug --out --build --runtime
--jobs --help (and -Debug -Out -Build -Runtime -Jobs -Help). It checks the
prefix -- `title_app 1` and --package in retro-hub --version, the runner's
game_package 1, the kit runs -- and prints RETRO_HUB=<absolute path> last.
Outputs (out/, build-local*/, .cache/) are already gitignored.

Run on Linux x86_64, with the submodule's runner and with
--runtime ../Retro-Runtime-buildlocal. build-local.ps1 parses under pwsh 7.4
and has not been run on Windows; the macOS branch of build-local.sh has not
been run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/RELEASES.md: the revision-4 flags in the Direct mode table (--core
--title --runner --hub --check-title, paths made absolute), --runner first in
the runner lookup, a Title-app mode section (finding the title, title.json
schema 1, ROM resolution and refusal, the data dir beside the app and its
fallback, RETRO_TITLE_STATE_DIR and mods.toml, --check-title's lines, --hub
and older hubs), revision 4 in the revision notes, title_app in --version,
and packaging/ in the bare hub archive.

packaging/README.md: packaging/common/, and a Title apps section (usage,
outputs per OS, the MANIFEST.txt rule, the three gates, tool caching, what is
unrun). README.md: scripts/build-local.*.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pp mode

A title app launched with --core <dev core> fell back to Direct mode and
lost its bundled title. Direct mode is now only a hub with no title found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… README.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…untime overlay) landed on main

CMakeLists.txt: retro-hub gets both hub_title.cpp and retro_overlay. The
Retro-Runtime pin is main's d5146fa. build-local + ctest 15/15; the Pokemon
Stadium title app rebuilt on it (dev runner d5146fa), every kit gate passed,
and it ran from a clean directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TechnicallyComputers
TechnicallyComputers changed the base branch from feat/direct-home to main September 27, 2026 01:47
@TechnicallyComputers
TechnicallyComputers merged commit 5aab2ae into main Sep 27, 2026
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.

1 participant