mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
56a6cfe782
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
731 lines
31 KiB
YAML
731 lines
31 KiB
YAML
# Bundled multi-platform release template for a PSXRecomp *game* repository.
|
|
#
|
|
# Install:
|
|
# mkdir -p .github/workflows
|
|
# cp psxrecomp/docs/ci/templates/game-release.yml .github/workflows/release.yml
|
|
# Then replace every YOUR_* token (or let setup_project / fill_tokens.py /
|
|
# tools/generate_ci do it). See docs/GAME_PROJECT_SETUP.md.
|
|
#
|
|
# WHAT THIS SHIPS. A compiled game. The title COMMITS its generated/ game C
|
|
# (psxrecomp_cli.py generate, run once by the developer from a legal disc);
|
|
# the framework COMMITS its OpenBIOS backend in psxrecomp/generated/
|
|
# (SCPH-1001 is local-only since #575). CI links it into the runtime and packages the
|
|
# executable with its runtime data. Players download, point it at their disc
|
|
# (and, if they want the retail BIOS, their own dump — the runtime needs the
|
|
# image's data and checks its identity), and play. Nothing is generated or
|
|
# compiled on a player's machine, and no emitters, sources, or CLI ship at
|
|
# the zip root.
|
|
#
|
|
# WHAT NEVER SHIPS. Disc images and BIOS dumps (the zip carries only the
|
|
# OpenBIOS image). The overlay shard cache: it is compiled FROM THE DISC,
|
|
# which CI does not have, so the committed static AOT shard
|
|
# (generated/overlays_static*.c) is linked and the bundled
|
|
# overlay_toolchain/ compiles anything else from the player's disc at
|
|
# runtime. Do not set PSX_PGO in CI.
|
|
#
|
|
# Shared (do not fork these into the title):
|
|
# ./psxrecomp/.github/actions/{build-emitters,fetch-toolchain}
|
|
# psxrecomp/tools/ci/{check_boot_exe,check_generated,record_pins}.sh
|
|
# psxrecomp/tools/{package_game_release,bundle_mingw_dlls}.sh
|
|
# psxrecomp/tools/release_stage.py (mod catalog + overlay toolchain staging)
|
|
#
|
|
# Title-owned:
|
|
# CMakeLists.txt flags, the thin scripts/package_release.sh wrapper
|
|
#
|
|
# Release hosts always build with PSX_HAVE_VULKAN (headers + glslc); the
|
|
# runtime still loads the ICD dynamically via SDL.
|
|
#
|
|
# workflow_dispatch: leave version empty to auto-bump patch (or choose bump).
|
|
# reuse_cached_emitters skips rebuilding psxrecomp-game/bios when the
|
|
# psxrecomp submodule SHA cache hits. See docs/ci/BUNDLED_RELEASES.md.
|
|
|
|
name: Release builds
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: >-
|
|
Release version (e.g. 0.1.0 or v0.1.0). Leave empty to auto-bump
|
|
from the latest vX.Y.Z git tag (see bump).
|
|
required: false
|
|
type: string
|
|
default: ''
|
|
bump:
|
|
description: 'When version is empty, which semver component to increment'
|
|
required: false
|
|
type: choice
|
|
options:
|
|
- patch
|
|
- minor
|
|
- major
|
|
default: patch
|
|
publish:
|
|
description: 'Create git tag vX.Y.Z and GitHub Release'
|
|
required: true
|
|
type: boolean
|
|
default: true
|
|
channel:
|
|
description: 'Release channel: public (diagnostics off) or playtest (diagnostics on, -playtest names)'
|
|
required: true
|
|
type: choice
|
|
options:
|
|
- public
|
|
- playtest
|
|
reuse_cached_emitters:
|
|
description: 'Skip emitter rebuild when build-recompiler cache hits (psxrecomp SHA)'
|
|
required: false
|
|
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 }}
|
|
reuse_emitters: ${{ steps.meta.outputs.reuse_emitters }}
|
|
steps:
|
|
- name: Checkout (tags for auto-bump)
|
|
if: github.event_name == 'workflow_dispatch'
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Normalize version / tag
|
|
id: meta
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
|
RAW="${{ github.event.inputs.version }}"
|
|
BUMP="${{ github.event.inputs.bump || 'patch' }}"
|
|
PUBLISH="${{ github.event.inputs.publish }}"
|
|
REUSE="${{ github.event.inputs.reuse_cached_emitters }}"
|
|
else
|
|
RAW="${{ github.ref_name }}"
|
|
BUMP="patch"
|
|
PUBLISH="true"
|
|
REUSE="true"
|
|
fi
|
|
RAW="$(printf '%s' "${RAW}" | tr -d '[:space:]')"
|
|
if [[ -z "${RAW}" ]]; then
|
|
# Prefer shared helper when present (optional); else inline bump.
|
|
if [[ -x psxrecomp/tools/ci/normalize_version.sh ]]; then
|
|
eval "$(bash psxrecomp/tools/ci/normalize_version.sh --next "${BUMP}" | grep -E '^(VERSION|TAG)=')"
|
|
VER="${VERSION}"
|
|
TAG="${TAG}"
|
|
else
|
|
LATEST="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1 || true)"
|
|
BASE="${LATEST#v}"
|
|
if [[ -z "${BASE}" && -f VERSION ]]; then
|
|
BASE="$(tr -d '[:space:]' <VERSION)"
|
|
BASE="${BASE#v}"
|
|
fi
|
|
BASE="${BASE:-0.0.0}"
|
|
BASE="${BASE%%[-+]*}"
|
|
IFS=. read -r MA MI PA <<<"${BASE}"
|
|
MA="${MA:-0}"; MI="${MI:-0}"; PA="${PA:-0}"
|
|
case "${BUMP}" in
|
|
major) MA=$((MA + 1)); MI=0; PA=0 ;;
|
|
minor) MI=$((MI + 1)); PA=0 ;;
|
|
*) PA=$((PA + 1)) ;;
|
|
esac
|
|
VER="${MA}.${MI}.${PA}"
|
|
TAG="v${VER}"
|
|
echo "Auto-bumped ${BUMP}: ${LATEST:-none} → ${VER}"
|
|
fi
|
|
else
|
|
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
|
|
TAG="v${VER}"
|
|
fi
|
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] && \
|
|
git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null 2>&1; then
|
|
echo "::error::tag ${TAG} already exists — bump again or pass an explicit version"
|
|
exit 1
|
|
fi
|
|
echo "version=${VER}" >>"$GITHUB_OUTPUT"
|
|
echo "tag=${TAG}" >>"$GITHUB_OUTPUT"
|
|
echo "publish=${PUBLISH}" >>"$GITHUB_OUTPUT"
|
|
echo "reuse_emitters=${REUSE}" >>"$GITHUB_OUTPUT"
|
|
echo "Using version ${VER} (tag ${TAG}), publish=${PUBLISH}, reuse_emitters=${REUSE}"
|
|
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)"
|
|
|
|
# Submodule gitlinks are the pin; this step only logs SHAs for the job.
|
|
- name: Record framework revisions
|
|
shell: bash
|
|
run: bash psxrecomp/tools/ci/record_pins.sh
|
|
|
|
# game.toml, CMakeLists GEN_MARKER and codegen_setup.c each carry the
|
|
# boot-EXE name; the committed generated/ files are named after it, so a
|
|
# skew means the build looks for C the checkout does not have.
|
|
- name: Lint boot-EXE name consistency
|
|
shell: bash
|
|
run: bash psxrecomp/tools/ci/check_boot_exe.sh .
|
|
|
|
# The release is built from COMMITTED C: the title's game C and the
|
|
# framework's BIOS backends. Fail here, at the top, when either is
|
|
# absent or untracked, or when a BIOS dump is in the tree.
|
|
- name: Verify committed game C
|
|
shell: bash
|
|
run: bash psxrecomp/tools/ci/check_generated.sh --root .
|
|
|
|
# The framework's committed BIOS backends must carry stamps that match
|
|
# the emitter fingerprint computed HERE. runtime.cmake re-checks this at
|
|
# configure (BIOS_STALE_FATAL); this step runs first and, on mismatch,
|
|
# prints every input's digest so the disagreement names a file.
|
|
- name: Verify BIOS backend stamps
|
|
shell: bash
|
|
run: bash psxrecomp/tools/ci/check_bios_stamps.sh --framework psxrecomp
|
|
|
|
- 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
|
|
mingw-w64-x86_64-vulkan-headers
|
|
mingw-w64-x86_64-shaderc
|
|
curl
|
|
zip
|
|
unzip
|
|
|
|
- name: Install deps (Linux)
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
sudo apt-get update
|
|
# X11 + xtst required when FetchContent builds SDL3 (XTEST).
|
|
sudo apt-get install -y --no-install-recommends \
|
|
build-essential cmake ninja-build pkg-config \
|
|
libsdl2-dev libgl1-mesa-dev libxtst-dev \
|
|
libx11-dev libxext-dev libxrandr-dev libxcursor-dev \
|
|
libxfixes-dev libxi-dev libxss-dev libxkbcommon-dev \
|
|
libasound2-dev libpulse-dev libudev-dev zip unzip \
|
|
libvulkan-dev glslc
|
|
|
|
- name: Install deps (macOS)
|
|
if: runner.os == 'macOS'
|
|
run: |
|
|
brew install cmake ninja pkg-config sdl3 \
|
|
vulkan-headers shaderc molten-vk \
|
|
|| brew install cmake ninja pkg-config sdl2 \
|
|
vulkan-headers shaderc molten-vk
|
|
|
|
# Headers + glslc are enough to compile PSX_HAVE_VULKAN (runtime still
|
|
# loads the ICD dynamically via SDL). Fail early if the stub path would
|
|
# be selected so releases always ship the real Vulkan backend.
|
|
- name: Verify Vulkan build tools
|
|
run: |
|
|
set -euo pipefail
|
|
command -v glslc
|
|
glslc --version || true
|
|
HDR=""
|
|
for candidate in \
|
|
/mingw64/include/vulkan/vulkan.h \
|
|
/usr/include/vulkan/vulkan.h \
|
|
/opt/homebrew/include/vulkan/vulkan.h \
|
|
/usr/local/include/vulkan/vulkan.h; do
|
|
if [[ -f "${candidate}" ]]; then
|
|
HDR="${candidate}"
|
|
break
|
|
fi
|
|
done
|
|
if [[ -z "${HDR}" ]] && command -v brew >/dev/null 2>&1; then
|
|
BREW_HDR="$(brew --prefix)/include/vulkan/vulkan.h"
|
|
if [[ -f "${BREW_HDR}" ]]; then
|
|
HDR="${BREW_HDR}"
|
|
fi
|
|
fi
|
|
if [[ -z "${HDR}" ]]; then
|
|
echo "::error::vulkan/vulkan.h not found"
|
|
exit 1
|
|
fi
|
|
echo "Found ${HDR}"
|
|
|
|
# Ubuntu 24.04 has no libsdl3-dev; Windows MSYS ships SDL2. The runtime
|
|
# therefore FetchContents the pinned SDL3 tarball. CMake's HTTP/2
|
|
# download from github.com release assets intermittently dies with
|
|
# REFUSED_STREAM (curl status 56). Prefetch with curl --http1.1 and point
|
|
# FetchContent at the extracted tree (macOS uses brew sdl3 and skips this).
|
|
- name: Cache SDL3 source
|
|
if: runner.os != 'macOS'
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: .cache/sdl3-src
|
|
key: sdl3-3.4.10-src
|
|
|
|
- name: Prefetch SDL3 source
|
|
if: runner.os != 'macOS'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
# Use repo-relative paths for tar: Windows GITHUB_WORKSPACE is D:\…
|
|
# and GNU tar treats drive-letter abs paths as remote hosts
|
|
# ("Cannot connect to D"). Prefer prefetch_sdl3.sh when present.
|
|
OUT=".cache/sdl3-src"
|
|
sdl3_env() {
|
|
local abs="${OUT}"
|
|
if command -v cygpath >/dev/null 2>&1; then
|
|
abs="$(cygpath -m "${OUT}")"
|
|
elif [[ -d "${OUT}" ]]; then
|
|
abs="$(cd "${OUT}" && pwd)"
|
|
fi
|
|
echo "PSX_SDL3_SOURCE_DIR=${abs}" >>"$GITHUB_ENV"
|
|
echo "SDL3 source ready: ${abs}"
|
|
}
|
|
if [[ -f "${OUT}/CMakeLists.txt" ]]; then
|
|
sdl3_env
|
|
echo "SDL3 source cache hit"
|
|
exit 0
|
|
fi
|
|
if [[ -f psxrecomp/tools/ci/prefetch_sdl3.sh ]]; then
|
|
chmod +x psxrecomp/tools/ci/prefetch_sdl3.sh
|
|
bash psxrecomp/tools/ci/prefetch_sdl3.sh --out-dir "${OUT}"
|
|
exit 0
|
|
fi
|
|
# Submodule may predate the helper — inline HTTP/1.1 prefetch.
|
|
echo "prefetch_sdl3.sh missing; using inline curl --http1.1"
|
|
mkdir -p .cache
|
|
VER=3.4.10
|
|
SHA=12b34280415ec8418c864408b93d008a20a6530687ee613d60bfbd20411f2785
|
|
URL="https://github.com/libsdl-org/SDL/releases/download/release-${VER}/SDL3-${VER}.tar.gz"
|
|
TAR=".cache/SDL3-${VER}.tar.gz"
|
|
curl -fsSL --http1.1 --retry 5 --retry-all-errors --retry-delay 2 -o "${TAR}" "${URL}"
|
|
echo "${SHA} ${TAR}" | sha256sum -c -
|
|
rm -rf "${OUT}.extracting"
|
|
mkdir -p "${OUT}.extracting"
|
|
# --force-local: belt-and-suspenders if a path ever gains a colon.
|
|
if tar --help 2>&1 | grep -q -- '--force-local'; then
|
|
tar --force-local -xzf "${TAR}" -C "${OUT}.extracting"
|
|
else
|
|
tar -xzf "${TAR}" -C "${OUT}.extracting"
|
|
fi
|
|
INNER="$(find "${OUT}.extracting" -mindepth 1 -maxdepth 1 -type d | head -n1)"
|
|
rm -rf "${OUT}"
|
|
mv "${INNER}" "${OUT}"
|
|
rm -rf "${OUT}.extracting"
|
|
sdl3_env
|
|
|
|
# Windows: build emitters with llvm-mingw from cmake-clang-v1 (static
|
|
# CLI). The pack is for the build machine only; it never ships.
|
|
- name: Fetch portable toolchain (Windows emitters)
|
|
if: runner.os == 'Windows'
|
|
uses: ./psxrecomp/.github/actions/fetch-toolchain
|
|
with:
|
|
artifact: ${{ matrix.artifact }}
|
|
|
|
# Emitters ship inside overlay_toolchain/ so the runtime can compile
|
|
# overlays from the player's disc (and a BIOS backend for a known image
|
|
# this build does not link). Host/UI-only bumps keep the same psxrecomp
|
|
# gitlink → cache hit skips rebuilding them (see reuse_cached_emitters).
|
|
- name: Emitter cache key
|
|
id: emitter_cache
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
SHA="$(git -C psxrecomp rev-parse HEAD)"
|
|
echo "psxrecomp_sha=${SHA}" >>"$GITHUB_OUTPUT"
|
|
echo "key=${{ runner.os }}-${{ matrix.artifact }}-emitters-${SHA}" >>"$GITHUB_OUTPUT"
|
|
echo "psxrecomp SHA ${SHA}"
|
|
|
|
- name: Cache emitters (build-recompiler)
|
|
id: cache_emitters
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: build-recompiler
|
|
key: ${{ steps.emitter_cache.outputs.key }}
|
|
|
|
- name: Build recompiler tools (emitters)
|
|
if: >-
|
|
steps.cache_emitters.outputs.cache-hit != 'true' ||
|
|
needs.prepare.outputs.reuse_emitters != 'true'
|
|
uses: ./psxrecomp/.github/actions/build-emitters
|
|
|
|
- name: Verify emitters present
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
# Restored cache or fresh build must leave both emitters in place.
|
|
test -d build-recompiler
|
|
find build-recompiler -type f \( -name 'psxrecomp-game*' -o -name 'psxrecomp-bios*' \) | head
|
|
count="$(find build-recompiler -type f \( -name 'psxrecomp-game*' -o -name 'psxrecomp-bios*' \) | wc -l | tr -d ' ')"
|
|
if [[ "${count}" -lt 2 ]]; then
|
|
echo "::error::expected psxrecomp-game + psxrecomp-bios under build-recompiler (found ${count})"
|
|
exit 1
|
|
fi
|
|
|
|
# The generated game C is tens of MB per title; ccache turns a host/UI
|
|
# bump into a link-only rebuild on every platform.
|
|
- name: Setup ccache (Linux/macOS)
|
|
if: runner.os != 'Windows'
|
|
uses: hendrikmuhs/ccache-action@v1.2
|
|
with:
|
|
key: game-${{ matrix.artifact }}
|
|
max-size: 2G
|
|
|
|
- name: Configure & build the game
|
|
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
|
|
if [[ "$(uname -s)" != MINGW* && "$(uname -s)" != MSYS* ]] && command -v ccache >/dev/null 2>&1; then
|
|
EXTRA+=("-DCMAKE_C_COMPILER_LAUNCHER=ccache")
|
|
EXTRA+=("-DCMAKE_CXX_COMPILER_LAUNCHER=ccache")
|
|
fi
|
|
# Point CMake at Vulkan headers/glslc. Prefer VULKAN_SDK layout
|
|
# (runtime.cmake checks $VULKAN_SDK first); also seed CMAKE_PREFIX_PATH
|
|
# so find_path works after the empty-_vk_inc fix in runtime.cmake.
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
BREW_PREFIX="$(brew --prefix)"
|
|
export PATH="${BREW_PREFIX}/bin${PATH:+:${PATH}}"
|
|
if command -v brew >/dev/null 2>&1; then
|
|
SHADERC_PREFIX="$(brew --prefix shaderc 2>/dev/null || true)"
|
|
VK_HDR_PREFIX="$(brew --prefix vulkan-headers 2>/dev/null || true)"
|
|
if [[ -n "${SHADERC_PREFIX}" && -d "${SHADERC_PREFIX}/bin" ]]; then
|
|
export PATH="${SHADERC_PREFIX}/bin:${PATH}"
|
|
fi
|
|
for p in "${VK_HDR_PREFIX}" "${SHADERC_PREFIX}" "${BREW_PREFIX}"; do
|
|
if [[ -n "${p}" && -d "${p}" ]]; then
|
|
export CMAKE_PREFIX_PATH="${p}${CMAKE_PREFIX_PATH:+:${CMAKE_PREFIX_PATH}}"
|
|
fi
|
|
done
|
|
else
|
|
export CMAKE_PREFIX_PATH="${BREW_PREFIX}${CMAKE_PREFIX_PATH:+:${CMAKE_PREFIX_PATH}}"
|
|
fi
|
|
if [[ -f "${BREW_PREFIX}/include/vulkan/vulkan.h" ]]; then
|
|
export VULKAN_SDK="${BREW_PREFIX}"
|
|
elif [[ -n "${VK_HDR_PREFIX:-}" && -f "${VK_HDR_PREFIX}/include/vulkan/vulkan.h" ]]; then
|
|
export VULKAN_SDK="${VK_HDR_PREFIX}"
|
|
fi
|
|
elif [[ "$(uname -s)" == "Linux" ]]; then
|
|
if [[ -z "${VULKAN_SDK:-}" && -f /usr/include/vulkan/vulkan.h ]]; then
|
|
export VULKAN_SDK=/usr
|
|
fi
|
|
elif [[ "$(uname -s)" == MINGW* || "$(uname -s)" == MSYS* ]]; then
|
|
if [[ -z "${VULKAN_SDK:-}" && -f /mingw64/include/vulkan/vulkan.h ]]; then
|
|
export VULKAN_SDK=/mingw64
|
|
fi
|
|
fi
|
|
if command -v glslc >/dev/null 2>&1; then
|
|
EXTRA+=("-DGLSLC_EXE=$(command -v glslc)")
|
|
fi
|
|
JOBS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)"
|
|
# Emitters may have set PSXRECOMP_TOOLCHAIN_DIR (llvm-mingw) via
|
|
# GITHUB_ENV. Clear it before the MSYS2 g++ configure so the pack's
|
|
# lib/ cannot win -L for -static-libstdc++ (codecvt link errors).
|
|
if [[ "$(uname -s)" == MINGW* || "$(uname -s)" == MSYS* ]]; then
|
|
unset PSXRECOMP_TOOLCHAIN_DIR TOOLCHAIN_DIR BPE_TOOLCHAIN_DIR RETCOMM_TOOLCHAIN_DIR || true
|
|
unset CMAKE_PREFIX_PATH SDL3_DIR ZLIB_ROOT || true
|
|
fi
|
|
# Full game build: the committed generated/ C is REQUIRED, so a
|
|
# checkout without it fails configure instead of silently producing
|
|
# a setup host. runtime.cmake links every BIOS backend whose C is
|
|
# committed in the submodule (OpenBIOS; SCPH-1001 is local-only), and
|
|
# BIOS_STALE_FATAL refuses a stamp that predates the emitter.
|
|
# PSX_SETUP_WIZARD stays on: it is the first-run BIOS/disc picker.
|
|
# Netplay titles: set PSX_NETPLAY ON in CMakeLists *before*
|
|
# include(runtime.cmake) (ENABLE_NETPLAY_IF_PRESENT alone is too
|
|
# late) and add -DPSX_NETPLAY=ON below as belt-and-suspenders.
|
|
# Add title-specific -D flags here if needed (e.g. -DMOTK_NATIVE=OFF).
|
|
if [[ -n "${PSX_SDL3_SOURCE_DIR:-}" && -f "${PSX_SDL3_SOURCE_DIR}/CMakeLists.txt" ]]; then
|
|
EXTRA+=("-DFETCHCONTENT_SOURCE_DIR_SDL3=${PSX_SDL3_SOURCE_DIR}")
|
|
elif [[ -f "${GITHUB_WORKSPACE}/.cache/sdl3-src/CMakeLists.txt" ]]; then
|
|
EXTRA+=("-DFETCHCONTENT_SOURCE_DIR_SDL3=${GITHUB_WORKSPACE}/.cache/sdl3-src")
|
|
fi
|
|
cmake -S . -B build-ci -G Ninja \
|
|
-DCMAKE_BUILD_TYPE=Release \
|
|
-DPSXRECOMP_REQUIRE_GAME_C=ON \
|
|
-DPSXRECOMP_BIOS_STALE_FATAL=ON \
|
|
-DPSXRECOMP_FORCE_SETUP_HOST=OFF \
|
|
-DPSXRECOMP_ALLOW_NO_BIOS=OFF \
|
|
-DPSX_SETUP_WIZARD=ON \
|
|
-DPSX_GAME_VERSION="${RELEASE_VERSION}" \
|
|
-DPSX_RELEASE_CHANNEL="${{ github.event_name == 'push' && 'public' || github.event.inputs.channel }}" \
|
|
"${EXTRA[@]}" 2>&1 | tee build-ci-configure.log
|
|
# Assert the configure took the FULL path, not the setup-host one.
|
|
if ! grep -q 'linking generated game C (full runtime)' build-ci-configure.log; then
|
|
echo "::error::CMake did not link the generated game C (see configure log)"
|
|
exit 1
|
|
fi
|
|
if ! grep -qE 'BIOS backends linked: .*OpenBIOS' build-ci-configure.log; then
|
|
echo "::error::CMake did not link the OpenBIOS backend (psxrecomp/generated missing or stale?)"
|
|
exit 1
|
|
fi
|
|
if ! grep -q 'Vulkan backend: headers' build-ci-configure.log; then
|
|
echo "::error::CMake did not enable PSX_HAVE_VULKAN (see configure log)"
|
|
exit 1
|
|
fi
|
|
# Ninja does not emit flags.make (Makefiles generator only). Probe
|
|
# whichever compile-flag artifact exists for the defines the release
|
|
# depends on.
|
|
for define in PSX_HAVE_VULKAN PSX_HAS_GAME_DISPATCH; do
|
|
FOUND=0
|
|
for f in \
|
|
build-ci/build.ninja \
|
|
build-ci/compile_commands.json \
|
|
build-ci/CMakeFiles/psx-runtime.dir/flags.make; do
|
|
if [[ -f "${f}" ]] && grep -q "${define}" "${f}"; then
|
|
FOUND=1
|
|
echo "Found ${define} in ${f}"
|
|
break
|
|
fi
|
|
done
|
|
if [[ "${FOUND}" -ne 1 ]]; then
|
|
echo "::error::${define} missing from psx-runtime compile flags"
|
|
exit 1
|
|
fi
|
|
done
|
|
cmake --build build-ci --target psx-runtime -j"${JOBS}"
|
|
# Lobby list pin must match RELEASE_VERSION (the packager enforces).
|
|
STAMP=""
|
|
for cand in \
|
|
build-ci/psx_game_version.txt \
|
|
build-ci/Release/psx_game_version.txt \
|
|
build-ci/*/psx_game_version.txt
|
|
do
|
|
if [[ -f "${cand}" ]]; then STAMP="${cand}"; break; fi
|
|
done
|
|
if [[ -z "${STAMP}" ]]; then
|
|
STAMP="$(find build-ci -name psx_game_version.txt -type f 2>/dev/null | head -n1 || true)"
|
|
fi
|
|
if [[ -z "${STAMP}" || ! -f "${STAMP}" ]]; then
|
|
echo "::error::missing psx_game_version.txt after build (update psxrecomp submodule)"
|
|
exit 1
|
|
fi
|
|
BUILT="$(tr -d '[:space:]' <"${STAMP}")"
|
|
BUILT="${BUILT#v}"
|
|
if [[ "${BUILT}" != "${RELEASE_VERSION}" ]]; then
|
|
echo "::error::lobby pin stamp ${BUILT} != RELEASE_VERSION ${RELEASE_VERSION} (${STAMP})"
|
|
exit 1
|
|
fi
|
|
echo "lobby pin ok: ${BUILT} (${STAMP})"
|
|
|
|
- name: Package game zip
|
|
# Windows code signing (Smart App Control / SmartScreen): the packager
|
|
# signs the game exe and its DLLs when these secrets exist and skips
|
|
# signing, with a notice, when they do not. Put the .pfx in the
|
|
# repo's Actions secrets base64-encoded (`base64 -w0 cert.pfx`).
|
|
# See psxrecomp/docs/ci/README.md, "Windows code signing". Because the
|
|
# shipped exe IS the game, the signature now covers what the player
|
|
# runs — there is no unsigned locally built binary any more.
|
|
env:
|
|
WINDOWS_SIGN_PFX_BASE64: ${{ secrets.WINDOWS_SIGN_PFX_BASE64 }}
|
|
WINDOWS_SIGN_PFX_PASSWORD: ${{ secrets.WINDOWS_SIGN_PFX_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
chmod +x scripts/package_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
|
|
scripts/package_release.sh build-ci "${{ matrix.artifact }}" build-recompiler
|
|
ls -la dist/
|
|
|
|
- name: Verify zip contents
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
# Assert the artifact, not the intent: every rule below was once
|
|
# "the packager does that by default" and shipped broken anyway.
|
|
ZIP=$(ls dist/YOUR_ZIP_PREFIX-*-"${{ matrix.artifact }}".zip | head -1)
|
|
echo "checking ${ZIP}"
|
|
LIST="$(unzip -Z1 "${ZIP}")"
|
|
# 1. It is a game: the executable, the bundled OpenBIOS, and the
|
|
# runtime data sit at the root.
|
|
for must in 'bios/openbios.bin' 'bios/OpenBIOS.LICENSE' 'game.toml' 'psx_game_version.txt' 'assets/fonts/' 'licenses/recomp-ui-LICENSE' 'assets/fonts/NOTICE.md' 'assets/img/NOTICE.md' 'mods/bundled/'; do
|
|
if ! grep -q "^${must}" <<<"${LIST}"; then
|
|
echo "::error::release zip lacks ${must}" >&2
|
|
printf '%s\n' "${LIST}" | head -40 >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
MANIFESTS=$(grep -c 'mods/bundled/.*/manifest\.toml' <<<"${LIST}" || true)
|
|
echo "mod manifests in zip: ${MANIFESTS}"
|
|
if [[ "${MANIFESTS}" -lt 1 ]]; then
|
|
echo "::error::release zip ships no mod catalog (mods/bundled/*/manifest.toml)" >&2
|
|
exit 1
|
|
fi
|
|
# 2. It is NOT a build kit: no framework tree, no CLI, no emitters at
|
|
# the root, no sources, no generated C.
|
|
for never in '^psxrecomp/' '^recomp-ui/' '^psxrecomp_cli.py' '^psxrecomp-game' '^psxrecomp-bios' \
|
|
'^generated/' '^seeds/' '^CMakeLists.txt' '^codegen_setup\.' '^toolchain/' '^src/'; do
|
|
if grep -qE "${never}" <<<"${LIST}"; then
|
|
echo "::error::release zip contains build-kit content matching ${never}" >&2
|
|
grep -E "${never}" <<<"${LIST}" | head -20 >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
if grep -E '\.c$' <<<"${LIST}" | grep -qv '^overlay_toolchain/'; then
|
|
echo "::error::release zip contains C sources outside overlay_toolchain/" >&2
|
|
grep -E '\.c$' <<<"${LIST}" | grep -v '^overlay_toolchain/' | head -20 >&2
|
|
exit 1
|
|
fi
|
|
# 2b. A bundled TinyCC ships its LGPL-2.1 notice beside the binary and
|
|
# in licenses/ (its win64 zip carries none of its own).
|
|
if grep -q '^overlay_toolchain/tcc/tcc\.exe$' <<<"${LIST}"; then
|
|
for must in 'overlay_toolchain/tcc/COPYING' 'licenses/TinyCC-LICENSE.txt'; do
|
|
if ! grep -qx "${must}" <<<"${LIST}"; then
|
|
echo "::error::release zip bundles TinyCC without ${must}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
fi
|
|
# 3. No BIOS dump, disc, or memory card; the only image is OpenBIOS.
|
|
if grep -iE '(^|/)SCPH|\.(cue|iso|img|chd|mcd|mcr)$' <<<"${LIST}" | grep -qv '\.toml$'; then
|
|
echo "::error::release zip contains a BIOS dump, disc, or memory card" >&2
|
|
grep -iE '(^|/)SCPH|\.(cue|iso|img|chd|mcd|mcr)$' <<<"${LIST}" | grep -v '\.toml$' >&2
|
|
exit 1
|
|
fi
|
|
if grep -iE '\.bin$' <<<"${LIST}" | grep -qv '^bios/openbios.bin$'; then
|
|
echo "::error::release zip contains a .bin other than bios/openbios.bin" >&2
|
|
grep -iE '\.bin$' <<<"${LIST}" | grep -v '^bios/openbios.bin$' >&2
|
|
exit 1
|
|
fi
|
|
# 4. Per-machine mod state never ships: state.toml is one machine's
|
|
# selections, mods/installed/ is one developer's installs, and
|
|
# developer-channel packages are unfinished work.
|
|
if grep -q 'state\.toml' <<<"${LIST}"; then
|
|
echo "::error::release zip contains mods/state.toml (per-machine state)" >&2
|
|
exit 1
|
|
fi
|
|
if grep -q 'mods/installed/' <<<"${LIST}"; then
|
|
echo "::error::release zip contains mods/installed (per-machine installs)" >&2
|
|
exit 1
|
|
fi
|
|
if unzip -p "${ZIP}" 'mods/**/manifest.toml' 2>/dev/null \
|
|
| grep -Eq '^[[:space:]]*channel[[:space:]]*=[[:space:]]*"developer"'; then
|
|
echo "::error::release zip contains developer-channel mod packages" >&2
|
|
exit 1
|
|
fi
|
|
echo "zip verified: compiled game, bundled OpenBIOS, mod catalog, no kit / dumps / dev mods"
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
# Artifact name uses the title zip prefix (e.g. bpe, motk).
|
|
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:
|
|
- name: Checkout (for tagging on workflow_dispatch)
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Download all artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
path: artifacts
|
|
merge-multiple: false
|
|
|
|
- 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 }}
|
|
# Quoted: titles may contain ':' (e.g. "Marvel vs. Capcom: …").
|
|
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 }}
|