diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..3f0db22 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,126 @@ +name: release + +# Builds a Linux release of the game and attaches it to a Forgejo/Gitea +# Release, so players can download a ready-to-run build and only need to +# bring their own Skate 3 ISO (the built-in installer extracts it). +# +# Runs on the self-hosted runner (see tools/ci/setup_runner.sh and +# docs/ci-releases.md). The runner must have the extracted game dump at +# /opt/skate3-game (default.xex + data/webkit/EAWebkit.xex, and optionally +# the TU_12K2276_* title-update package). The game files are used only for +# codegen; they are NOT bundled into the release artifact. + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + build-linux: + # Match this label to your self-hosted runner's label (Docker runners + # commonly use ubuntu-latest / ubuntu-22.04 / ubuntu-24.04). + runs-on: ubuntu-24.04 + env: + GAME_ROOT: /opt/skate3-game + steps: + - name: Checkout + uses: https://code.forgejo.org/actions/checkout@v4 + with: + # Submodules are initialized manually so we can self-heal the + # broken upstream pins (see tools/ci/init-submodules.sh). + submodules: false + + - name: Init submodules + shell: bash + env: + SUBMODULE_KEY: ${{ secrets.SUBMODULE_KEY }} + run: | + set -euo pipefail + # SSH key for the self-hosted rexglue fork submodule + # (git@git.mithraic.cloud:ad3laid3/rexglue-skate3.git). + if [ -n "${SUBMODULE_KEY:-}" ]; then + mkdir -p ~/.ssh && chmod 700 ~/.ssh + printf '%s\n' "$SUBMODULE_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan git.mithraic.cloud >> ~/.ssh/known_hosts 2>/dev/null || true + fi + git submodule update --init --jobs 4 + bash tools/ci/init-submodules.sh + + - name: Install build dependencies + shell: bash + run: | + # Docker runner containers usually run as root (no sudo). + if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" != 0 ]; then + APT="sudo apt-get" + else + APT="apt-get" + fi + $APT update + $APT install -y --no-install-recommends \ + git cmake ninja-build pkg-config p7zip-full zip \ + clang-20 lld-20 \ + libgtk-3-dev libx11-xcb-dev \ + libvulkan-dev vulkan-tools mesa-vulkan-drivers \ + libasound2-dev libpulse-dev libpipewire-0.3-dev libudev-dev + + - name: Configure + shell: bash + run: | + set -euo pipefail + TU="" + if compgen -G "$GAME_ROOT"/TU_12K2276_* >/dev/null; then + TU="-DSKATE3_TITLE_UPDATE_PACKAGE=$(compgen -G "$GAME_ROOT"/TU_12K2276_* | head -1)" + fi + echo "TU_ARGS=$TU" >> "$GITHUB_ENV" + cmake --preset linux-relwithdebinfo -DSKATE3_GAME_DATA_ROOT="$GAME_ROOT" $TU + + - name: Generate recompiled code + shell: bash + run: cmake --build --preset linux-relwithdebinfo --target generate-all --parallel + + - name: Reconfigure (generated sources) + shell: bash + run: | + set -euo pipefail + TU="${TU_ARGS:-}" + cmake --preset linux-relwithdebinfo -DSKATE3_GAME_DATA_ROOT="$GAME_ROOT" $TU + + - name: Build + shell: bash + run: cmake --build --preset linux-relwithdebinfo --parallel + + - name: Package release zip + shell: bash + run: | + set -euo pipefail + OUT="out/build/linux-relwithdebinfo" + VER="${GITHUB_REF_NAME#v}" + [ -n "$VER" ] || VER="dev" + NAME="Skate3Recomp-Linux-$VER" + STAGE="$NAME" + rm -rf "$STAGE"; mkdir -p "$STAGE" + cp "$OUT/skate3" "$STAGE/" + cp "$OUT/librexruntime.so" "$STAGE/" 2>/dev/null || true + cp "$OUT/ghost_relay" "$STAGE/" 2>/dev/null || true + # Title-update patches staged next to the binary when the build + # used the TU package (the CMake stage target places them here). + if [ -f "$OUT/game/default.xexp" ]; then + mkdir -p "$STAGE/game/data/webkit" + cp "$OUT/game/default.xexp" "$STAGE/game/" + cp "$OUT/game/data/webkit/EAWebkit.xexp" "$STAGE/game/data/webkit/" 2>/dev/null || true + fi + cp README.md "$STAGE/" 2>/dev/null || true + zip -r "$NAME.zip" "$STAGE" + echo "ARTIFACT=$NAME.zip" >> "$GITHUB_ENV" + + - name: Publish release + uses: https://code.forgejo.org/forgejo/release-action@v2 + with: + token: ${{ secrets.RELEASE_TOKEN }} + files: ${{ env.ARTIFACT }} + tag: ${{ github.ref_name }} + title: "Skate 3 Recomp ${{ github.ref_name }}" + draft: false + prerelease: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index d4f1fed..a727d62 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,15 @@ /generated/ /generated.backup-*/ /tools/* -# ...except the ghost multiplayer relay server (a first-party tool). +# ...except the ghost multiplayer relay server + release/CI tooling (first-party). !/tools/ghost_relay_main.cpp !/tools/ghost_relay.service !/tools/ghost_relay_deploy.sh +!/tools/ci/ +!/tools/release/ +# Local release zips/stages are build products. +Skate3Recomp-*.zip +/Skate3Recomp-*/ # Local game dump and retail media. Users must provide their own copy. /game/ diff --git a/docs/releases.md b/docs/releases.md new file mode 100644 index 0000000..26b81a3 --- /dev/null +++ b/docs/releases.md @@ -0,0 +1,104 @@ +# Releases + +Releases are prebuilt binaries that players download so they only need their +own Skate 3 ISO — **no building from scratch**. This project does not and +cannot ship game content; the release zip is just the recompiled binaries. + +## What's in a release (and what isn't) + +A release zip (`Skate3Recomp-Linux-.zip`) contains only: + +- `skate3` — the recompiled game binary (also the in-app **ISO installer**) +- `librexruntime.so` (Linux) / `librexruntime.dll` (Windows) / + `librexruntime.dylib` (macOS) +- `ghost_relay` — the multiplayer relay server binary +- `README.md` +- `game/default.xexp` + `game/data/webkit/EAWebkit.xexp` — **only** when the + build used the Title Update package (these are the recompiled update + patches, not retail content) + +**No game files.** Players download the zip, run `skate3`, click "Select +ISO", and the built-in installer extracts their own legally-owned copy. The +game dump (`default.xex` + `data/webkit/EAWebkit.xex`) is required **only at +build time** — codegen mechanically translates the actual Xbox 360 binary +into C++ source, which is what gets compiled. It is never included in, or +uploaded with, a release. + +## Option A — build locally, upload manually (recommended) + +You already build successfully on your machine, so this is the simple path: + +```sh +# on your build machine, from the repo root +./tools/release/release.sh # build + package this platform +./tools/release/release.sh --tag v1.0.0 --upload # build + publish to Forgejo +``` + +The script builds (configure → codegen → build), packages the zip (binaries ++ `librexruntime.so` + `ghost_relay` + README + TU patches if used), prints a +`sha256sum`, and with `--upload` creates/updates a Forgejo Release and +attaches the zip. + +`--upload` needs: +- `GITEA_TOKEN` — a Forgejo token with **write:repository** scope (Settings → + Applications → Generate New Token), covering the release APIs. +- Optional `FORGEJO_REPO` (default `ad3laid3/skate3recomp-multiplayer`) and + `FORGEJO_INSTANCE` (default `https://git.mithraic.cloud`). + +Per-platform notes: + +| Platform | Build machine | Command | +|---|---|---| +| Linux | any Linux with the toolchain (README: Ubuntu 24.04 + Clang 20) | `./tools/release/release.sh` | +| Windows | a **Windows** machine (MSVC/clang-cl + Windows SDK, D3D12) | run the same script on Windows (`--os windows`) | +| macOS | an **Apple Silicon/Intel Mac** (Homebrew LLVM + MoltenVK, per README) | run the same script on macOS | + +Cross-compiling Windows/macOS from a Linux box is not practical for this +stack (D3D12 on Windows, Cocoa/MoltenVK on macOS need their native SDKs), so +each platform's release is built on that platform. + +## Option B — automated via Forgejo Actions (optional) + +`.forgejo/workflows/release.yml` builds and publishes Linux releases on push +of a `v*` tag (or a manual run), on a self-hosted runner. + +Requirements for the automation path: + +1. **A runner with the game dump.** Codegen needs the `.xex` files at build + time. On the runner, put the extracted dump at `/opt/skate3-game` + (`default.xex` + `data/webkit/EAWebkit.xex`, optionally the + `TU_12K2276_*` package). If your runner is a Docker container, mount that + directory into it (`-v /opt/skate3-game:/opt/skate3-game`). The game files + stay on the runner — they are never uploaded to the repo. +2. **Secrets** on the repo: + - `SUBMODULE_KEY` — an SSH deploy key with read access to + `ad3laid3/rexglue-skate3` (the self-hosted rexglue fork submodule). + - `RELEASE_TOKEN` — a Forgejo token with `write:repository` scope. +3. **Runner label** — the workflow uses `runs-on: ubuntu-24.04`; match it to + your runner's label (Docker runners commonly use + `ubuntu-latest`/`ubuntu-22.04`/`ubuntu-24.04` — change the `runs-on` line + to match). +4. **Broken submodule pins.** A fresh recursive clone currently fails because + the rexglue fork pins `thirdparty/imgui` to a commit `ocornut/imgui` no + longer carries. `tools/ci/init-submodules.sh` self-heals this (checks out + the upstream default tip for any nested submodule left empty) and the + workflow calls it after `git submodule update --init`. + +Windows/macOS jobs can be added to the same workflow when native +Windows/macOS runners are available (each job uses its platform preset and +artifact names; the package step is per-OS). + +## Doing it by hand + +If you'd rather not use the script, the equivalent steps are: + +```sh +cmake --preset linux-relwithdebinfo -DSKATE3_GAME_DATA_ROOT="$PWD/game" # add -DSKATE3_TITLE_UPDATE_PACKAGE=... if used +cmake --build --preset linux-relwithdebinfo --target generate-all --parallel +cmake --preset linux-relwithdebinfo -DSKATE3_GAME_DATA_ROOT="$PWD/game" +cmake --build --preset linux-relwithdebinfo --parallel +# zip out/build/linux-relwithdebinfo/{skate3,librexruntime.so,ghost_relay} + README +``` + +Then create a Release in the Forgejo web UI (` → Releases → New +Release`) and attach the zip. \ No newline at end of file diff --git a/tools/ci/init-submodules.sh b/tools/ci/init-submodules.sh new file mode 100755 index 0000000..6a87d13 --- /dev/null +++ b/tools/ci/init-submodules.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Recursive submodule init for the rexglue SDK with self-healing for broken +# upstream pins. +# +# The rexglue fork pins several nested third-party submodules (imgui, ...) to +# commits their upstreams no longer carry, so a plain +# `git submodule update --init --recursive` aborts at the first broken pin +# and leaves every later nested submodule empty (a fresh clone cannot build). +# For each nested submodule still left empty, this script fetches its origin +# and checks out the default-branch tip, which is enough to build. +# +# Run from the repository root after `git submodule update --init` (top +# level). No-op when everything initialized fine. + +set -uo pipefail + +SDK="third_party/rexglue-sdk" +if [ ! -e "$SDK/.git" ]; then + echo "init-submodules: $SDK is not initialized (run git submodule update --init first)" + exit 1 +fi + +( + cd "$SDK" + git submodule update --init --recursive --jobs 4 >/tmp/ghost_sub_init.log 2>&1 || true + + while IFS= read -r path; do + if [ ! -d "$path" ] || [ -z "$(ls -A "$path" 2>/dev/null)" ]; then + echo "init-submodules: self-healing empty submodule $path" + if (cd "$path" && git fetch --depth 1 origin >/dev/null 2>&1 && + git checkout -q FETCH_HEAD >/dev/null 2>&1); then + echo " ok: $path -> $(git -C "$path" rev-parse --short HEAD)" + else + echo " WARN: could not self-heal $path (build may fail on this dependency)" + fi + fi + done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$' | awk '{print $2}') +) + +exit 0 \ No newline at end of file diff --git a/tools/release/release.sh b/tools/release/release.sh new file mode 100755 index 0000000..0b71f29 --- /dev/null +++ b/tools/release/release.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# Build a Skate 3 recomp release from this checkout and optionally publish it +# to a Forgejo/Gitea Release. +# +# IMPORTANT: the release zip contains ONLY the recompiled binaries +# (skate3 + librexruntime.so + ghost_relay + README) - NO game files. Players +# bring their own ISO and the in-app installer extracts it. The game dump +# (default.xex + data/webkit/EAWebkit.xex) is required at BUILD TIME only, +# because codegen translates the actual Xbox 360 binary into C++ source; it +# is never included in, or uploaded with, the release. +# +# Usage: +# ./tools/release/release.sh # build this platform, no upload +# ./tools/release/release.sh --tag v1.0.0 --upload # build + publish +# ./tools/release/release.sh --game-root /path/to/game --tu /path/to/TU_12K2276_... +# +# Options: +# --tag TAG release tag/version (default: last git tag or 'dev') +# --game-root DIR extracted game dump (default: $REPO/game) +# --tu FILE optional Title Update STFS package for codegen +# --os linux|windows|macos override platform (default: host OS) +# --upload publish the zip to a Forgejo Release +# +# Env for --upload: +# GITEA_TOKEN Forgejo token with write:repository (release) scope +# FORGEJO_REPO default "ad3laid3/skate3recomp-multiplayer" +# FORGEJO_INSTANCE default "https://git.mithraic.cloud" + +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO" + +UPLOAD=0 +TAG="" +GAME_ROOT="${SKATE3_GAME_DATA_ROOT:-$REPO/game}" +TU="${SKATE3_TITLE_UPDATE_PACKAGE:-}" +OS_ARG="" + +while [ $# -gt 0 ]; do + case "$1" in + --tag) TAG="${2:?--tag needs a value}"; shift 2;; + --game-root) GAME_ROOT="$2"; shift 2;; + --tu) TU="$2"; shift 2;; + --os) OS_ARG="$2"; shift 2;; + --upload) UPLOAD=1; shift;; + -h|--help) + sed -n '2,30p' "$0"; exit 0;; + *) echo "unknown argument: $1"; exit 2;; + esac +done + +# ---- platform --------------------------------------------------------------- +UNAME_S="$(uname -s 2>/dev/null || echo Windows)" +case "${OS_ARG:-$UNAME_S}" in + linux|Linux) PRESET="linux-relwithdebinfo"; BIN="skate3"; LIB="librexruntime.so"; EXT="Linux";; + windows|Windows|MINGW*|MSYS*|CYGWIN*) + PRESET="relwithdebinfo"; BIN="skate3.exe"; LIB="librexruntime.dll"; EXT="Windows";; + macos|macOS|Darwin) PRESET="macos-relwithdebinfo"; BIN="skate3"; LIB="librexruntime.dylib"; EXT="macOS";; + *) echo "unsupported platform: ${OS_ARG:-$UNAME_S} (set --os linux|windows|macos)"; exit 2;; +esac + +if [ -z "$TAG" ]; then + TAG="$(git describe --tags --always 2>/dev/null || echo dev)" +fi +VER="${TAG#v}" +NAME="Skate3Recomp-$EXT-$VER" + +if [ ! -f "$GAME_ROOT/default.xex" ]; then + echo "ERROR: game dump not found at $GAME_ROOT (need default.xex + data/webkit/EAWebkit.xex)" + echo " It is required for codegen; it is NOT shipped in the release." + exit 1 +fi + +TU_ARGS="" +if [ -n "$TU" ]; then + if [ ! -f "$TU" ]; then echo "ERROR: title update package not found: $TU"; exit 1; fi + TU_ARGS="-DSKATE3_TITLE_UPDATE_PACKAGE=$TU" +fi + +echo "==> Release $TAG ($EXT) preset=$PRESET game=$GAME_ROOT" +echo "==> Configure" +cmake --preset "$PRESET" -DSKATE3_GAME_DATA_ROOT="$GAME_ROOT" $TU_ARGS +echo "==> Generate recompiled code" +cmake --build --preset "$PRESET" --target generate-all --parallel +echo "==> Reconfigure (generated sources)" +cmake --preset "$PRESET" -DSKATE3_GAME_DATA_ROOT="$GAME_ROOT" $TU_ARGS +echo "==> Build" +cmake --build --preset "$PRESET" --parallel + +# ---- package --------------------------------------------------------------- +OUT="out/build/$PRESET" +STAGE="$NAME" +rm -rf "$STAGE"; mkdir -p "$STAGE" +cp "$OUT/$BIN" "$STAGE/" +cp "$OUT/$LIB" "$STAGE/" 2>/dev/null || true +cp "$OUT/ghost_relay" "$STAGE/" 2>/dev/null || true +cp "$OUT/ghost_relay.exe" "$STAGE/" 2>/dev/null || true +# Title-update patches staged next to the binary when the build used the TU +if [ -f "$OUT/game/default.xexp" ]; then + mkdir -p "$STAGE/game/data/webkit" + cp "$OUT/game/default.xexp" "$STAGE/game/" + cp "$OUT/game/data/webkit/EAWebkit.xexp" "$STAGE/game/data/webkit/" 2>/dev/null || true +fi +cp README.md "$STAGE/" +rm -f "$NAME.zip" +zip -r "$NAME.zip" "$STAGE" +rm -rf "$STAGE" +echo "==> Packaged: $NAME.zip ($(du -h "$NAME.zip" | cut -f1))" +sha256sum "$NAME.zip" + +# ---- publish ---------------------------------------------------------------- +if [ "$UPLOAD" = 1 ]; then + : "${GITEA_TOKEN:?--upload needs GITEA_TOKEN}" + REPO_FULL="${FORGEJO_REPO:-ad3laid3/skate3recomp-multiplayer}" + INSTANCE="${FORGEJO_INSTANCE:-https://git.mithraic.cloud}" + AUTH="Authorization: token $GITEA_TOKEN" + echo "==> Publishing $NAME.zip to $INSTANCE/$REPO_FULL release '$TAG'" + + RELEASE_ID="$(curl -s -X POST "$INSTANCE/api/v1/repos/$REPO_FULL/releases" \ + -H "$AUTH" -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"Skate 3 Recomp $TAG\",\"target_commitish\":\"main\"}" \ + | python3 -c "import sys,json +try: print(json.load(sys.stdin).get('id','')) +except Exception: print('')")" + + if [ -z "$RELEASE_ID" ]; then + RELEASE_ID="$(curl -s "$INSTANCE/api/v1/repos/$REPO_FULL/releases/tags/$TAG" -H "$AUTH" \ + | python3 -c "import sys,json +try: print(json.load(sys.stdin).get('id','')) +except Exception: print('')")" + fi + if [ -z "$RELEASE_ID" ]; then + echo "ERROR: could not create/find release '$TAG'"; exit 1 + fi + + curl -s -X POST "$INSTANCE/api/v1/repos/$REPO_FULL/releases/$RELEASE_ID/assets" \ + -H "$AUTH" -F "attachment=@$NAME.zip" -F "name=$NAME.zip" \ + | python3 -c "import sys,json +try: + a=json.load(sys.stdin); print('Uploaded asset', a.get('name'), a.get('size'), 'bytes') +except Exception as e: print('upload response parse failed:', e)" + echo "==> Done: https://git.mithraic.cloud/$REPO_FULL/releases/tag/$TAG" +fi \ No newline at end of file