Files
psxrecomp/docs/RELEASING_A_TITLE.md
Matthew Stanley 56a6cfe782 Add one shared title release command with a required channel
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:46:47 -07:00

9.0 KiB

Releasing a title

One framework command cuts every player release, for every title, on Windows and Linux. A title keeps only its release config, packaging/release.toml. Release fixes land here once instead of in a forked packager per title.

Commands

From the title repository root, with submodules checked out at their pins:

# Windows host: Windows ZIP, Linux AppImage (through WSL), or both
psxrecomp/tools/package_title_release.ps1 --channel public   --platform all
psxrecomp/tools/package_title_release.ps1 --channel playtest --platform all
# Native Linux host: the AppImage
psxrecomp/tools/package_title_release.sh --channel public
psxrecomp/tools/package_title_release.sh --channel playtest

--channel is required and has no default. Without it the command fails at once with PACKAGING FAILED. Artifacts land in dist/:

Channel Diagnostics default Windows Linux
public off <Name>-v<ver>-windows-x64.zip <Name>-v<ver>-linux-x86_64.AppImage
playtest on <Name>-v<ver>-playtest-windows-x64.zip <Name>-v<ver>-playtest-linux-x86_64.AppImage

Each artifact has a .sha256 file beside it. RELEASE_MANIFEST.json inside the package records the channel, version, title/framework/recomp-ui commits, mod list, imports or glibc, and a hash for every file. The command never tags, pushes or uploads.

Other options: --jobs N (default min(6, cores-2)), --out-dir, --work-dir (default build-package/<platform>), --toolchain-bin (Windows MinGW, default C:\msys64\mingw64\bin), and --allow-dirty. Without --allow-dirty, uncommitted tracked changes, including a submodule moved off its pin, are refused. With it they are recorded in the manifest, so that package must not be published.

verify --channel C ARTIFACT... extracts finished artifacts and re-runs every check. The command already does this after each package.

What the command does

Any failure ends with PACKAGING FAILED: <reason> and a non-zero exit code. No step warns and then continues.

  1. Provenance. Reads the title, framework and recomp-ui commits and checks for tracked changes.
  2. Emitters. Builds psxrecomp-game and psxrecomp-bios before any slow step. The overlay toolchain needs both.
  3. Sources. Regenerates the recompiled OpenBIOS if it is missing, then regenerates the game C from the disc. For AOT mode static, it also builds the static AOT units from the original disc. On Windows, --platform linux or all prepares the sources once there. The WSL build then compiles those same audited sources.
  4. Build. Release, PSX_DEBUG_TOOLS=OFF, OpenBIOS-only backend (PSXRECOMP_BIOS_STEMS=OpenBIOS), PSX_RELEASE_CHANNEL=<channel>, and PSX_GAME_VERSION from the title's version file. Every flag the command owns is read back from CMakeCache.txt. A title's cmake_args may not set any of them.
  5. Stage. Stages these items:
    • the executable and its version stamp
    • launcher assets
    • OpenBIOS and its MIT notice
    • game.toml and game_options.toml
    • the mod catalog, checked against the build's own catalog list. Developer-channel features are removed from public packages.
    • for AOT mode cache, the original-disc AOT cache with AOT_CACHE_AUDIT.json
    • the overlay toolchain, if the title ships it
    • framework, recomp-ui and third-party notices
    • the title's files
    • START_HERE.txt with @VERSION@ filled in. A template without the token is an error.
    • the execution contract
  6. Gates. Refuses the package if it contains any of these: disc images, retail BIOS images, .bin files other than bios/openbios.bin, saves, memory cards, save states, personal settings or mod state, generated or other C sources outside overlay_toolchain/, or build-kit trees. It also checks that the Playtest Diagnostics defaults match the channel, and runs tools/ci/audit_playtest_diagnostics_package.py --mode <channel>. On Windows the executable must import only system DLLs. For AOT mode static, every audited unit must match its receipt and be linked into the executable. A diagnostics package that the config does not declare is an error.
  7. Package and verify. Writes a Windows ZIP with portable / names, or builds a Linux AppImage with tools/package_appimage.sh. The AppImage uses fixed linuxdeploy 1-alpha-20251107-1 and appimagetool 1.9.1, checked by SHA-256. The command writes the .sha256 file, then extracts the artifact and verifies it.

Linux needs cmake, ninja (or make), gcc, Python 3.11+, ImageMagick, curl, file and the SDL3/OpenGL development packages. On a Windows host, the Linux half runs inside the default WSL distribution, using the same checkout through /mnt/<drive>. The build tree stays on that drive. Only the payload, AppDir and verification extraction use /tmp, and they are removed afterwards.

The backstop: no package without a channel

runtime/psx_release_channel.cmake makes PSX_RELEASE_CHANNEL (public | playtest) the only release input. PSX_PUBLIC_RELEASE is now derived from it. Configure fails in these cases:

  • a channel other than public or playtest
  • -DPSX_PUBLIC_RELEASE=ON without a channel (the old way to ask for a public build)
  • a PSX_PUBLIC_RELEASE value that contradicts the channel

A tree configured with no channel is a dev build. The packager configures its own tree with the channel, then reads it back before staging.

packaging/release.toml

Unknown keys are errors, so a typo cannot silently drop a step.

schema = 1

[product]
name = "JerseyDevilRecomp"            # artifact name prefix (and AppImage data dir token)
display_name = "Jersey Devil Recompiled"
exe_name = "JerseyDevil_Recompiled"   # staged executable; default = CMake OUTPUT_NAME
version_file = "packaging/release/VERSION"   # raw text = PSX_GAME_VERSION and @VERSION@
# game_toml = "game.toml"             # codegen config (default)
# player_game_toml = "game.toml"      # shipped config (default = game_toml)
# runtime_target = "psx-runtime"
# zip_folder = "MediEvilIIRecomp"     # optional single top-level ZIP folder
launcher_boxart = "launcher_assets/img/boxart.tga"   # staged copy must match

[build]
cmake_args = ["-DPSX_SDL_BACKEND=SDL3", "-DPSX_PGXP_VARIANT=OFF"]
# windows_cmake_args / linux_cmake_args: per-platform extras
# regenerate = true                   # psxrecomp-game --config game.toml first
# binary_identity = "{version}+{commit12}"   # bytes the executable must contain

[aot]
mode = "cache"        # cache: ship the original-disc AOT cache
                      # static: compile audited units into the exe
                      # none
profile = "aot/overlays.json"
# cps = true          # default: true for static, false for cache

[overlay_toolchain]
ship = true           # bundle the runtime overlay compiler

[diagnostics]         # required if the catalog has a *.diagnostics package
game_id = "SCUS-94907"
package_id = "jerseydevil.diagnostics"
# replay_helper = false   # title records with its own tool (no replay-diagnostics.py)

[payload]
start_here = "packaging/release/START_HERE.txt"   # must contain @VERSION@
files = ["README.md", { from = "packaging/release/input.ini", to = "input.ini" }]
# build_files = ["..."]   # extra files from beside the built executable

[linux]
app_run = "packaging/linux/AppRun"    # @VERSION@ @APP_NAME@ @EXE_NAME@ @PAYLOAD_DIR@
                                      # @ENV_PREFIX@ @ARTIFACT_NAME@ are filled in
desktop_file = "packaging/linux/io.github.mstan.JerseyDevilRecomp.desktop"
icon = "launcher_assets/img/boxart.tga"   # PNG, or converted with ImageMagick
payload_dir = "jerseydevilrecomp"         # usr/share/<payload_dir>
env_prefix = "JERSEYDEVIL_RECOMP"
# prune_unreferenced_libs = true      # drop usr/lib entries the exe does not load

# Optional title steps, run at prepare | post_build | post_stage. Tokens:
# {python} {root} {framework} {channel} {platform} {version} {build_dir}
# {work_dir}, plus {exe} (post_build) and {stage} (post_stage). A non-zero
# exit fails the release.
# [[hooks.post_stage]]
# command = ["{python}", "tools/check_my_payload.py", "{stage}"]
# platforms = ["windows", "linux"]

aot.mode = "static" checks generated/AOT_STATIC_AUDIT.json in these ways: every pair is valid, the recipe count matches expected_records, the game.toml and profile hashes match, each unit's hash matches its receipt, and each unit appears in the executable's link line.

Migrating a title

Rules for every migrated title:

  • Delete the forked packagers (tools/package_release.ps1, tools/package_appimage.sh, tools/package_release.py, prepare scripts, scripts/package_setup_release.sh) and their tests.
  • Write packaging/release.toml.
  • Put @VERSION@ in START_HERE.txt.
  • Remove any title CMake that forces PSX_PUBLIC_RELEASE.
  • Pin the latest psxrecomp and recomp-ui.

Prove the migration with both channels on both platforms, then verify the artifacts. Titles migrated so far: Jersey Devil and MediEvil II. The backlog is tracked in bead beads-eio.3.369.