From d7581dbecee951141aec91a3ec1453a4aa9b5d48 Mon Sep 17 00:00:00 2001 From: Andrew Quenehen Date: Mon, 3 Aug 2026 09:04:25 -0300 Subject: [PATCH] Ship Switch releases as an SD-ready zip only. Players extract one zip at the microSD root for install and update; saves under pokemon-love2d/ survive merge. Drop the bare .nro from GitHub Release assets. Co-authored-by: Cursor --- .github/workflows/release.yml | 19 ++-- README.md | 6 +- docs/switch-build.md | 19 ++-- docs/switch-development.md | 11 +- docs/switch-hardware-evidence.md | 2 +- docs/switch-install.md | 46 +++++---- docs/switch-transfer.md | 13 ++- scripts/build_switch.sh | 15 ++- scripts/switch/pack_sd_zip.sh | 128 ++++++++++++++++++++++++ scripts/switch/selftest_build_switch.sh | 106 +++++++++++++++++++- tests/switch_ci_workflows_test.lua | 24 +++++ 11 files changed, 328 insertions(+), 61 deletions(-) create mode 100755 scripts/switch/pack_sd_zip.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7658c4e..8568f37f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,7 @@ name: Release # Builds the macOS, Windows, and Linux desktop apps, an Android APK, an iOS -# IPA, a Nintendo Switch fused NRO (experimental), and the Anbernic RG34XXSP +# IPA, a Nintendo Switch SD-ready zip (experimental), and the Anbernic RG34XXSP # (Stock OS 64-bit MOD / PortMaster) port on the self-hosted Mac runner, and # publishes them as a GitHub Release. # @@ -270,13 +270,11 @@ jobs: [ -f "$ipa" ] || { echo "::error::$ipa not found (expected from scripts/build_ios.sh --device)"; exit 1; } cp "$ipa" "$outdir/gen1recomp-${v}-ios.ipa" - nro="dist/switch/gen1recomp-${v}-switch.nro" - [ -f "$nro" ] || { echo "::error::$nro not found (expected from scripts/build_switch.sh --fused)"; exit 1; } - cp "$nro" "$outdir/gen1recomp-${v}-switch.nro" - # Sidecar written by build_fused.sh when the fused NRO succeeds. - if [ -f "${nro}.sha256" ]; then - cp "${nro}.sha256" "$outdir/gen1recomp-${v}-switch.nro.sha256" - fi + swzip="dist/switch/gen1recomp-${v}-switch.zip" + [ -f "$swzip" ] || { echo "::error::$swzip not found (expected from scripts/build_switch.sh --fused → pack_sd_zip.sh)"; exit 1; } + cp "$swzip" "$outdir/gen1recomp-${v}-switch.zip" + # Local fused .nro stays under dist/switch/ for PR CI / debug; release + # publishes the SD-ready zip only. # Anbernic handheld port (suffix names the CFW it targets, so a # future RG35XX/other-CFW pack can ship alongside it). @@ -395,14 +393,11 @@ jobs: "dist/release/gen1recomp-${v}-linux.zip" "dist/release/gen1recomp-${v}-android.apk" "dist/release/gen1recomp-${v}-ios.ipa" - "dist/release/gen1recomp-${v}-switch.nro" + "dist/release/gen1recomp-${v}-switch.zip" "dist/release/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip" "dist/release/gen1recomp-${v}.love" "dist/release/sha256sums.txt" ) - if [ -f "dist/release/gen1recomp-${v}-switch.nro.sha256" ]; then - release_files+=("dist/release/gen1recomp-${v}-switch.nro.sha256") - fi gh release create "$tag" \ --target "$GITHUB_SHA" \ diff --git a/README.md b/README.md index 0a8483aa..c1d4d08e 100644 --- a/README.md +++ b/README.md @@ -231,15 +231,15 @@ Install steps, controls, and troubleshooting live in ## Nintendo Switch -Releases ship a fused `gen1recomp-*-switch.nro` (issue +Releases ship an SD-ready `gen1recomp-*-switch.zip` (issue [#531](https://github.com/bryanthaboi/gen1recomp/issues/531)). Runtime target is pinned [love-nx](https://github.com/retronx-team/love-nx) `11.5-nx1`. Requires a console that can run Switch homebrew. Hardware evidence: **OLED** (author) and **V1 / Erista** boot (community). - Players: [docs/switch-install.md](docs/switch-install.md) — download the - NRO, copy to the SD, title-override launch, import your own legal ROM, - Joy-Con controls and shortcuts. + zip, extract at the microSD root (install or update), title-override + launch, import your own legal ROM, Joy-Con controls and shortcuts. - Builders: [docs/switch-build.md](docs/switch-build.md) — `--fetch` / `--loose` / `--fused`, toolchain, Docker fallback, and **CI vs release** (path-gated ubuntu selftest, canonical fused PR artifact, release hard-fail). diff --git a/docs/switch-build.md b/docs/switch-build.md index 3a2e3806..63ddfb26 100644 --- a/docs/switch-build.md +++ b/docs/switch-build.md @@ -1,14 +1,14 @@ # Build the Nintendo Switch NRO — contributor guide -Want to play a release build instead? Download the fused NRO and copy it to -your console — see [switch-install.md](switch-install.md). +Want to play a release build instead? Download the SD-ready zip and extract it +at your microSD root — see [switch-install.md](switch-install.md). This guide is for contributors who build Gen1Recomp for Switch from source. Hardware evidence, MTP operator loops, and deeper notes live in [switch-development.md](switch-development.md). -> Releases ship a fused `gen1recomp-*-switch.nro` (issue -> [#531](https://github.com/bryanthaboi/gen1recomp/issues/531)). Hardware +> Releases ship `gen1recomp-*-switch.zip` (SD tree under `switch/gen1recomp/`; +> issue [#531](https://github.com/bryanthaboi/gen1recomp/issues/531)). Hardware > evidence: **OLED** (author) and **V1 boot** (community). See > [switch-development.md](switch-development.md) for known limitations. @@ -64,7 +64,7 @@ builds can fall back to the pinned image when native tools are missing. | ---- | ------------ | | `--fetch` | Downloads pinned **love.nro** + **love.elf** into `.bazinga/love-nx/11.5-nx1/` and verifies SHA-256 against `scripts/switch/love-nx-11.5-nx1.sha256`. | | `--loose` | Packs `game.love`, copies pinned `love.nro` → `dist/switch/loose/` as `gen1recomp.nro` + `game.love` side by side. Needs the pin. | -| `--fused` | Builds a single `dist/switch/gen1recomp--switch.nro` (game in romfs) via `nacptool` + `elf2nro`. Needs the pin + toolchain (native or Docker). | +| `--fused` | Builds `dist/switch/gen1recomp--switch.nro` (game in romfs) via `nacptool` + `elf2nro`, then packs `dist/switch/gen1recomp--switch.zip` (SD-ready tree). Needs the pin + toolchain (native or Docker). GitHub Releases publish the **zip only**. | Rules: @@ -109,12 +109,13 @@ scripts/build_switch.sh --fetch # Loose pair for iteration (fetch + assemble) scripts/build_switch.sh --fetch --loose -# Single fused NRO for a release-like artifact +# Single fused NRO + SD-ready zip for a release-like artifact scripts/build_switch.sh --fetch --fused --version 0.2.0 ``` Outputs land under `dist/switch/` (and `dist/switch/loose/` for loose mode). -The fused path also writes `gen1recomp--switch.nro.sha256`. +The fused path also writes `gen1recomp--switch.nro.sha256` and +`gen1recomp--switch.zip` (+ `.sha256` sidecar for the zip). Offline packaging smoke (no network, no nacptool required): @@ -164,7 +165,9 @@ other platforms — this is a **hard gate** (no `continue-on-error`): scripts/build_switch.sh --fetch --fused --version "" ``` -A Switch packaging failure fails the entire release job. +A Switch packaging failure fails the entire release job. The release asset is +`gen1recomp--switch.zip` (SD-ready); the versioned `.nro` stays under +`dist/switch/` for the packer and for PR CI artifacts. ### Runner provisioning diff --git a/docs/switch-development.md b/docs/switch-development.md index bc22723a..10525801 100644 --- a/docs/switch-development.md +++ b/docs/switch-development.md @@ -1,8 +1,9 @@ # Nintendo Switch development (love-nx) > Fused NRO support for issue [#531](https://github.com/bryanthaboi/gen1recomp/issues/531). -> Releases ship `gen1recomp-*-switch.nro`. Console copy is manual; title override -> required. See [Known limitations](#known-limitations-read-before-reviewing). +> Releases ship `gen1recomp-*-switch.zip` (SD-ready tree). Console copy is +> extract/merge at microSD root; title override required. See +> [Known limitations](#known-limitations-read-before-reviewing). **Canonical install / build / transfer docs** (start here unless you need hardware depth): @@ -27,9 +28,9 @@ the transfer runbook). | ---- | ----- | | Feature | **Available** — playable fused NRO path (issue #531) | | Runtime | Pinned love-nx **`11.5-nx1`** | -| Product artifact | Single fused `gen1recomp.nro` (game in romfs); loose `nro`+`game.love` for iteration | +| Product artifact | Releases: SD-ready `gen1recomp-*-switch.zip`; local/PR: fused `.nro`; loose `nro`+`game.love` for iteration | | Hardware | **OLED** validated (author, title override); **V1 / Erista** boot confirmed (community). Lite, docked soak, and Pro Controller matrices welcome | -| Deploy / install | Releases publish fused NRO; **console copy is manual** (MTP / SD / FTP — [switch-transfer.md](switch-transfer.md)); no `nxlink` path yet | +| Deploy / install | Releases publish SD-ready zip; **extract/merge at microSD root** (MTP / SD / FTP — [switch-transfer.md](switch-transfer.md)); no `nxlink` path yet | | Contributor transfer | Documented for **macOS, Linux, and Windows**; OpenMTP on Mac is one example, not the only contract | | Network features on NX | Self-update / remote mod download **disabled** (`networkValidated == false`) | | Community help | Welcome — especially HOS / love-nx packaging and broader hardware coverage | @@ -165,7 +166,7 @@ Detail for **macOS / Linux / Windows** and **MTP / SD / FTP** lives in | Layer | Intent | | ----- | ------ | -| **Runtime / players** | Put the NRO under `sdmc:/switch/gen1recomp/` and land ROMs/mods under the save-dir inboxes. The game does not hard-depend on OpenMTP or macOS. | +| **Runtime / players** | Extract the release zip at microSD root (`switch/gen1recomp/`) and land ROMs/mods under the save-dir inboxes. The game does not hard-depend on OpenMTP or macOS. | | **Contributor loop** | Manual copy via MTP (DBI responder), direct SD (Hekate UMS / reader), or FTP. Fully manual — no CI deploy, no `nxlink` yet. | The Mac + OpenMTP steps that remain below are the **OLED evidence reproduction** path; prefer the transfer runbook for day-to-day contrib on other hosts. diff --git a/docs/switch-hardware-evidence.md b/docs/switch-hardware-evidence.md index 80b125a4..ad0a6ba7 100644 --- a/docs/switch-hardware-evidence.md +++ b/docs/switch-hardware-evidence.md @@ -127,7 +127,7 @@ Operator smoke for the switch-build-pipeline packaging CLI (closes matrix-deferr | love-nx | `11.5-nx1` (manifest checksums match) | | Artifact | `dist/switch/gen1recomp-0.0.0-test-switch.nro` | | NRO SHA-256 | `210efb884a8d27443dc1c64ed8f071b0f862d8d0c9b140ad8185093c4e4027db` | -| Install doc | `docs/switch-install.md` — copy NRO under `sdmc:/switch/gen1recomp/` | +| Install doc | `docs/switch-install.md` — at the time of this row: copy NRO under `sdmc:/switch/gen1recomp/` (releases now ship an SD-ready zip; same folder) | | Console | Switch OLED | | Operator | Andrew | | Date | 2026-08-01 | diff --git a/docs/switch-install.md b/docs/switch-install.md index 7e2ba99a..d9b32f35 100644 --- a/docs/switch-install.md +++ b/docs/switch-install.md @@ -1,8 +1,9 @@ # Install Gen1Recomp on Nintendo Switch -Every GitHub Release that includes Switch support ships a fused homebrew -binary: `gen1recomp-*-switch.nro`. Copy it to your microSD, launch with -**title override**, then import your own legal `.gb` ROM. +Every GitHub Release that includes Switch support ships an SD-ready zip: +`gen1recomp-*-switch.zip`. Extract it at the root of your microSD (install +**or** update — same steps), launch with **title override**, then import your +own legal `.gb` ROM. > You need a console that can run Switch homebrew (custom firmware / hbmenu). > This project does not help you set that up. Tracks issue @@ -16,30 +17,36 @@ Prefer building from source? See [switch-build.md](switch-build.md). Port by [andrewqsantos](https://github.com/andrewqsantos). Community testing help from [booshankles](https://github.com/booshankles). -## 1. Download the NRO +## 1. Download the zip 1. Open [Releases](https://github.com/bryanthaboi/gen1recomp/releases). -2. Download `gen1recomp-*-switch.nro` for the version you want. - (Optional: the matching `*.nro.sha256` sidecar if you want to verify the - download.) +2. Download `gen1recomp-*-switch.zip` for the version you want. + (Optional: verify against `sha256sums.txt` in the same release.) -## 2. Copy it to the microSD +## 2. Extract onto the microSD -Put the file here on the SD card: +Extract the zip at the **root** of the microSD so you get: ```text sdmc:/switch/gen1recomp/gen1recomp.nro +sdmc:/switch/gen1recomp/pokemon-love2d/imports/ +sdmc:/switch/gen1recomp/pokemon-love2d/imports/mods/ +sdmc:/switch/gen1recomp/pokemon-love2d/imports/saves/... ``` -(or keep the versioned name under `sdmc:/switch/gen1recomp/` — hbmenu will -list it either way). - -Any method that lands the file on the SD is fine: **MTP** (DBI → Run MTP +Merge folders if your OS asks. Any method works: **MTP** (DBI → Run MTP responder + a client), **direct SD** (Hekate UMS or a card reader), or **FTP**. Exit MTP / unmount / stop FTP cleanly before launching. Step-by-step for macOS, Linux, and Windows: [switch-transfer.md](switch-transfer.md). +### Updating + +Use the **same** extract/merge. It replaces `gen1recomp.nro` (and the small +help `README.txt` / `INSTALL.txt` files). Saves, imported ROMs, mods, and +options live under `pokemon-love2d/` — **do not delete that folder** when +updating, or you will lose progress. + ## 3. Launch with title override **Applet Mode is not supported** for this game (not enough memory). @@ -55,15 +62,12 @@ Do **not** launch from the Album applet path for normal play. This project ships **no** game data. On first launch: -1. Put your own legally obtained Pokémon Red or Blue `.gb` into the ROM - inbox under the game’s save directory (`imports/` — the launcher shows - the live path). +1. Put your own legally obtained Pokémon Red or Blue `.gb` into + `switch/gen1recomp/pokemon-love2d/imports/` (the launcher also shows + the live save-dir path). 2. Use **Scan again** on the Red/Blue tab if you add the file after the first open. -Saves live in the LÖVE save directory and **persist across NRO updates** — -you can replace only the `.nro` and keep your progress. - ## 5. Import / Export a raw `.sav` Continue a cart or PC battery save (or pull a slot off-console) via MTP / @@ -75,6 +79,8 @@ SD / FTP — same transfer methods as ROMs. Paths are **per game**: | Blue | `imports/saves/blue/` | `exports/blue/` | | Yellow | `imports/saves/yellow/` | `exports/yellow/` | +(Under the save dir `pokemon-love2d/` — the zip already creates these folders.) + 1. Copy a Gen1 `.sav` (32 KB) into that game’s inbox under the save dir ([switch-transfer.md](switch-transfer.md)). 2. With the game’s ROM already imported, open **that game’s tab** → @@ -164,7 +170,7 @@ and ## Prefer building it yourself? -Building the fused (or loose) NRO from source is covered in +Building the fused NRO (and SD-ready zip) from source is covered in [switch-build.md](switch-build.md). Copying artifacts and inbox files (MTP / SD / FTP on macOS, Linux, Windows): [switch-transfer.md](switch-transfer.md). Status, limitations, and how we tested: [switch-development.md](switch-development.md). diff --git a/docs/switch-transfer.md b/docs/switch-transfer.md index 938a4f49..f42ad4c6 100644 --- a/docs/switch-transfer.md +++ b/docs/switch-transfer.md @@ -19,7 +19,7 @@ Player install (what to download, title override) stays in | What | Where on the console | | ---- | -------------------- | -| Fused release NRO | `sdmc:/switch/gen1recomp/gen1recomp.nro` (or versioned name under that folder) | +| SD-ready release zip | Extract at microSD **root** → `sdmc:/switch/gen1recomp/gen1recomp.nro` plus `pokemon-love2d/` inbox folders. Install and update use the same merge; do **not** delete `pokemon-love2d/` | | Loose iteration pair | `sdmc:/switch/gen1recomp/gen1recomp.nro` **and** `game.love` beside it | | ROM inbox | LÖVE save dir → `imports/` (launcher shows the live `getSaveDirectory()` path; under MTP often `1: SD Card//imports/`) | | Mod zip inbox | Same save dir → `imports/mods/` then MODS → **Scan again** | @@ -28,7 +28,8 @@ Player install (what to download, title override) stays in | Opt-in diagnostics | Empty `switch-debug.txt` in the save dir → `switch.log` | | Lua error log | `lua-error.log` in the save dir | -Saves persist across **NRO-only** replacements. Never commit ROM dumps, `.sav` +Saves persist across zip re-extract / NRO replacements as long as +`pokemon-love2d/` is left in place. Never commit ROM dumps, `.sav` files, or third-party mod zips to git. --- @@ -51,7 +52,8 @@ hardware evidence — **one contributor example**, not a Mac-only product rule. 1. Quit other MTP clients. 2. Open OpenMTP → select the DBI device → **`1: SD Card`**. -3. Create `switch/gen1recomp/` if needed; copy NRO (and `game.love` for loose). +3. Create `switch/gen1recomp/` if needed; extract the release zip at SD root + (or copy NRO / `game.love` for loose). 4. For ROMs/mods/saves, open the save-dir `imports/`, `imports/mods/`, `imports/saves//`, or `exports//` path the launcher prints. @@ -68,7 +70,8 @@ fails to open. desktops, or your distro’s KDE MTP stack). 2. With DBI MTP active, open **Files** / **Dolphin** / **Thunar** and select the Switch / DBI device → **`1: SD Card`**. -3. Copy into `switch/gen1recomp/` and the save-dir inboxes as above. +3. Extract the release zip at SD root (merge), or copy into `switch/gen1recomp/` + and the save-dir inboxes as above. 4. Use **only one** MTP accessor at a time. If `mtp-tools` / `mtpfs` reports “device is busy”, close the file manager’s MTP mount (or the CLI mount) and retry with a single client. @@ -81,7 +84,7 @@ card reader) or **FTP** instead — same destinations in the table above. 1. With DBI MTP active, open **This PC** / **File Explorer** and look under **Portable Devices** for the Switch / DBI MTP volume → **`1: SD Card`**. -2. Copy files into `switch\gen1recomp\` and the save-dir inboxes. +2. Copy / extract into `switch\gen1recomp\` and the save-dir inboxes. 3. Optional: [OpenMTP](https://github.com/ganeshrvel/openmtp) on Windows if Explorer is flaky. 4. If Windows does not show an MTP device: Device Manager → find DBI / Switch diff --git a/scripts/build_switch.sh b/scripts/build_switch.sh index 91dbdf58..9bb2544f 100755 --- a/scripts/build_switch.sh +++ b/scripts/build_switch.sh @@ -19,10 +19,13 @@ # (gen1recomp.nro + game.love side by side). Requires the pin # (run --fetch first, or combine --fetch --loose). # -# --fused Build a single gen1recomp--switch.nro via nacptool+elf2nro. -# Uses native tools (PATH or $DEVKITPRO/tools/bin) first; else -# Docker from scripts/switch/dkp-docker.image (override -# GEN1_DKP_IMAGE). Requires the pin (run --fetch or combine). +# --fused Build a single gen1recomp--switch.nro via nacptool+elf2nro, +# then pack dist/switch/gen1recomp--switch.zip (SD-ready +# tree under switch/gen1recomp/). Uses native tools (PATH or +# $DEVKITPRO/tools/bin) first; else Docker from +# scripts/switch/dkp-docker.image (override GEN1_DKP_IMAGE). +# Requires the pin (run --fetch or combine). GitHub Releases +# publish the zip only; the versioned .nro stays local / PR CI. # # Combinable: --fetch alone, or --fetch with --loose / --fused. # XOR: --loose and --fused cannot be used together. @@ -46,7 +49,7 @@ say() { printf '\033[1;32m==>\033[0m %s\n' "$*" >&2; } fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } usage() { - sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//' + sed -n '2,34p' "$0" | sed 's/^# \{0,1\}//' } while [ $# -gt 0 ]; do @@ -128,7 +131,9 @@ fi if [ "$FUSED" -eq 1 ]; then GAME_LOVE="$(pack_game_love)" OUT_NRO="$DIST/gen1recomp-${VERSION}-switch.nro" + OUT_ZIP="$DIST/gen1recomp-${VERSION}-switch.zip" "$ROOT/scripts/switch/build_fused.sh" "$GAME_LOVE" "$VERSION" "$OUT_NRO" + "$ROOT/scripts/switch/pack_sd_zip.sh" "$OUT_NRO" "$VERSION" "$OUT_ZIP" cp "$WORK/build-info.json" "$DIST/gen1recomp-${VERSION}-build-info.json" say "done. See $DIST/" exit 0 diff --git a/scripts/switch/pack_sd_zip.sh b/scripts/switch/pack_sd_zip.sh new file mode 100755 index 00000000..8f76cac0 --- /dev/null +++ b/scripts/switch/pack_sd_zip.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Pack a Switch SD-ready zip: extract at microSD root (merge-safe update). +# +# Usage: +# scripts/switch/pack_sd_zip.sh NRO_PATH VERSION OUT_ZIP +# +# Layout inside the zip (SD root): +# switch/gen1recomp/gen1recomp.nro +# switch/gen1recomp/INSTALL.txt +# switch/gen1recomp/pokemon-love2d/imports/.../README.txt +# switch/gen1recomp/pokemon-love2d/exports/.../README.txt +# +# Does not ship ROMs, saves, or mods. Re-extracting merges over an existing +# install and only overwrites the NRO + these text placeholders — keep +# pokemon-love2d/ to preserve progress. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=common.sh +. "$SCRIPT_DIR/common.sh" + +NRO_PATH="${1:-}" +VERSION="${2:-}" +OUT_ZIP="${3:-}" + +[ -n "$NRO_PATH" ] && [ -n "$VERSION" ] && [ -n "$OUT_ZIP" ] \ + || fail "usage: scripts/switch/pack_sd_zip.sh NRO_PATH VERSION OUT_ZIP" + +[ -f "$NRO_PATH" ] || fail "missing NRO at $NRO_PATH" + +command -v zip >/dev/null 2>&1 || fail "need zip on PATH" + +# Absolutize before any cd — relative OUT_ZIP would otherwise land inside the +# staging dir and vanish when the EXIT trap cleans up. +NRO_PATH="$(cd "$(dirname "$NRO_PATH")" && pwd)/$(basename "$NRO_PATH")" +OUT_DIR="$(dirname "$OUT_ZIP")" +mkdir -p "$OUT_DIR" +OUT_ZIP="$(cd "$OUT_DIR" && pwd)/$(basename "$OUT_ZIP")" + +STAGE="$(mktemp -d "${TMPDIR:-/tmp}/gen1recomp-sd-zip.XXXXXX")" +cleanup() { rm -rf "$STAGE"; } +trap cleanup EXIT + +APP_DIR="$STAGE/switch/gen1recomp" +SAVE_ROOT="$APP_DIR/pokemon-love2d" +mkdir -p "$APP_DIR" +cp "$NRO_PATH" "$APP_DIR/gen1recomp.nro" + +cat > "$APP_DIR/INSTALL.txt" < "$path" +} + +write_readme "$SAVE_ROOT/imports/README.txt" \ + "Put a legal Pokemon Red or Blue .gb / .gbc here, then Scan again in the launcher." +write_readme "$SAVE_ROOT/imports/mods/README.txt" \ + "Put community mod .zip files here, then MODS → Scan again." +write_readme "$SAVE_ROOT/imports/saves/red/README.txt" \ + "Put a Red .sav (32 KB) here, then Red tab → SAVE FILES → Import save." +write_readme "$SAVE_ROOT/imports/saves/blue/README.txt" \ + "Put a Blue .sav (32 KB) here, then Blue tab → SAVE FILES → Import save." +write_readme "$SAVE_ROOT/imports/saves/yellow/README.txt" \ + "Put a Yellow .sav (32 KB) here, then Yellow tab → SAVE FILES → Import save." +write_readme "$SAVE_ROOT/exports/red/README.txt" \ + "After Export save (Red), copy the .sav out of this folder via MTP / SD / FTP." +write_readme "$SAVE_ROOT/exports/blue/README.txt" \ + "After Export save (Blue), copy the .sav out of this folder via MTP / SD / FTP." +write_readme "$SAVE_ROOT/exports/yellow/README.txt" \ + "After Export save (Yellow), copy the .sav out of this folder via MTP / SD / FTP." + +rm -f "$OUT_ZIP" +( + cd "$STAGE" + zip -q -r "$OUT_ZIP" switch +) + +[ -f "$OUT_ZIP" ] || fail "zip was not created at $OUT_ZIP" +[ -s "$OUT_ZIP" ] || fail "zip is empty: $OUT_ZIP" + +LISTING="$(unzip -Z1 "$OUT_ZIP" 2>/dev/null || unzip -l "$OUT_ZIP")" +printf '%s\n' "$LISTING" | grep -q 'switch/gen1recomp/gen1recomp.nro' \ + || fail "zip missing switch/gen1recomp/gen1recomp.nro" + +REQUIRED=( + "switch/gen1recomp/INSTALL.txt" + "switch/gen1recomp/pokemon-love2d/imports/README.txt" + "switch/gen1recomp/pokemon-love2d/imports/mods/README.txt" + "switch/gen1recomp/pokemon-love2d/imports/saves/red/README.txt" + "switch/gen1recomp/pokemon-love2d/imports/saves/blue/README.txt" + "switch/gen1recomp/pokemon-love2d/imports/saves/yellow/README.txt" + "switch/gen1recomp/pokemon-love2d/exports/red/README.txt" + "switch/gen1recomp/pokemon-love2d/exports/blue/README.txt" + "switch/gen1recomp/pokemon-love2d/exports/yellow/README.txt" +) +for rel in "${REQUIRED[@]}"; do + printf '%s\n' "$LISTING" | grep -Fq "$rel" || fail "zip missing $rel" +done + +ZIP_SHA="$(sha256_file "$OUT_ZIP")" +printf '%s %s\n' "$ZIP_SHA" "$OUT_ZIP" > "${OUT_ZIP}.sha256" +say "SD-ready zip: $OUT_ZIP" +printf '%s %s\n' "$ZIP_SHA" "$OUT_ZIP" diff --git a/scripts/switch/selftest_build_switch.sh b/scripts/switch/selftest_build_switch.sh index 76e0702b..7377c5d9 100755 --- a/scripts/switch/selftest_build_switch.sh +++ b/scripts/switch/selftest_build_switch.sh @@ -4,8 +4,8 @@ # Usage: scripts/switch/selftest_build_switch.sh # # Covers: sha256_file, --help glossary, XOR loose/fused, fail_need_fetch, -# verify_love_nx mismatch, fail_fused_toolchain message. Does not download -# love-nx or invoke nacptool/elf2nro/Docker. +# verify_love_nx mismatch, fail_fused_toolchain message, pack_sd_zip layout. +# Does not download love-nx or invoke nacptool/elf2nro/Docker. set -euo pipefail @@ -209,6 +209,108 @@ else bad "fail_fused_toolchain missing OS hints:$OS_MISSING" fi +# --------------------------------------------------------------------------- +# 6. pack_sd_zip.sh builds SD-ready tree (offline; fake NRO) +# --------------------------------------------------------------------------- +FAKE_NRO="$STAGING/fake.nro" +printf 'fake-nro-bytes\n' > "$FAKE_NRO" +FAKE_ZIP="$STAGING/gen1recomp-0.0.0-test-switch.zip" +PACK_RC=0 +"$ROOT/scripts/switch/pack_sd_zip.sh" "$FAKE_NRO" "0.0.0-test" "$FAKE_ZIP" \ + >"$STAGING/pack.out" 2>"$STAGING/pack.err" || PACK_RC=$? +if [ "$PACK_RC" -eq 0 ] && [ -f "$FAKE_ZIP" ] && [ -s "$FAKE_ZIP" ]; then + ok "pack_sd_zip.sh writes a non-empty zip" +else + bad "pack_sd_zip.sh failed (rc=$PACK_RC err=$(cat "$STAGING/pack.err"))" +fi + +ZIP_LIST="$(unzip -Z1 "$FAKE_ZIP" 2>/dev/null || unzip -l "$FAKE_ZIP")" +PACK_MISSING="" +for rel in \ + "switch/gen1recomp/gen1recomp.nro" \ + "switch/gen1recomp/INSTALL.txt" \ + "switch/gen1recomp/pokemon-love2d/imports/README.txt" \ + "switch/gen1recomp/pokemon-love2d/imports/mods/README.txt" \ + "switch/gen1recomp/pokemon-love2d/imports/saves/red/README.txt" \ + "switch/gen1recomp/pokemon-love2d/imports/saves/blue/README.txt" \ + "switch/gen1recomp/pokemon-love2d/imports/saves/yellow/README.txt" \ + "switch/gen1recomp/pokemon-love2d/exports/red/README.txt" \ + "switch/gen1recomp/pokemon-love2d/exports/blue/README.txt" \ + "switch/gen1recomp/pokemon-love2d/exports/yellow/README.txt" +do + printf '%s\n' "$ZIP_LIST" | grep -Fq "$rel" || PACK_MISSING="${PACK_MISSING} ${rel}" +done +if [ -z "$PACK_MISSING" ]; then + ok "pack_sd_zip.sh zip contains SD tree + inbox READMEs" +else + bad "pack_sd_zip.sh zip missing:$PACK_MISSING" +fi + +EXTRACT_DIR="$STAGING/extract-v1" +rm -rf "$EXTRACT_DIR" +mkdir -p "$EXTRACT_DIR" +unzip -q "$FAKE_ZIP" -d "$EXTRACT_DIR" +if cmp -s "$FAKE_NRO" "$EXTRACT_DIR/switch/gen1recomp/gen1recomp.nro"; then + ok "pack_sd_zip.sh NRO bytes match source" +else + bad "pack_sd_zip.sh NRO inside zip differs from source" +fi + +if [ -f "${FAKE_ZIP}.sha256" ]; then + ok "pack_sd_zip.sh writes .sha256 sidecar" +else + bad "pack_sd_zip.sh should write ${FAKE_ZIP}.sha256" +fi + +MISSING_NRO_RC=0 +"$ROOT/scripts/switch/pack_sd_zip.sh" "$STAGING/does-not-exist.nro" "0.0.0" \ + "$STAGING/should-fail.zip" >/dev/null 2>&1 || MISSING_NRO_RC=$? +if [ "$MISSING_NRO_RC" -ne 0 ]; then + ok "pack_sd_zip.sh fails when NRO is missing" +else + bad "pack_sd_zip.sh should fail on missing NRO" +fi + +# Relative OUT_ZIP must survive (absolutized before cd into staging) +REL_DIR="$STAGING/rel-out" +mkdir -p "$REL_DIR" +REL_RC=0 +( + cd "$REL_DIR" + "$ROOT/scripts/switch/pack_sd_zip.sh" "$FAKE_NRO" "0.0.1" "relative.zip" \ + >"$STAGING/rel.out" 2>"$STAGING/rel.err" +) || REL_RC=$? +if [ "$REL_RC" -eq 0 ] && [ -f "$REL_DIR/relative.zip" ] && [ -s "$REL_DIR/relative.zip" ]; then + ok "pack_sd_zip.sh accepts relative OUT_ZIP" +else + bad "pack_sd_zip.sh relative OUT_ZIP failed (rc=$REL_RC err=$(cat "$STAGING/rel.err"))" +fi + +# Merge-safe update: second extract replaces NRO, keeps user data +printf 'KEEP-SAVE' > "$EXTRACT_DIR/switch/gen1recomp/pokemon-love2d/slot.sav" +printf 'KEEP-ROM' > "$EXTRACT_DIR/switch/gen1recomp/pokemon-love2d/imports/red.gb" +printf 'KEEP-MOD' > "$EXTRACT_DIR/switch/gen1recomp/pokemon-love2d/imports/mods/mod.zip" +printf 'KEEP-OPTS' > "$EXTRACT_DIR/switch/gen1recomp/pokemon-love2d/options.lua" + +FAKE_NRO2="$STAGING/fake-v2.nro" +printf 'fake-nro-bytes-v2\n' > "$FAKE_NRO2" +FAKE_ZIP2="$STAGING/gen1recomp-0.0.1-test-switch.zip" +"$ROOT/scripts/switch/pack_sd_zip.sh" "$FAKE_NRO2" "0.0.1-test" "$FAKE_ZIP2" \ + >"$STAGING/pack2.out" 2>"$STAGING/pack2.err" +unzip -qo "$FAKE_ZIP2" -d "$EXTRACT_DIR" + +MERGE_OK=1 +cmp -s "$FAKE_NRO2" "$EXTRACT_DIR/switch/gen1recomp/gen1recomp.nro" || MERGE_OK=0 +[ "$(cat "$EXTRACT_DIR/switch/gen1recomp/pokemon-love2d/slot.sav")" = "KEEP-SAVE" ] || MERGE_OK=0 +[ "$(cat "$EXTRACT_DIR/switch/gen1recomp/pokemon-love2d/imports/red.gb")" = "KEEP-ROM" ] || MERGE_OK=0 +[ "$(cat "$EXTRACT_DIR/switch/gen1recomp/pokemon-love2d/imports/mods/mod.zip")" = "KEEP-MOD" ] || MERGE_OK=0 +[ "$(cat "$EXTRACT_DIR/switch/gen1recomp/pokemon-love2d/options.lua")" = "KEEP-OPTS" ] || MERGE_OK=0 +if [ "$MERGE_OK" -eq 1 ]; then + ok "pack_sd_zip.sh merge update preserves user data" +else + bad "pack_sd_zip.sh merge update lost user data or failed to replace NRO" +fi + # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- diff --git a/tests/switch_ci_workflows_test.lua b/tests/switch_ci_workflows_test.lua index 4e274e29..51ca554f 100644 --- a/tests/switch_ci_workflows_test.lua +++ b/tests/switch_ci_workflows_test.lua @@ -172,4 +172,28 @@ do mustContain(block, "Hard-fail", "release Build Switch comment") end +-- Release publishes SD-ready zip only (no bare .nro / .nro.sha256 assets) +do + local start = release:find("- name: Stage release assets", 1, true) + check(start ~= nil, "release Stage release assets present") + local rest = release:sub(start) + local nextStep = rest:find("\n - name:", 2) + local block = nextStep and rest:sub(1, nextStep - 1) or rest + mustContain(block, "gen1recomp-${v}-switch.zip", "release Stage Switch zip") + mustContain(block, "pack_sd_zip.sh", "release Stage cites pack_sd_zip") + mustNotContain(block, "gen1recomp-${v}-switch.nro", "release Stage no bare NRO") + mustNotContain(block, "switch.nro.sha256", "release Stage no NRO sha256 sidecar") +end + +do + local start = release:find("- name: Publish GitHub Release", 1, true) + check(start ~= nil, "release Publish GitHub Release present") + local rest = release:sub(start) + local nextStep = rest:find("\n - name:", 2) + local block = nextStep and rest:sub(1, nextStep - 1) or rest + mustContain(block, "gen1recomp-${v}-switch.zip", "release Publish Switch zip") + mustNotContain(block, "gen1recomp-${v}-switch.nro", "release Publish no bare NRO") + mustNotContain(block, "switch.nro.sha256", "release Publish no NRO sha256 sidecar") +end + T.finish("switch_ci_workflows_test")