diff --git a/.github/actions/fetch-toolchain/action.yml b/.github/actions/fetch-toolchain/action.yml new file mode 100644 index 00000000..fc24ab2e --- /dev/null +++ b/.github/actions/fetch-toolchain/action.yml @@ -0,0 +1,51 @@ +name: Fetch psxrecomp portable toolchain +description: > + Download cmake-clang-v1 from retcomm-toolchains and export TOOLCHAIN_DIR / + PSXRECOMP_TOOLCHAIN_DIR / BPE_TOOLCHAIN_DIR for later packaging steps. + +inputs: + artifact: + description: Platform pack id (linux-x64, windows-x64, macos-arm64, macos-x64) + required: true + framework: + description: Path to the psxrecomp checkout (contains tools/fetch_toolchain.sh) + required: false + default: psxrecomp + dl-dir: + description: Download cache directory + required: false + default: .cache/toolchain-dl + out-dir: + description: Unpack directory + required: false + default: .cache/toolchain-pack + repo: + description: GitHub owner/name for toolchain releases + required: false + default: TechnicallyComputers/retcomm-toolchains + +outputs: + toolchain-dir: + description: Absolute path to the unpacked toolchain root (bin/ present) + value: ${{ steps.fetch.outputs.toolchain-dir }} + +runs: + using: composite + steps: + - id: fetch + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + SCRIPT="${{ inputs.framework }}/tools/fetch_toolchain.sh" + if [[ ! -f "${SCRIPT}" ]]; then + echo "::error::missing ${SCRIPT}" + exit 1 + fi + chmod +x "${SCRIPT}" + bash "${SCRIPT}" \ + --artifact "${{ inputs.artifact }}" \ + --dl-dir "${{ inputs.dl-dir }}" \ + --out-dir "${{ inputs.out-dir }}" \ + --repo "${{ inputs.repo }}" diff --git a/.github/actions/stage-setup-sdk/action.yml b/.github/actions/stage-setup-sdk/action.yml new file mode 100644 index 00000000..e0b372cf --- /dev/null +++ b/.github/actions/stage-setup-sdk/action.yml @@ -0,0 +1,71 @@ +name: Stage psxrecomp setup SDK +description: > + Copy emitters, OpenBIOS profiles, retcomm-sdk.json, portable toolchain, and + Windows MinGW runtime DLLs into an existing setup-host stage directory. + +inputs: + stage: + description: Absolute or workspace-relative stage directory + required: true + framework: + description: Path to the psxrecomp checkout + required: false + default: psxrecomp + sdk-overlay: + description: Optional overlay directory (e.g. psxrecomp-sdk) + required: false + default: '' + recompiler-build: + description: Build dir containing psxrecomp-game / psxrecomp-bios + required: false + default: build-recompiler + toolchain-dir: + description: Unpacked toolchain root (defaults to env PSXRECOMP_TOOLCHAIN_DIR / TOOLCHAIN_DIR / BPE_TOOLCHAIN_DIR) + required: false + default: '' + host-exe: + description: Optional host .exe path for Windows DLL bundling + required: false + default: '' + runtime-bin: + description: MinGW bin dir for runtime DLLs (Windows) + required: false + default: '' + allow-no-toolchain: + description: If true, warn instead of failing when toolchain is unset + required: false + default: 'false' + +runs: + using: composite + steps: + - shell: bash + run: | + set -euo pipefail + SCRIPT="${{ inputs.framework }}/tools/stage_setup_sdk.sh" + if [[ ! -f "${SCRIPT}" ]]; then + echo "::error::missing ${SCRIPT}" + exit 1 + fi + chmod +x "${SCRIPT}" + args=( + --stage "${{ inputs.stage }}" + --framework "${{ inputs.framework }}" + --recompiler-build "${{ inputs.recompiler-build }}" + ) + if [[ -n "${{ inputs.sdk-overlay }}" ]]; then + args+=(--sdk-overlay "${{ inputs.sdk-overlay }}") + fi + if [[ -n "${{ inputs.toolchain-dir }}" ]]; then + args+=(--toolchain-dir "${{ inputs.toolchain-dir }}") + fi + if [[ "${{ inputs.allow-no-toolchain }}" == "true" ]]; then + args+=(--allow-no-toolchain) + fi + if [[ -n "${{ inputs.host-exe }}" ]]; then + args+=(--host-exe "${{ inputs.host-exe }}") + fi + if [[ -n "${{ inputs.runtime-bin }}" ]]; then + args+=(--runtime-bin "${{ inputs.runtime-bin }}") + fi + bash "${SCRIPT}" "${args[@]}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ac8ab37a..26bf7cea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,20 +16,22 @@ New to the codebase? Read [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) and (`recompiler/`), the runtime engine (`runtime/`), shared tools, and docs. Framework changes go here. - **Per-game repos** (`mstan/TombaRecomp`, `mstan/MegaManX6Recomp`, - `mstan/ApeEscapeRecomp`, …) — one per title. Each contains only that game's - config, seeds, and build glue and links this framework in as a submodule at - `psxrecomp/`. Game-specific work goes in the game repo; nothing game-specific - belongs in the framework. + `mstan/ApeEscapeRecomp`, …) — one per title. Each keeps **game code at the + repo root** and pins this framework (and usually `recomp-ui`) as **root-level + submodules** (`psxrecomp/`, `recomp-ui/`). Game-specific work goes in the game + repo; nothing game-specific belongs in the framework. Each game repo pins an **exact framework commit** — the git submodule pointer at `psxrecomp/` (some older game repos also carry a human-readable `psxrecomp-v4.pin` record; the submodule pointer is the source of truth). The framework evolves on its own cadence; a game only moves to a newer framework when someone deliberately -bumps that submodule pointer. See -[Linking the framework](docs/BUILDING.md#linking-the-framework) for the submodule -+ local-junction dev setup, and -[Framework changes and the pin](#framework-changes-and-the-pin) below for how a -framework change actually reaches a game. +bumps that submodule pointer. + +**Starting a new title or shipping a setup-host zip?** Follow +[`docs/GAME_PROJECT_SETUP.md`](docs/GAME_PROJECT_SETUP.md) (layout, CI template +under `docs/ci/templates/setup-release.yml`, release checklist). Also see +[Linking the framework](docs/BUILDING.md#linking-the-framework) and +[Framework changes and the pin](#framework-changes-and-the-pin) below. ## The core rules diff --git a/README.md b/README.md index 6ea297c1..fd141a7e 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,10 @@ standalone BIOS runtime supporting OpenBIOS and a compatible retail BIOS** — see [Release Package](#release-package) below. Bringing up a title of your own? Start with -[`CONTRIBUTING.md`](CONTRIBUTING.md) and open an issue — community projects are -listed here alongside the rest. +[`docs/GAME_PROJECT_SETUP.md`](docs/GAME_PROJECT_SETUP.md) (submodules, +setup-host CI template, release checklist), then +[`CONTRIBUTING.md`](CONTRIBUTING.md). Community projects are listed here +alongside the rest. ## What It Is @@ -95,15 +97,22 @@ Three things sit on that foundation: execution. PSXRecomp is a **framework**. Game-specific projects live in their own -repositories and link this one in as a **git submodule** to build a game binary. +repositories with **`psxrecomp/` and `recomp-ui/` as root-level submodules** +and game code (`game.toml`, seeds, CMake) at the repo root. See +[`docs/GAME_PROJECT_SETUP.md`](docs/GAME_PROJECT_SETUP.md). **New here?** The fastest way in: -[`docs/EXECUTION_MODEL.md`](docs/EXECUTION_MODEL.md) (how a game actually -runs — static / native-overlay / interpreter), then -[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md), -[`docs/BUILDING.md`](docs/BUILDING.md), -[`docs/MOD_PACKAGES.md`](docs/MOD_PACKAGES.md) (versioned runtime mods), -[`CONTRIBUTING.md`](CONTRIBUTING.md). + +| Path | Doc | +|------|-----| +| How a game runs | [`docs/EXECUTION_MODEL.md`](docs/EXECUTION_MODEL.md) | +| Architecture | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | +| Build the framework | [`docs/BUILDING.md`](docs/BUILDING.md) | +| **Ship a game repo** (submodules + CI + release checklist) | [`docs/GAME_PROJECT_SETUP.md`](docs/GAME_PROJECT_SETUP.md) | +| Setup-host CI template | [`docs/ci/templates/setup-release.yml`](docs/ci/templates/setup-release.yml) | +| Local Generate & rebuild CLI | [`docs/LOCAL_CODEGEN_SDK.md`](docs/LOCAL_CODEGEN_SDK.md) | +| Mods | [`docs/MOD_PACKAGES.md`](docs/MOD_PACKAGES.md) | +| Contributing | [`CONTRIBUTING.md`](CONTRIBUTING.md) | ## Which PlayStation BIOS does it use? @@ -709,13 +718,16 @@ the selected faithfully recompiled BIOS — OpenBIOS or retail BIOS — is the baseline and oracle, generated code is never hand-edited (fix the recompiler and regenerate), and a change proves itself against the Beetle oracle / on screen rather than by assertion. Game-specific work lives in the game repos, which pin -an exact framework commit as a submodule. +exact framework and UI commits as root-level submodules. -Read [`CONTRIBUTING.md`](CONTRIBUTING.md) before opening a PR — it covers the core -rules, how to verify a change, the regression checklist across the known games, -and how a framework fix reaches a game through its pin. Bugs and build problems go -to GitHub issues (include `gcc -v` / OS / generator for build failures); design -discussion happens in the **R.A.I.D.** Discord (invite below). +- New title / setup-host release: + [`docs/GAME_PROJECT_SETUP.md`](docs/GAME_PROJECT_SETUP.md) +- Framework PRs: [`CONTRIBUTING.md`](CONTRIBUTING.md) (rules, verification, + regression checklist, how a fix reaches a game through its pin) + +Bugs and build problems go to GitHub issues (include `gcc -v` / OS / generator +for build failures); design discussion happens in the **R.A.I.D.** Discord +(invite below). ## License diff --git a/docs/GAME_PROJECT_SETUP.md b/docs/GAME_PROJECT_SETUP.md new file mode 100644 index 00000000..275a92af --- /dev/null +++ b/docs/GAME_PROJECT_SETUP.md @@ -0,0 +1,193 @@ +# Bringing up a game project on PSXRecomp + +This guide is for **title developers**: you keep game code and config in your +own repository, pin this framework and `recomp-ui` as **root-level submodules**, +and (optionally) ship a **setup-host** zip that users generate from a legal disc +locally — the same model as Bomberman Party Edition / Masters of Teräs Käsi. + +Related docs: + +| Doc | When | +|-----|------| +| [`BUILDING.md`](BUILDING.md) | Local build of the framework / BIOS runtime | +| [`LOCAL_CODEGEN_SDK.md`](LOCAL_CODEGEN_SDK.md) | CLI contract for Generate & rebuild | +| [`ci/README.md`](ci/README.md) | Shared CI scripts and composite actions | +| [`CONTRIBUTING.md`](../CONTRIBUTING.md) | Framework contribution rules | + +--- + +## Recommended repository layout + +Everything game-specific lives at the **root** of *your* title repo. Framework +and UI are submodules next to that code — not nested under each other. + +```text +YourGameRecomp/ # your git repo +├── .gitmodules +├── CMakeLists.txt # game target + setup-host options +├── game.toml # disc / recompiler / runtime config +├── VERSION # release pin (e.g. 0.1.0) +├── seeds/ # function-start seeds for psxrecomp-game +├── host/ # optional: codegen host glue for the wizard +├── codegen_setup.c / .h # optional: PSX_HAS_GAME_CODEGEN wiring +├── launcher_assets/ # boxart / fonts / brand (recomp-ui) +├── scripts/ +│ └── package_setup_release.sh # title packager (thin; calls psxrecomp tools) +├── .github/workflows/ +│ └── release.yml # copied from docs/ci/templates/setup-release.yml +├── psxrecomp/ # submodule → mstan/psxrecomp (pinned commit) +├── recomp-ui/ # submodule → TechnicallyComputers/recomp-ui +├── psxrecomp-sdk/ # optional: CLI / prepare_disc overlay for zips +├── generated/ # local only — gitignore (not in CI setup zip) +└── prepared_disc/ / disc/ # local disc working tree — gitignore +``` + +### Add the submodules + +```bash +cd YourGameRecomp +git submodule add https://github.com/mstan/psxrecomp.git psxrecomp +git submodule add https://github.com/TechnicallyComputers/recomp-ui.git recomp-ui +# optional SDK surface used by RetComM / setup zips: +# git submodule add psxrecomp-sdk + +git submodule update --init --recursive +``` + +Pin **exact commits** (do not track `main` floating). Bump deliberately when you +want a newer framework or UI. + +Clone for contributors: + +```bash +git clone --recurse-submodules https://github.com/you/YourGameRecomp.git +``` + +--- + +## Local development loop + +1. **Legal disc** — place your dump where `game.toml` expects it (or pass + `--disc` to the CLI / wizard). Never commit disc images or retail BIOS dumps. +2. **Build emitters** (once per machine / after recompiler changes): + + ```bash + cmake -S psxrecomp/recompiler -B build-recompiler -G Ninja \ + -DCMAKE_BUILD_TYPE=Release + cmake --build build-recompiler --target psxrecomp-game psxrecomp-bios -j"$(nproc)" + ``` + +3. **Generate game (+ OpenBIOS) C** — either the setup wizard, or: + + ```bash + python3 psxrecomp/psxrecomp_cli.py generate \ + --config game.toml --project-root . --disc /path/to/dump.cue + ``` + +4. **Build the playable runtime**: + + ```bash + cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release + cmake --build build-release --target psx-runtime -j"$(nproc)" + ``` + +Details: [`LOCAL_CODEGEN_SDK.md`](LOCAL_CODEGEN_SDK.md), [`BUILDING.md`](BUILDING.md). + +--- + +## Setup-host releases (self-build zip) + +CI ships a zip **without** `generated/` game C and **without** retail BIOS +dumps. The zip includes: + +- Setup host exe (`recomp-ui` + generate/rebuild wizard) +- Your game sources (`game.toml`, seeds, CMake, host glue, …) +- `psxrecomp/` (runtime + CLI + OpenBIOS profiles + emitters) +- `recomp-ui/` sources (needed to rebuild) +- `toolchain/` — portable `cmake-clang-v1` from + [retcomm-toolchains](https://github.com/TechnicallyComputers/retcomm-toolchains) +- On Windows: MinGW runtime DLLs beside the host and emitters + +Players (or [RetComM](https://github.com/TechnicallyComputers/RetComM-Launcher)) +run Generate & rebuild locally. After a successful rebuild the CLI can prune +`toolchain/` and build intermediates. + +Do **not** set `PSX_PGO` in CI. PGO stays user-local when `[pgo] enabled = true`. + +--- + +## CI workflow template + +1. Copy the template into your title repo: + + ```bash + mkdir -p .github/workflows + cp psxrecomp/docs/ci/templates/setup-release.yml .github/workflows/release.yml + ``` + +2. Replace every `YOUR_*` placeholder (exe name, zip prefix, cmake setup flag, + release title, package script path). +3. Keep using the shared actions — do not reimplement toolchain fetch or DLL + bundling: + + ```yaml + - uses: ./psxrecomp/.github/actions/fetch-toolchain + with: + artifact: ${{ matrix.artifact }} + ``` + +4. Your `scripts/package_setup_release.sh` should: + - Stage the host exe, assets, and game sources + - Copy filtered `psxrecomp/` + `recomp-ui/` + - Call `psxrecomp/tools/stage_setup_sdk.sh` (emitters, OpenBIOS checks, + `toolchain/`, MinGW DLLs) + +Full action reference: [`ci/README.md`](ci/README.md). + +--- + +## Bundled release checklist + +Use this before tagging a setup-host release that matches other titles +(BPE / MotK / RetComM). + +### Repository + +- [ ] `psxrecomp/` and `recomp-ui/` are root-level submodules on pinned commits +- [ ] `game.toml` has disc identity (`[prepare_disc]` hashes/sizes) and boot EXE +- [ ] Seeds cover the boot path; `VERSION` matches the release you will tag +- [ ] Disc images, `generated/`, and `SCPH1001.BIN` are gitignored +- [ ] Setup-host CMake path builds with **no** game C and **no** BIOS backends + (e.g. `-DPSXRECOMP_ALLOW_NO_BIOS=ON` + your title’s force-setup option) +- [ ] Codegen host / `PSX_HAS_GAME_CODEGEN` wired so the wizard can regenerate + +### Packaging (shared helpers) + +- [ ] CI builds `psxrecomp-game` and `psxrecomp-bios` into `build-recompiler/` +- [ ] CI uses `./psxrecomp/.github/actions/fetch-toolchain` for the matrix OS +- [ ] Packager calls `psxrecomp/tools/stage_setup_sdk.sh` with + `--recompiler-build`, `--toolchain-dir` (or env from fetch), and on + Windows `--runtime-bin /mingw64/bin` + `--host-exe` +- [ ] Staged tree includes `psxrecomp/psxrecomp_cli.py` (sdk overlay if needed) +- [ ] Staged `psxrecomp/bios/` has `OpenBIOS.toml`, `openbios.bin`, + `OpenBIOS.LICENSE`, `SCPH1001.toml` — and **no** retail `.BIN` +- [ ] Windows zip has `libstdc++-6.dll` + `libgcc_s_seh-1.dll` next to both + emitters (and host deps such as `zlib1.dll` next to the exe) +- [ ] Zip mtimes are normalized (packager `touch`) so Ninja does not see + “future” files after extract + +### Zip contents smoke test + +- [ ] Linux / macOS / Windows artifacts named consistently + (`YOUR_PREFIX--.zip`) +- [ ] Extract → run host → Generate with a legal disc succeeds end-to-end +- [ ] After rebuild, game launches; saves/settings land beside the exe +- [ ] RetComM install/update (if you publish a catalog entry) uses this **same** + zip — no separate tools pack required + +### Publish + +- [ ] Tag `vX.Y.Z`; GitHub Release attaches all four platform zips +- [ ] Release notes say the zip is a setup host (no disc / no retail BIOS / + no pre-generated game C) +- [ ] Catalog / RetComM entry points at the release assets when ready diff --git a/docs/LOCAL_CODEGEN_SDK.md b/docs/LOCAL_CODEGEN_SDK.md index 1d9fab15..bcf720c4 100644 --- a/docs/LOCAL_CODEGEN_SDK.md +++ b/docs/LOCAL_CODEGEN_SDK.md @@ -20,7 +20,12 @@ then game C, then rebuild links everything. |-------|------| | Setup exe | `recomp-ui` + codegen host; opens Generate & rebuild | | Game zip `psxrecomp/` | `psxrecomp_cli.py`, `tools/`, `psxrecomp-game` + `psxrecomp-bios` | +| `tools/fetch_toolchain.sh` | CI: download/unpack portable `cmake-clang-v1` | +| `tools/stage_setup_sdk.sh` | CI/pack: emitters, OpenBIOS checks, embed `toolchain/`, MinGW DLLs | +| `tools/bundle_mingw_dlls.sh` | Windows: copy MinGW runtime DLLs next to host + emitters | | Game zip `toolchain/` | Portable `cmake-clang-v1` (cmake/ninja/clang); pruned after rebuild | +| `docs/ci/` | Composite actions + [`templates/setup-release.yml`](ci/templates/setup-release.yml) | +| `docs/GAME_PROJECT_SETUP.md` | Submodules, CI template usage, bundled-release checklist | | Game sources | `game.toml`, seeds, `CMakeLists.txt`, `psxrecomp/`, `recomp-ui/` | RetComM harvests emitters + toolchain into shared caches (no separate tools diff --git a/docs/ci/README.md b/docs/ci/README.md new file mode 100644 index 00000000..cd03f3e3 --- /dev/null +++ b/docs/ci/README.md @@ -0,0 +1,65 @@ +# CI helpers for setup-host releases + +Shared scripts and GitHub Actions so new PSXRecomp titles do not reimplement +toolchain fetch, emitter staging, or Windows MinGW DLL bundling. + +**Start here for a new game repo:** +[`../GAME_PROJECT_SETUP.md`](../GAME_PROJECT_SETUP.md) + +## Workflow template + +Copy into the **game** repository (not into this framework alone): + +```bash +mkdir -p .github/workflows +cp psxrecomp/docs/ci/templates/setup-release.yml .github/workflows/release.yml +# edit YOUR_* placeholders +``` + +Template path: [`templates/setup-release.yml`](templates/setup-release.yml) + +A shorter stub also lives at [`setup-release.yml.example`](setup-release.yml.example) +(points at the full template). + +## Tools + +| Script | Role | +|--------|------| +| `tools/fetch_toolchain.sh` | Download/unpack `cmake-clang-v1` from retcomm-toolchains | +| `tools/stage_setup_sdk.sh` | Stage emitters, OpenBIOS checks, `toolchain/`, MinGW DLLs | +| `tools/bundle_mingw_dlls.sh` | Copy imported non-system DLLs next to Windows PEs | + +## Composite actions + +Use from a game repo after `actions/checkout` with `submodules: recursive`: + +```yaml +- uses: ./psxrecomp/.github/actions/fetch-toolchain + with: + artifact: ${{ matrix.artifact }} # linux-x64 | windows-x64 | macos-arm64 | macos-x64 + +- uses: ./psxrecomp/.github/actions/stage-setup-sdk + with: + stage: dist/stage-setup-${{ matrix.artifact }} + sdk-overlay: psxrecomp-sdk + recompiler-build: build-recompiler + runtime-bin: /mingw64/bin # Windows / MSYS2 only +``` + +`fetch-toolchain` exports `TOOLCHAIN_DIR`, `PSXRECOMP_TOOLCHAIN_DIR`, and +`BPE_TOOLCHAIN_DIR` via `GITHUB_ENV` for later steps. + +## Title responsibilities + +Keep in the game repo: + +- Setup-host CMake flags and exe basename +- Copying game sources / assets into the stage +- Zip naming and release notes +- Thin `scripts/package_setup_release.sh` that stages title files then calls + `psxrecomp/tools/stage_setup_sdk.sh` + +## Release checklist + +See the **Bundled release checklist** in +[`../GAME_PROJECT_SETUP.md`](../GAME_PROJECT_SETUP.md#bundled-release-checklist). diff --git a/docs/ci/setup-release.yml.example b/docs/ci/setup-release.yml.example new file mode 100644 index 00000000..169e5121 --- /dev/null +++ b/docs/ci/setup-release.yml.example @@ -0,0 +1,23 @@ +# Deprecated stub — use the full template: +# +# docs/ci/templates/setup-release.yml +# +# Install: +# cp psxrecomp/docs/ci/templates/setup-release.yml .github/workflows/release.yml +# +# Guide: docs/GAME_PROJECT_SETUP.md +# Actions: docs/ci/README.md + +name: Release builds (see docs/ci/templates/setup-release.yml) + +on: + workflow_dispatch: + +jobs: + redirect: + runs-on: ubuntu-24.04 + steps: + - run: | + echo "Copy psxrecomp/docs/ci/templates/setup-release.yml to .github/workflows/release.yml" + echo "See psxrecomp/docs/GAME_PROJECT_SETUP.md" + exit 1 diff --git a/docs/ci/templates/setup-release.yml b/docs/ci/templates/setup-release.yml new file mode 100644 index 00000000..0cdc178e --- /dev/null +++ b/docs/ci/templates/setup-release.yml @@ -0,0 +1,270 @@ +# Setup-host multi-platform release template for a PSXRecomp *game* repository. +# +# Install: +# mkdir -p .github/workflows +# cp psxrecomp/docs/ci/templates/setup-release.yml .github/workflows/release.yml +# Then replace every YOUR_* token. See docs/GAME_PROJECT_SETUP.md. +# +# Shared (do not fork these into the title): +# ./psxrecomp/.github/actions/fetch-toolchain +# ./psxrecomp/.github/actions/stage-setup-sdk +# psxrecomp/tools/{fetch_toolchain,stage_setup_sdk,bundle_mingw_dlls}.sh +# +# Title-owned: +# CMake setup-host flags, exe basename, zip prefix, package_setup_release.sh + +name: Release builds + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g. 0.1.0 or v0.1.0)' + required: true + type: string + publish: + description: 'Create git tag vX.Y.Z and GitHub Release' + required: true + type: boolean + default: true + push: + tags: + - 'v*' + +concurrency: + group: yourgame-release-${{ github.event_name }}-${{ github.ref }}-${{ github.run_id }} + cancel-in-progress: true + +permissions: + contents: write + +env: + CMAKE_BUILD_TYPE: Release + +jobs: + prepare: + name: Resolve version + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.meta.outputs.version }} + tag: ${{ steps.meta.outputs.tag }} + publish: ${{ steps.meta.outputs.publish }} + steps: + - name: Normalize version / tag + id: meta + shell: bash + run: | + set -euo pipefail + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + RAW="${{ github.event.inputs.version }}" + PUBLISH="${{ github.event.inputs.publish }}" + else + RAW="${{ github.ref_name }}" + PUBLISH="true" + fi + RAW="$(printf '%s' "${RAW}" | tr -d '[:space:]')" + VER="${RAW#v}" + if [[ ! "${VER}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.+-][A-Za-z0-9.+-]*)?$ ]]; then + echo "::error::invalid version '${RAW}' (want X.Y.Z or vX.Y.Z)" + exit 1 + fi + echo "version=${VER}" >>"$GITHUB_OUTPUT" + echo "tag=v${VER}" >>"$GITHUB_OUTPUT" + echo "publish=${PUBLISH}" >>"$GITHUB_OUTPUT" + + build: + name: ${{ matrix.artifact }} + needs: prepare + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + artifact: linux-x64 + shell: bash + - os: windows-2022 + artifact: windows-x64 + shell: 'msys2 {0}' + - os: macos-15 + artifact: macos-arm64 + shell: bash + deployment_target: '11.0' + - os: macos-15-intel + artifact: macos-x64 + shell: bash + deployment_target: '11.0' + defaults: + run: + shell: ${{ matrix.shell }} + env: + # Rename to match your packager (e.g. BPE_RELEASE_VERSION). + RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 1 + + - name: Pin VERSION + shell: bash + run: | + set -euo pipefail + printf '%s\n' "${RELEASE_VERSION}" >VERSION + echo "VERSION=$(cat VERSION)" + + - name: Setup host mode (no game C / BIOS backends in CI) + shell: bash + run: | + set -euo pipefail + rm -rf generated psxrecomp/generated + echo "Cleared generated/ — CI builds a BIOS-free setup host." + + - name: Setup MSYS2 (Windows) + if: runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + mingw-w64-x86_64-toolchain + mingw-w64-x86_64-cmake + mingw-w64-x86_64-ninja + mingw-w64-x86_64-SDL2 + mingw-w64-x86_64-pkgconf + curl + zip + + - name: Install deps (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build pkg-config \ + libsdl2-dev libgl1-mesa-dev zip + + - name: Install deps (macOS) + if: runner.os == 'macOS' + run: | + brew install cmake ninja pkg-config sdl3 || brew install cmake ninja pkg-config sdl2 + + - name: Build recompiler tools (emitters) + run: | + set -euo pipefail + JOBS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)" + if [[ -d /mingw64/bin ]]; then + export PATH="/mingw64/bin:${PATH}" + fi + cmake -S psxrecomp/recompiler -B build-recompiler -G Ninja \ + -DCMAKE_BUILD_TYPE=Release + cmake --build build-recompiler \ + --target psxrecomp-game psxrecomp-bios -j"${JOBS}" + for bin in psxrecomp-bios psxrecomp-game; do + if [[ -f "build-recompiler/${bin}.exe" ]]; then + chmod +x "build-recompiler/${bin}.exe" + elif [[ -f "build-recompiler/${bin}" ]]; then + chmod +x "build-recompiler/${bin}" + else + echo "::error::${bin} missing after recompiler build" + exit 1 + fi + done + + - name: Configure & build setup host + env: + CMAKE_OSX_DEPLOYMENT_TARGET: ${{ matrix.deployment_target || '' }} + run: | + set -euo pipefail + EXTRA=() + if [[ -n "${CMAKE_OSX_DEPLOYMENT_TARGET:-}" ]]; then + EXTRA+=("-DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}") + fi + JOBS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)" + # YOUR_SETUP_CMAKE_FLAGS: title-specific. Example from BPE: + # -DYOURGAME_FORCE_SETUP_HOST=ON -DPSXRECOMP_ALLOW_NO_BIOS=ON + cmake -S . -B build-ci -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DPSXRECOMP_ALLOW_NO_BIOS=ON \ + -DYOURGAME_FORCE_SETUP_HOST=ON \ + -DPSX_GAME_VERSION="${RELEASE_VERSION}" \ + "${EXTRA[@]}" + cmake --build build-ci --target psx-runtime -j"${JOBS}" + + - name: Fetch portable toolchain pack + uses: ./psxrecomp/.github/actions/fetch-toolchain + with: + artifact: ${{ matrix.artifact }} + + - name: Package setup zip + run: | + set -euo pipefail + chmod +x scripts/package_setup_release.sh + if [[ "${{ runner.os }}" == "Windows" ]]; then + export PSXRECOMP_RUNTIME_BIN_DIR=/mingw64/bin + export BPE_RUNTIME_BIN_DIR=/mingw64/bin + fi + export RELEASE_VERSION + # Packager stages title files, then calls stage_setup_sdk.sh. + if [[ -z "${PSXRECOMP_TOOLCHAIN_DIR:-${TOOLCHAIN_DIR:-${BPE_TOOLCHAIN_DIR:-}}}" ]]; then + echo "::error::toolchain dir not set — fetch-toolchain failed?" + exit 1 + fi + scripts/package_setup_release.sh build-ci "${{ matrix.artifact }}" build-recompiler + ls -la dist/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + # YOUR_ZIP_PREFIX e.g. bpe, motk, yourgame + name: YOUR_ZIP_PREFIX-${{ matrix.artifact }} + path: dist/YOUR_ZIP_PREFIX-*-${{ matrix.artifact }}.zip + if-no-files-found: error + retention-days: 30 + + release: + name: GitHub Release + needs: [prepare, build] + if: needs.prepare.outputs.publish == 'true' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Flatten zips + run: | + set -euo pipefail + mkdir -p dist + find artifacts -type f -name 'YOUR_ZIP_PREFIX-*.zip' -exec cp -v {} dist/ \; + ls -la dist/ + + - name: Ensure release tag exists + if: github.event_name == 'workflow_dispatch' + env: + TAG: ${{ needs.prepare.outputs.tag }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + if ! git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then + git tag -a "${TAG}" -m "YOUR_GAME_TITLE ${TAG}" + fi + if ! git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then + git push origin "refs/tags/${TAG}" + fi + + - name: Publish release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare.outputs.tag }} + name: YOUR_GAME_TITLE ${{ needs.prepare.outputs.tag }} + files: | + dist/YOUR_ZIP_PREFIX-*.zip + generate_release_notes: true + fail_on_unmatched_files: true + target_commitish: ${{ github.sha }} diff --git a/tools/bundle_mingw_dlls.sh b/tools/bundle_mingw_dlls.sh new file mode 100755 index 00000000..3e676f0d --- /dev/null +++ b/tools/bundle_mingw_dlls.sh @@ -0,0 +1,225 @@ +#!/usr/bin/env bash +# Bundle imported non-system DLLs next to Windows PE executables. +# +# Title packagers (BPE/MotK setup zips) call this so MSYS2 MinGW emitters +# (psxrecomp-game / psxrecomp-bios) ship with libstdc++/libgcc — the portable +# llvm-mingw toolchain does not provide those GCC runtimes. +# +# Usage: +# bundle_mingw_dlls.sh [options] --exe [--dest ] [--label ] ... +# +# Options: +# --runtime-bin DIR Preferred DLL search directory (repeatable) +# --search-dir DIR Extra DLL search directory (repeatable) +# --exe PATH PE to inspect (repeatable; --dest/--label apply to it) +# --dest DIR Copy DLLs here (default: dirname of the preceding --exe) +# --label NAME Log label for the preceding --exe +# --require DLL After bundling, DLL must exist in every --dest that +# imported it (repeatable) +# --soft-missing Warn instead of exit 1 when objdump is unavailable +# +# Exit 1 if an imported DLL cannot be found, or a --require check fails. +set -euo pipefail + +SOFT_MISSING=0 +RUNTIME_BINS=() +SEARCH_DIRS=() +REQUIRE=() + +# Parallel arrays for targets. +EXE_PATHS=() +DEST_DIRS=() +LABELS=() + +pending_dest="" +pending_label="" + +usage() { + sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//' + exit 2 +} + +flush_pending_meta() { + # Apply trailing --dest/--label to the last --exe. + local n="${#EXE_PATHS[@]}" + if [[ "$n" -eq 0 ]]; then + return 0 + fi + local i=$((n - 1)) + if [[ -n "${pending_dest}" ]]; then + DEST_DIRS[$i]="${pending_dest}" + pending_dest="" + fi + if [[ -n "${pending_label}" ]]; then + LABELS[$i]="${pending_label}" + pending_label="" + fi +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage ;; + --soft-missing) SOFT_MISSING=1; shift ;; + --runtime-bin) + RUNTIME_BINS+=("${2:?}"); shift 2 ;; + --search-dir) + SEARCH_DIRS+=("${2:?}"); shift 2 ;; + --require) + REQUIRE+=("${2:?}"); shift 2 ;; + --exe) + flush_pending_meta + EXE_PATHS+=("${2:?}") + DEST_DIRS+=("") + LABELS+=("") + shift 2 + ;; + --dest) + pending_dest="${2:?}"; shift 2 ;; + --label) + pending_label="${2:?}"; shift 2 ;; + *) + echo "error: unknown arg: $1" >&2 + usage + ;; + esac +done +flush_pending_meta + +if [[ ${#EXE_PATHS[@]} -eq 0 ]]; then + echo "error: at least one --exe is required" >&2 + usage +fi + +OBJDUMP="" +if command -v x86_64-w64-mingw32-objdump >/dev/null 2>&1; then + OBJDUMP="x86_64-w64-mingw32-objdump" +elif command -v objdump >/dev/null 2>&1; then + OBJDUMP="objdump" +else + if [[ "${SOFT_MISSING}" -eq 1 ]]; then + echo "warning: no objdump; skipping MinGW DLL bundling" >&2 + exit 0 + fi + echo "error: no objdump; cannot bundle MinGW DLLs" >&2 + exit 1 +fi + +SYSTEM_DLL_RE='^(KERNEL32|USER32|GDI32|ADVAPI32|SHELL32|OLE32|OLEAUT32|WS2_32|WINMM|IMM32|SETUPAPI|VERSION|OPENGL32|COMCTL32|COMDLG32|RPCRT4|SHLWAPI|CRYPT32|BCRYPT|IPHLPAPI|NSI|DNSAPI|MSVCRT|UCRTBASE|VCRUNTIME|API-MS-).*\.DLL$' + +PROBE_DLLS=( + SDL2.dll + zlib1.dll + libgcc_s_seh-1.dll + libstdc++-6.dll + libwinpthread-1.dll + libssp-0.dll +) + +exe_imports_dll() { + local exe="$1" + local dll="$2" + "${OBJDUMP}" -p "${exe}" 2>/dev/null | grep -qi "DLL Name:[[:space:]]*${dll}" +} + +bundle_one() { + local exe="$1" + local dest_dir="$2" + local label="$3" + local dll src key cand d + local -a needed=() + local -a unique=() + local -a candidates=() + local -A seen=() + + if [[ ! -f "${exe}" ]]; then + echo "error: cannot bundle DLLs; missing ${exe}" >&2 + exit 1 + fi + if [[ -z "${dest_dir}" ]]; then + dest_dir="$(dirname "${exe}")" + fi + if [[ -z "${label}" ]]; then + label="$(basename "${exe}")" + fi + mkdir -p "${dest_dir}" + + mapfile -t needed < <( + "${OBJDUMP}" -p "${exe}" 2>/dev/null \ + | awk '/DLL Name:/{print $3}' \ + | grep -viE "${SYSTEM_DLL_RE}" \ + | sort -u || true + ) + needed+=("${PROBE_DLLS[@]}") + + for dll in "${needed[@]}"; do + [[ -n "${dll}" ]] || continue + key="$(printf '%s' "${dll}" | tr '[:upper:]' '[:lower:]')" + if [[ -n "${seen[$key]:-}" ]]; then + continue + fi + seen[$key]=1 + unique+=("${dll}") + done + + for dll in "${unique[@]}"; do + if ! exe_imports_dll "${exe}" "${dll}"; then + continue + fi + src="" + candidates=( + "$(dirname "${exe}")/${dll}" + "${dest_dir}/${dll}" + "/mingw64/bin/${dll}" + "/usr/x86_64-w64-mingw32/bin/${dll}" + ) + for d in "${SEARCH_DIRS[@]+"${SEARCH_DIRS[@]}"}"; do + candidates+=("${d}/${dll}") + done + for d in "${RUNTIME_BINS[@]+"${RUNTIME_BINS[@]}"}"; do + candidates+=("${d}/${dll}") + done + for cand in "${candidates[@]}"; do + [[ -n "${cand}" ]] || continue + if [[ -f "${cand}" ]]; then + src="${cand}" + break + fi + done + if [[ -z "${src}" ]]; then + echo "error: required DLL missing for ${label}: ${dll}" >&2 + echo " exe: ${exe}" >&2 + echo " dest: ${dest_dir}" >&2 + if [[ ${#SEARCH_DIRS[@]} -gt 0 ]]; then + echo " search-dirs: ${SEARCH_DIRS[*]}" >&2 + fi + if [[ ${#RUNTIME_BINS[@]} -gt 0 ]]; then + echo " runtime-bins: ${RUNTIME_BINS[*]}" >&2 + fi + exit 1 + fi + cp -f "${src}" "${dest_dir}/" + echo "bundled ${dll} → ${dest_dir}/ (${label})" + done +} + +for i in "${!EXE_PATHS[@]}"; do + bundle_one "${EXE_PATHS[$i]}" "${DEST_DIRS[$i]}" "${LABELS[$i]}" +done + +for dll in "${REQUIRE[@]}"; do + [[ -n "${dll}" ]] || continue + for i in "${!EXE_PATHS[@]}"; do + exe="${EXE_PATHS[$i]}" + dest="${DEST_DIRS[$i]:-}" + if [[ -z "${dest}" ]]; then + dest="$(dirname "${exe}")" + fi + if ! exe_imports_dll "${exe}" "${dll}"; then + continue + fi + if [[ ! -f "${dest}/${dll}" ]]; then + echo "error: $(basename "${exe}") imported ${dll} but it is missing under ${dest}" >&2 + exit 1 + fi + done +done diff --git a/tools/fetch_toolchain.sh b/tools/fetch_toolchain.sh new file mode 100755 index 00000000..f04375a2 --- /dev/null +++ b/tools/fetch_toolchain.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Download and unpack a retcomm-toolchains cmake-clang-v1 pack. +# +# Usage: +# fetch_toolchain.sh --artifact \ +# [--dl-dir DIR] [--out-dir DIR] [--repo OWNER/NAME] +# +# Prints: TOOLCHAIN_DIR= +# When GITHUB_ENV is set (Actions), also appends: +# TOOLCHAIN_DIR=... +# PSXRECOMP_TOOLCHAIN_DIR=... +# BPE_TOOLCHAIN_DIR=... (compat alias for existing title workflows) +# +# Env: +# GH_TOKEN / GITHUB_TOKEN — optional; used for gh or Authorization on curl +set -euo pipefail + +ARTIFACT="" +DL_DIR=".cache/toolchain-dl" +OUT_DIR=".cache/toolchain-pack" +REPO="TechnicallyComputers/retcomm-toolchains" + +usage() { + sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//' + exit 2 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage ;; + --artifact) ARTIFACT="${2:?}"; shift 2 ;; + --dl-dir) DL_DIR="${2:?}"; shift 2 ;; + --out-dir) OUT_DIR="${2:?}"; shift 2 ;; + --repo) REPO="${2:?}"; shift 2 ;; + *) + echo "error: unknown arg: $1" >&2 + usage + ;; + esac +done + +if [[ -z "${ARTIFACT}" ]]; then + echo "error: --artifact is required" >&2 + usage +fi + +case "${ARTIFACT}" in + linux-x64) + PATTERN='cmake-clang-v1*linux*.zip' + ASSET_NAME='cmake-clang-v1-linux-x64.zip' + ;; + windows-x64) + PATTERN='cmake-clang-v1*windows*.zip' + ASSET_NAME='cmake-clang-v1-windows-x64.zip' + ;; + macos-arm64|macos-x64) + PATTERN='cmake-clang-v1*macos*.zip' + ASSET_NAME='cmake-clang-v1-macos-universal.zip' + ;; + *) + echo "error: unknown artifact '${ARTIFACT}' (want linux-x64|windows-x64|macos-arm64|macos-x64)" >&2 + exit 1 + ;; +esac + +TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}" + +rm -rf "${DL_DIR}" "${OUT_DIR}" +mkdir -p "${DL_DIR}" "${OUT_DIR}" + +if command -v gh >/dev/null 2>&1; then + if [[ -n "${TOKEN}" ]]; then + export GH_TOKEN="${TOKEN}" + fi + gh release download -R "${REPO}" -p "${PATTERN}" -D "${DL_DIR}" --clobber +elif command -v curl >/dev/null 2>&1; then + ASSET_URL="https://github.com/${REPO}/releases/latest/download/${ASSET_NAME}" + echo "gh not on PATH; curling ${ASSET_URL}" + curl_args=(-fsSL -o "${DL_DIR}/${ASSET_NAME}") + if [[ -n "${TOKEN}" ]]; then + curl_args+=(-H "Authorization: Bearer ${TOKEN}") + fi + curl "${curl_args[@]}" "${ASSET_URL}" +else + echo "error: neither gh nor curl available to fetch toolchain pack" >&2 + exit 1 +fi + +ZIP="$(find "${DL_DIR}" -maxdepth 1 -type f -name '*.zip' | head -n1)" +if [[ -z "${ZIP}" ]]; then + echo "error: no toolchain zip matching ${PATTERN}" >&2 + ls -la "${DL_DIR}" >&2 || true + exit 1 +fi + +echo "Unpacking toolchain from ${ZIP}" +if command -v unzip >/dev/null 2>&1; then + unzip -q "${ZIP}" -d "${OUT_DIR}" +else + python3 -c ' +import pathlib, zipfile, sys +z = next(pathlib.Path(sys.argv[1]).glob("*.zip")) +zipfile.ZipFile(z).extractall(sys.argv[2]) +' "${DL_DIR}" "${OUT_DIR}" +fi + +# Unwrap single top-level directory if present. +shopt -s nullglob +entries=("${OUT_DIR}"/*) +shopt -u nullglob +if [[ ${#entries[@]} -eq 1 && -d "${entries[0]}" ]]; then + TOOLCHAIN_DIR="$(cd "${entries[0]}" && pwd)" +else + TOOLCHAIN_DIR="$(cd "${OUT_DIR}" && pwd)" +fi + +if [[ ! -d "${TOOLCHAIN_DIR}/bin" ]]; then + echo "error: toolchain pack missing bin/ under ${TOOLCHAIN_DIR}" >&2 + find "${OUT_DIR}" -maxdepth 3 >&2 || true + exit 1 +fi + +echo "TOOLCHAIN_DIR=${TOOLCHAIN_DIR}" +if [[ -n "${GITHUB_ENV:-}" ]]; then + { + echo "TOOLCHAIN_DIR=${TOOLCHAIN_DIR}" + echo "PSXRECOMP_TOOLCHAIN_DIR=${TOOLCHAIN_DIR}" + echo "BPE_TOOLCHAIN_DIR=${TOOLCHAIN_DIR}" + } >>"${GITHUB_ENV}" +fi +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "toolchain-dir=${TOOLCHAIN_DIR}" >>"${GITHUB_OUTPUT}" +fi diff --git a/tools/stage_setup_sdk.sh b/tools/stage_setup_sdk.sh new file mode 100755 index 00000000..ea12f52b --- /dev/null +++ b/tools/stage_setup_sdk.sh @@ -0,0 +1,251 @@ +#!/usr/bin/env bash +# Stage psxrecomp emitters + OpenBIOS surface + optional portable toolchain +# into an existing setup-host release stage directory. +# +# Title packagers copy the host exe, game sources, and framework tree first, +# then call this to finish the RetComM/wizard-complete zip layout. +# +# Usage: +# stage_setup_sdk.sh --stage [options] +# +# Options: +# --framework DIR psxrecomp source tree (default: /psxrecomp) +# --sdk-overlay DIR Overlay (e.g. psxrecomp-sdk) onto stage/psxrecomp +# --recompiler-build DIR Where psxrecomp-game/bios were built (repeatable) +# --toolchain-dir DIR Pack root with bin/; embedded as stage/toolchain/ +# --allow-no-toolchain Warn instead of failing when toolchain unset +# --host-exe PATH Host PE for Windows DLL bundling (optional) +# --runtime-bin DIR MinGW runtime DLL search dir (repeatable) +# --search-dir DIR Extra DLL search dir (repeatable) +# --require-cli Require psxrecomp_cli.py after overlays (default on) +# +# Env aliases for toolchain dir (first wins): +# PSXRECOMP_TOOLCHAIN_DIR, TOOLCHAIN_DIR, BPE_TOOLCHAIN_DIR +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +FW_TOOLS="${SCRIPT_DIR}" + +STAGE="" +FRAMEWORK="" +SDK_OVERLAY="" +RECOMPILER_BUILDS=() +TOOLCHAIN_DIR="${PSXRECOMP_TOOLCHAIN_DIR:-${TOOLCHAIN_DIR:-${BPE_TOOLCHAIN_DIR:-}}}" +ALLOW_NO_TOOLCHAIN=0 +HOST_EXE="" +RUNTIME_BINS=() +SEARCH_DIRS=() +REQUIRE_CLI=1 + +usage() { + sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//' + exit 2 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage ;; + --stage) STAGE="${2:?}"; shift 2 ;; + --framework) FRAMEWORK="${2:?}"; shift 2 ;; + --sdk-overlay) SDK_OVERLAY="${2:?}"; shift 2 ;; + --recompiler-build) RECOMPILER_BUILDS+=("${2:?}"); shift 2 ;; + --toolchain-dir) TOOLCHAIN_DIR="${2:?}"; shift 2 ;; + --allow-no-toolchain) ALLOW_NO_TOOLCHAIN=1; shift ;; + --host-exe) HOST_EXE="${2:?}"; shift 2 ;; + --runtime-bin) RUNTIME_BINS+=("${2:?}"); shift 2 ;; + --search-dir) SEARCH_DIRS+=("${2:?}"); shift 2 ;; + --require-cli) REQUIRE_CLI=1; shift ;; + --no-require-cli) REQUIRE_CLI=0; shift ;; + *) + echo "error: unknown arg: $1" >&2 + usage + ;; + esac +done + +if [[ -z "${STAGE}" ]]; then + echo "error: --stage is required" >&2 + usage +fi +STAGE="$(cd "${STAGE}" && pwd)" + +if [[ -z "${FRAMEWORK}" ]]; then + if [[ -d "${PWD}/psxrecomp" ]]; then + FRAMEWORK="$(cd "${PWD}/psxrecomp" && pwd)" + else + FRAMEWORK="$(cd "${SCRIPT_DIR}/.." && pwd)" + fi +else + FRAMEWORK="$(cd "${FRAMEWORK}" && pwd)" +fi + +mkdir -p "${STAGE}/psxrecomp" + +# Optional SDK overlay (CLI / prepare_disc / progress helpers). +if [[ -n "${SDK_OVERLAY}" ]]; then + if [[ ! -d "${SDK_OVERLAY}" ]]; then + echo "error: --sdk-overlay not a directory: ${SDK_OVERLAY}" >&2 + exit 1 + fi + if command -v rsync >/dev/null 2>&1; then + rsync -a "${SDK_OVERLAY}/" "${STAGE}/psxrecomp/" + else + cp -a "${SDK_OVERLAY}/." "${STAGE}/psxrecomp/" + fi + echo "overlaid SDK from ${SDK_OVERLAY}" +fi + +find_tool_bin() { + local name="$1" + local dir cand + local -a roots=() + local r + for r in "${RECOMPILER_BUILDS[@]+"${RECOMPILER_BUILDS[@]}"}"; do + roots+=("$(cd "${r}" && pwd)") + done + roots+=( + "${STAGE}/psxrecomp/recompiler/build" + "${FRAMEWORK}/recompiler/build" + "${PWD}/build-recompiler" + ) + for dir in "${roots[@]}"; do + [[ -d "${dir}" ]] || continue + for cand in \ + "${dir}/${name}" \ + "${dir}/${name}.exe" \ + "${dir}/Release/${name}.exe" + do + if [[ -f "${cand}" ]]; then + echo "${cand}" + return 0 + fi + done + done + return 1 +} + +GAME_BIN="$(find_tool_bin psxrecomp-game || true)" +BIOS_BIN="$(find_tool_bin psxrecomp-bios || true)" +if [[ -z "${GAME_BIN}" ]]; then + echo "error: psxrecomp-game not found (pass --recompiler-build)" >&2 + exit 1 +fi +if [[ -z "${BIOS_BIN}" ]]; then + echo "error: psxrecomp-bios not found (required for OpenBIOS regen)" >&2 + exit 1 +fi + +mkdir -p "${STAGE}/psxrecomp/recompiler/build" +cp -a "${GAME_BIN}" \ + "${STAGE}/psxrecomp/recompiler/build/$(basename "${GAME_BIN}")" +cp -a "${BIOS_BIN}" \ + "${STAGE}/psxrecomp/recompiler/build/$(basename "${BIOS_BIN}")" +chmod +x "${STAGE}/psxrecomp/recompiler/build/$(basename "${GAME_BIN}")" 2>/dev/null || true +chmod +x "${STAGE}/psxrecomp/recompiler/build/$(basename "${BIOS_BIN}")" 2>/dev/null || true +echo "staged emitters from ${GAME_BIN%/*}" + +cat >"${STAGE}/psxrecomp/retcomm-sdk.json" <<'EOF' +{ + "cli": "psxrecomp_cli.py", + "id": "psxrecomp-tools", + "game_bin": "recompiler/build/psxrecomp-game", + "bios_bin": "recompiler/build/psxrecomp-bios" +} +EOF + +for f in OpenBIOS.toml openbios.bin OpenBIOS.LICENSE SCPH1001.toml; do + if [[ ! -f "${STAGE}/psxrecomp/bios/${f}" ]]; then + echo "error: missing psxrecomp/bios/${f} in staged tree" >&2 + exit 1 + fi +done + +if [[ "${REQUIRE_CLI}" -eq 1 && ! -f "${STAGE}/psxrecomp/psxrecomp_cli.py" ]]; then + echo "error: missing psxrecomp/psxrecomp_cli.py (pass --sdk-overlay or copy CLI into stage)" >&2 + exit 1 +fi + +# Never ship retail BIOS dumps from the stage. +rm -f "${STAGE}/psxrecomp/bios/SCPH1001.BIN" \ + "${STAGE}/psxrecomp/bios/SCPH1001.bin" 2>/dev/null || true + +if [[ -n "${TOOLCHAIN_DIR}" && -d "${TOOLCHAIN_DIR}" ]]; then + if [[ ! -d "${TOOLCHAIN_DIR}/bin" ]]; then + echo "error: toolchain dir missing bin/: ${TOOLCHAIN_DIR}" >&2 + exit 1 + fi + mkdir -p "${STAGE}/toolchain" + if command -v rsync >/dev/null 2>&1; then + rsync -a --delete "${TOOLCHAIN_DIR}/" "${STAGE}/toolchain/" + else + rm -rf "${STAGE}/toolchain" + mkdir -p "${STAGE}/toolchain" + cp -a "${TOOLCHAIN_DIR}/." "${STAGE}/toolchain/" + fi + echo "bundled toolchain from ${TOOLCHAIN_DIR}" +elif [[ "${ALLOW_NO_TOOLCHAIN}" -eq 1 ]]; then + echo "warning: toolchain unset — zip will need system cmake/ninja" >&2 +else + echo "error: toolchain dir required (pass --toolchain-dir or set PSXRECOMP_TOOLCHAIN_DIR)" >&2 + exit 1 +fi + +# Windows MinGW DLL bundling for host + emitters. +BUNDLE="${FW_TOOLS}/bundle_mingw_dlls.sh" +if [[ ! -f "${BUNDLE}" ]]; then + # Staged copy (after sdk overlay) may own the helper. + if [[ -f "${STAGE}/psxrecomp/tools/bundle_mingw_dlls.sh" ]]; then + BUNDLE="${STAGE}/psxrecomp/tools/bundle_mingw_dlls.sh" + fi +fi + +need_dlls=0 +if [[ -n "${HOST_EXE}" && "${HOST_EXE}" == *.exe ]]; then + need_dlls=1 +fi +if [[ -f "${STAGE}/psxrecomp/recompiler/build/psxrecomp-game.exe" ]]; then + need_dlls=1 +fi + +if [[ "${need_dlls}" -eq 1 ]]; then + if [[ ! -f "${BUNDLE}" ]]; then + echo "error: bundle_mingw_dlls.sh not found next to stage_setup_sdk.sh" >&2 + exit 1 + fi + chmod +x "${BUNDLE}" 2>/dev/null || true + + for emitter in psxrecomp-game.exe psxrecomp-bios.exe; do + if [[ ! -f "${STAGE}/psxrecomp/recompiler/build/${emitter}" ]]; then + echo "error: missing ${emitter} in staged tree" >&2 + exit 1 + fi + done + + args=() + for d in "${RUNTIME_BINS[@]+"${RUNTIME_BINS[@]}"}"; do + args+=(--runtime-bin "${d}") + done + for d in "${SEARCH_DIRS[@]+"${SEARCH_DIRS[@]}"}"; do + args+=(--search-dir "${d}") + done + if [[ -n "${HOST_EXE}" && -f "${HOST_EXE}" ]]; then + args+=( + --exe "${HOST_EXE}" + --dest "${STAGE}" + --label "$(basename "${HOST_EXE}")" + ) + fi + args+=( + --exe "${STAGE}/psxrecomp/recompiler/build/psxrecomp-game.exe" + --dest "${STAGE}/psxrecomp/recompiler/build" + --label "psxrecomp-game.exe" + --exe "${STAGE}/psxrecomp/recompiler/build/psxrecomp-bios.exe" + --dest "${STAGE}/psxrecomp/recompiler/build" + --label "psxrecomp-bios.exe" + --require libgcc_s_seh-1.dll + --require libstdc++-6.dll + ) + bash "${BUNDLE}" "${args[@]}" +fi + +echo "stage_setup_sdk: ready under ${STAGE}/psxrecomp"