376 lines
14 KiB
YAML
376 lines
14 KiB
YAML
# Build the Legend of Zelda: A Link to the Past SETUP PACK for Linux, Windows, macOS
|
|
# arm64 and macOS Intel, tag the commit, and publish the four zips as a
|
|
# GitHub Release.
|
|
#
|
|
# HUMAN-TRIGGERED ONLY. There is no push, pull_request or schedule trigger and
|
|
# there must never be one: a run is a release decision, made by a person on
|
|
# the Actions page. Releases across the port set are driven from the studio
|
|
# bulk tool, and a per-push run would compete with it for minutes and for the
|
|
# meaning of "did CI pass".
|
|
#
|
|
# WHAT A RUN DOES, with the defaults:
|
|
# 1. Picks the next version: the latest v* tag with its patch number bumped
|
|
# (or VERSION when there is no tag yet, or whatever `version` says).
|
|
# 2. Builds the setup host for all four platforms with that version as the
|
|
# lobby pin, and packages it.
|
|
# 3. Tags the commit the run was started from as vX.Y.Z and publishes a
|
|
# public GitHub Release with the four zips. Uncheck publish_release for a
|
|
# dry run that only uploads workflow artifacts.
|
|
#
|
|
# WHAT THIS BUILDS. Not the game. src/gen/*.c is recompiler output derived
|
|
# from ROM bytes: never committed, never in CI, never in a zip. What CI can
|
|
# honestly produce is the SETUP HOST -- the same executable built with
|
|
# -DSNESRECOMP_SETUP_HOST=ON, i.e. without recompiled code -- packaged with
|
|
# the recompiler and this source tree. On the player's machine the launcher
|
|
# takes their own ROM, generates, rebuilds, and relaunches into the real game.
|
|
#
|
|
# THE TOOLCHAIN. Each pack is built with the retcomm-toolchains cmake-clang-v1
|
|
# pack, because that is what the player's own rebuild uses: if it cannot build
|
|
# this game, CI is where that must fail. By default the pack is embedded in
|
|
# the zip (embed_toolchain) so first run downloads nothing; turn it off for a
|
|
# lean zip that fetches the pack on demand (Linux pack ~800 MB, Windows
|
|
# ~200 MB, macOS ~90 MB).
|
|
|
|
name: Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
publish_release:
|
|
description: 'Tag this commit and publish a public GitHub Release'
|
|
type: boolean
|
|
default: true
|
|
version:
|
|
description: 'Version to release, e.g. 0.2.0 (empty = bump the latest v* tag)'
|
|
type: string
|
|
default: ''
|
|
bump:
|
|
description: 'When version is empty, which part of the latest tag to bump'
|
|
type: choice
|
|
options: [patch, minor, major]
|
|
default: patch
|
|
embed_toolchain:
|
|
description: 'Embed the cmake-clang-v1 pack in each zip (else the launcher downloads it)'
|
|
type: boolean
|
|
default: true
|
|
toolchain_tag:
|
|
description: 'retcomm-toolchains release to build with (empty = latest)'
|
|
type: string
|
|
default: ''
|
|
verify_pins:
|
|
description: 'Fail if the submodules disagree with framework_pins.txt'
|
|
type: boolean
|
|
default: false
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
EXE_NAME: LegendOfZeldaALinkToThePastSNESRecomp
|
|
ZIP_PREFIX: legendofzeldaalinktothepast
|
|
|
|
jobs:
|
|
preflight:
|
|
name: Preflight
|
|
runs-on: ubuntu-24.04
|
|
outputs:
|
|
version: ${{ steps.meta.outputs.version }}
|
|
tag: ${{ steps.meta.outputs.tag }}
|
|
publish: ${{ steps.meta.outputs.publish }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
|
|
- name: Record framework pins
|
|
run: |
|
|
set -euo pipefail
|
|
ARGS=()
|
|
if [[ "${{ inputs.verify_pins }}" == "true" ]]; then
|
|
ARGS+=(--check framework_pins.txt)
|
|
fi
|
|
bash snesrecomp/tools/ci/record_pins.sh "${ARGS[@]}"
|
|
|
|
- name: Resolve release version
|
|
id: meta
|
|
env:
|
|
IN_VERSION: ${{ inputs.version }}
|
|
IN_BUMP: ${{ inputs.bump || 'patch' }}
|
|
IN_PUBLISH: ${{ inputs.publish_release }}
|
|
run: |
|
|
set -euo pipefail
|
|
PUBLISH=true
|
|
[[ "${IN_PUBLISH}" == "false" ]] && PUBLISH=false
|
|
FILE_VER="$(tr -d '[:space:]' < VERSION)"
|
|
RAW="$(printf '%s' "${IN_VERSION}" | tr -d '[:space:]')"
|
|
LATEST="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1 || true)"
|
|
|
|
if [[ -n "${RAW}" ]]; then
|
|
VER="${RAW#v}"
|
|
echo "version given: ${VER}"
|
|
elif [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
|
# Started from an existing tag: release exactly that.
|
|
VER="${GITHUB_REF_NAME#v}"
|
|
echo "running on tag ${GITHUB_REF_NAME}: releasing ${VER}"
|
|
elif [[ -z "${LATEST}" ]]; then
|
|
# First release: VERSION says what it is.
|
|
VER="${FILE_VER}"
|
|
echo "no v* tag yet: first release is VERSION = ${VER}"
|
|
else
|
|
BASE="${LATEST#v}"; BASE="${BASE%%[-+]*}"
|
|
IFS=. read -r MA MI PA <<<"${BASE}"
|
|
case "${IN_BUMP}" in
|
|
major) MA=$((MA + 1)); MI=0; PA=0 ;;
|
|
minor) MI=$((MI + 1)); PA=0 ;;
|
|
*) PA=$((PA + 1)) ;;
|
|
esac
|
|
VER="${MA}.${MI}.${PA}"
|
|
# A deliberate bump of VERSION in the tree outranks the automatic
|
|
# patch step, so a maintainer can move to 0.2.0 by editing the file.
|
|
if [[ "$(printf '%s\n%s\n' "${FILE_VER}" "${VER}" | sort -V | tail -n1)" == "${FILE_VER}" \
|
|
&& "${FILE_VER}" != "${BASE}" ]]; then
|
|
echo "VERSION (${FILE_VER}) is ahead of ${LATEST} bumped (${VER}); using VERSION"
|
|
VER="${FILE_VER}"
|
|
else
|
|
echo "auto-bump ${IN_BUMP}: ${LATEST} -> ${VER}"
|
|
fi
|
|
fi
|
|
|
|
if [[ ! "${VER}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.+-][A-Za-z0-9.+-]*)?$ ]]; then
|
|
echo "::error::'${VER}' is not a version (want X.Y.Z)"
|
|
exit 1
|
|
fi
|
|
TAG="v${VER}"
|
|
if [[ "${PUBLISH}" == "true" && "${GITHUB_REF}" != "refs/tags/${TAG}" ]] \
|
|
&& git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
|
echo "::error::tag ${TAG} already exists at $(git rev-parse --short "${TAG}"). Pass a new version, or start the run from that tag to re-release it."
|
|
exit 1
|
|
fi
|
|
|
|
echo "version=${VER}" >>"$GITHUB_OUTPUT"
|
|
echo "tag=${TAG}" >>"$GITHUB_OUTPUT"
|
|
echo "publish=${PUBLISH}" >>"$GITHUB_OUTPUT"
|
|
echo "release ${TAG} from ${GITHUB_SHA::12}; publish=${PUBLISH}"
|
|
|
|
build:
|
|
name: ${{ matrix.artifact }}
|
|
needs: preflight
|
|
runs-on: ${{ matrix.os }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- os: ubuntu-24.04
|
|
artifact: linux-x64
|
|
- os: windows-2022
|
|
artifact: windows-x64
|
|
- os: macos-15
|
|
artifact: macos-arm64
|
|
- os: macos-15-intel
|
|
artifact: macos-x64
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
env:
|
|
# The lobby pin compiled into the binary and written into the pack's
|
|
# VERSION, so the tag, the binary and the player's own rebuild agree.
|
|
RELEASE_VERSION: ${{ needs.preflight.outputs.version }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
fetch-depth: 1
|
|
|
|
- name: Record framework pins
|
|
run: bash snesrecomp/tools/ci/record_pins.sh
|
|
|
|
# A setup host is defined by what it lacks. The checkout must carry no
|
|
# generated C; if it ever does, that is committed ROM-derived output and
|
|
# the build must stop here rather than package it.
|
|
- name: Assert no generated C in the tree
|
|
run: |
|
|
set -euo pipefail
|
|
if compgen -G 'src/gen/*.c' >/dev/null || [[ -f recomp/funcs.h ]]; then
|
|
echo "::error::src/gen/*.c or recomp/funcs.h is present in the checkout."
|
|
exit 1
|
|
fi
|
|
echo "src/gen is empty — building the setup host."
|
|
|
|
- name: Cache toolchain pack
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: .cache/toolchain-pack
|
|
key: cmake-clang-v1-${{ matrix.artifact }}-${{ inputs.toolchain_tag || 'latest' }}
|
|
|
|
- name: Fetch toolchain pack
|
|
run: |
|
|
set -euo pipefail
|
|
ARGS=(--artifact "${{ matrix.artifact }}")
|
|
if [[ -n "${{ inputs.toolchain_tag }}" ]]; then
|
|
ARGS+=(--tag "${{ inputs.toolchain_tag }}")
|
|
fi
|
|
bash snesrecomp/tools/ci/fetch_toolchain.sh "${ARGS[@]}"
|
|
|
|
# The macOS pack is cmake + ninja + python; SDL3 comes from
|
|
# runner.cmake's pinned source fallback. Prefetch the tarball with curl
|
|
# (HTTP/1.1) and cache it, so configure does not download.
|
|
- name: Cache SDL3 source (macOS)
|
|
if: runner.os == 'macOS'
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: .cache/sdl3-src
|
|
key: sdl3-src-3.4.10
|
|
|
|
- name: Prefetch SDL3 source (macOS)
|
|
if: runner.os == 'macOS'
|
|
run: bash snesrecomp/tools/ci/prefetch_sdl3.sh --out-dir .cache/sdl3-src
|
|
|
|
- name: Configure and build the setup host
|
|
run: |
|
|
set -euo pipefail
|
|
# The pack's env.sh is its own statement of how to use it (PATH,
|
|
# CC/CXX, sysroot, SDL3_DIR). Source it rather than restate it.
|
|
# shellcheck disable=SC1091
|
|
source "${TOOLCHAIN_DIR}/env.sh"
|
|
echo "cmake: $(command -v cmake) ($(cmake --version | head -n1))"
|
|
echo "cc: ${CC:-$(command -v cc)}"
|
|
EXTRA=()
|
|
if [[ -n "${SNESRECOMP_SDL3_SOURCE_DIR:-}" ]]; then
|
|
EXTRA+=("-DSNESRECOMP_SDL3_SOURCE_DIR=${SNESRECOMP_SDL3_SOURCE_DIR}")
|
|
fi
|
|
if [[ "${{ runner.os }}" == "macOS" ]]; then
|
|
EXTRA+=("-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0")
|
|
fi
|
|
JOBS="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)"
|
|
cmake -S . -B build-ci -G Ninja \
|
|
-DCMAKE_BUILD_TYPE=Release \
|
|
-DSNESRECOMP_SETUP_HOST=ON \
|
|
-DSNESRECOMP_ENABLE_TRACE=OFF \
|
|
-DSNESRECOMP_BUILD_VERSION="${RELEASE_VERSION}" \
|
|
"${EXTRA[@]+"${EXTRA[@]}"}"
|
|
cmake --build build-ci -j"${JOBS}" --target "${EXE_NAME}"
|
|
|
|
- name: Package setup pack
|
|
run: |
|
|
set -euo pipefail
|
|
# The packager inspects the executable's imports and zips the
|
|
# stage; Git Bash on Windows has neither tool, the pack does.
|
|
# shellcheck disable=SC1091
|
|
source "${TOOLCHAIN_DIR}/env.sh"
|
|
ARGS=()
|
|
if [[ "${{ inputs.embed_toolchain }}" != "false" ]]; then
|
|
ARGS+=(--embed-toolchain)
|
|
fi
|
|
bash scripts/package_release.sh build-ci "${{ matrix.artifact }}" "${ARGS[@]+"${ARGS[@]}"}"
|
|
ls -la dist/
|
|
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ env.ZIP_PREFIX }}-${{ matrix.artifact }}
|
|
path: dist/${{ env.ZIP_PREFIX }}-*-${{ matrix.artifact }}.zip
|
|
if-no-files-found: error
|
|
retention-days: 7
|
|
compression-level: 0
|
|
|
|
release:
|
|
name: Tag and publish
|
|
needs: [preflight, build]
|
|
if: needs.preflight.outputs.publish == 'true'
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
submodules: recursive
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
|
|
- uses: actions/download-artifact@v4
|
|
with:
|
|
path: artifacts
|
|
|
|
- name: Collect zips
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p dist
|
|
find artifacts -type f -name "${ZIP_PREFIX}-*.zip" -exec cp -v {} dist/ \;
|
|
# Four platforms in, four zips out. A release that silently carries
|
|
# three is the failure this counts for.
|
|
count="$(find dist -name "${ZIP_PREFIX}-*.zip" | wc -l | tr -d ' ')"
|
|
if [[ "${count}" -ne 4 ]]; then
|
|
echo "::error::expected 4 platform zips, found ${count}"
|
|
ls -la dist
|
|
exit 1
|
|
fi
|
|
( cd dist && sha256sum "${ZIP_PREFIX}"-*.zip > SHA256SUMS )
|
|
cat dist/SHA256SUMS
|
|
|
|
# The tag is created here, after all four builds succeeded, so a failed
|
|
# build never leaves a version number claimed by nothing.
|
|
- name: Tag the commit
|
|
env:
|
|
TAG: ${{ needs.preflight.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
|
|
echo "tag ${TAG} already exists at $(git rev-parse --short "${TAG}") (re-release from that tag)"
|
|
else
|
|
git tag -a "${TAG}" -m "Legend of Zelda: A Link to the Past ${TAG}" "${GITHUB_SHA}"
|
|
git push origin "refs/tags/${TAG}"
|
|
echo "tagged ${GITHUB_SHA::12} as ${TAG}"
|
|
fi
|
|
|
|
- name: Release notes
|
|
env:
|
|
TAG: ${{ needs.preflight.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
# From rom_identity.txt, not a copy baked into this workflow.
|
|
ROM_FILE="$(python3 snesrecomp/tools/rom_identity.py rom_identity.txt --get rom_file)"
|
|
ROM_SHA256="$(python3 snesrecomp/tools/rom_identity.py rom_identity.txt --get expected_sha256)"
|
|
{
|
|
echo "Legend of Zelda: A Link to the Past ${TAG} — setup packs"
|
|
echo
|
|
echo "These packs do not contain the game. Each holds a setup program, the"
|
|
echo "recompiler and the source; on first run it takes your own copy of"
|
|
echo "${ROM_FILE} (SHA-256 ${ROM_SHA256}), recompiles it locally, and builds"
|
|
echo "the playable game. No ROM data is included or downloaded."
|
|
echo
|
|
if [[ "${{ inputs.embed_toolchain }}" != "false" ]]; then
|
|
echo "The portable build tools (cmake-clang-v1) are embedded in every pack."
|
|
else
|
|
echo "The portable build tools (cmake-clang-v1) are downloaded on first run."
|
|
fi
|
|
echo
|
|
echo '### Framework revisions'
|
|
echo
|
|
echo '```'
|
|
bash snesrecomp/tools/ci/record_pins.sh
|
|
echo '```'
|
|
} > notes.md
|
|
cat notes.md
|
|
|
|
- name: Publish release
|
|
uses: softprops/action-gh-release@v2
|
|
with:
|
|
tag_name: ${{ needs.preflight.outputs.tag }}
|
|
target_commitish: ${{ github.sha }}
|
|
name: "Legend of Zelda: A Link to the Past ${{ needs.preflight.outputs.tag }}"
|
|
body_path: notes.md
|
|
files: |
|
|
dist/${{ env.ZIP_PREFIX }}-*.zip
|
|
dist/SHA256SUMS
|
|
fail_on_unmatched_files: true
|
|
make_latest: true
|