mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
The site's one-liners pointed at develop, so any installer change went live to users the moment it merged, before a release. Point every installer URL at master: the site's RAW_URL and source links, the README, the scripts' own usage text, the nightly notes, and install.sh's RAW, which --auto-update re-fetches. Co-authored-by: Claude Opus 5.5 <[email protected]>
1181 lines
54 KiB
YAML
1181 lines
54 KiB
YAML
name: Release
|
|
|
|
# Three publication paths share one build matrix:
|
|
# push tag v* -> the `release` job: a real, permanent GitHub release
|
|
# push to develop -> the `nightly` job, tag `nightly`, after a 10 minute
|
|
# debounce (see the `guard` job) so a burst of merges
|
|
# builds once, for the last one
|
|
# push to release/** -> the same job, tag `prerelease` (issue #507)
|
|
# The build/vita-build/switch-build jobs below are trigger-agnostic; only the
|
|
# final publishing job differs. Neither rolling tag is named v-anything, so a
|
|
# CI-pushed tag can never re-enter the release path.
|
|
#
|
|
# The release/** lane exists because a release branch was otherwise invisible
|
|
# until it was tagged: the code most in need of 24-platform coverage got none,
|
|
# and the libretro buildbot only reacted after the tag, when a break becomes a
|
|
# hotfix rather than a fix.
|
|
#
|
|
# workflow_dispatch exists ONLY to re-publish a rolling tag, so it must be run
|
|
# with the branch set to `develop` or a `release/*` branch. GitHub defaults
|
|
# the branch picker to the repo default (`master` here), and every publishing
|
|
# job is ref-gated, so a dispatch left on the default would build all 24
|
|
# targets and publish nothing.
|
|
# The `guard` job below fails that case immediately instead of burning the
|
|
# matrix. To test-build an arbitrary branch, use c-cpp.yml — it has its own
|
|
# workflow_dispatch and runs on PRs.
|
|
on:
|
|
push:
|
|
tags: ['v*']
|
|
branches: [develop, 'release/**']
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
# Tag pushes always run to completion — a release build must never be
|
|
# cancelled. Develop pushes cancel superseded runs, because only the newest
|
|
# commit's binaries matter for a rolling prerelease and an in-flight build of
|
|
# an older tip is pure waste. The `guard` job's debounce sleep makes the cancel
|
|
# land while a burst of merges is still asleep, before any build starts.
|
|
# c-cpp.yml / acid-test.yml no longer run on develop pushes at all (their PR runs
|
|
# already covered the tree), so this is the only per-merge workflow on develop.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: ${{ github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release/') }}
|
|
|
|
jobs:
|
|
# ----------------------------------------------------------------
|
|
# Fail a pointless dispatch in seconds rather than after 24 builds.
|
|
# Only reachable via workflow_dispatch: push events always arrive on a
|
|
# ref one of the two publishing jobs accepts.
|
|
# ----------------------------------------------------------------
|
|
guard:
|
|
name: Check dispatch ref
|
|
runs-on: ${{ contains(fromJSON(vars.VJ_TEMP_RUNNERS || '{"refs":[]}').refs, github.ref) && fromJSON(vars.VJ_TEMP_RUNNERS)['ubuntu-latest'] || 'ubuntu-latest' }}
|
|
steps:
|
|
# Debounce for the rolling nightly. A burst of merges to develop would
|
|
# otherwise start one 24-target build per merge and cancel it when the next
|
|
# lands. Every build job needs this job, so the whole matrix waits here;
|
|
# a newer develop push cancels this run (concurrency below) while it is still
|
|
# asleep, at the cost of one idle ubuntu runner, and only the last push of a
|
|
# burst goes on to build. Only develop pushes wait: tags, release/** and
|
|
# manual dispatches start immediately.
|
|
- name: Wait for the develop burst to settle
|
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
|
run: sleep 600
|
|
|
|
- name: Refuse a dispatch that would publish nothing
|
|
if: ${{ github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/develop' && !startsWith(github.ref, 'refs/heads/release/') }}
|
|
run: |
|
|
# Built up in one variable because a ::error:: annotation has to be a
|
|
# single line to render as one.
|
|
# TARGETS = 22 build configs + vita-build + switch-build = 24
|
|
MSG="Run this workflow with the branch set to 'develop' or a 'release/*' branch."
|
|
MSG="$MSG The publishing jobs are ref-gated (tags v* -> release,"
|
|
MSG="$MSG develop -> nightly, release/* -> prerelease), so a dispatch"
|
|
MSG="$MSG from '${GITHUB_REF_NAME}' would build all 24 targets and"
|
|
MSG="$MSG publish nothing."
|
|
MSG="$MSG To test-build a branch, use the C/C++ CI workflow instead."
|
|
echo "::error::$MSG"
|
|
exit 1
|
|
|
|
# ----------------------------------------------------------------
|
|
# Native + cross builds (Linux/macOS/Windows/iOS/tvOS/Android/WASM)
|
|
# Each job builds with RELEASE_DEBUG_INFO=1 so we keep -g on the
|
|
# optimized binary, then extracts split debug symbols and strips the
|
|
# shipped binary.
|
|
# ----------------------------------------------------------------
|
|
build:
|
|
needs: [guard]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
config:
|
|
# Linux
|
|
- platform: linux-x86_64
|
|
artifact: virtualjaguar_libretro.so
|
|
os: ubuntu-latest
|
|
cc: gcc
|
|
cxx: g++
|
|
|
|
- platform: linux-aarch64
|
|
artifact: virtualjaguar_libretro.so
|
|
os: ubuntu-24.04-arm
|
|
cc: gcc
|
|
cxx: g++
|
|
|
|
- platform: linux-i686
|
|
artifact: virtualjaguar_libretro.so
|
|
os: ubuntu-latest
|
|
cc: 'gcc -m32'
|
|
cxx: 'g++ -m32'
|
|
multilib: true
|
|
|
|
# Raspberry Pi -- per-SoC tuned (issue #560).
|
|
#
|
|
# Cross-compiled on x86_64: -mcpu names a core to generate code FOR,
|
|
# it does not require running on one, so no Pi hardware is needed to
|
|
# produce these. The per-SoC flags live in the Makefile's rpi*
|
|
# platform blocks -- do not duplicate them here.
|
|
#
|
|
# rpi0 is deliberately absent: it is the same BCM2835/ARM1176 part
|
|
# as rpi1 and resolves to identical flags, so the rpi1 artifact
|
|
# serves both. Every other row is a genuinely distinct target.
|
|
#
|
|
# 32- and 64-bit rows for the same chip are BOTH needed: they are
|
|
# different userlands (Raspberry Pi OS ships both), and the 32-bit
|
|
# ones additionally carry -mfpu/-mfloat-abi, which aarch64 gcc
|
|
# rejects outright.
|
|
- platform: linux-rpi1-armv6
|
|
artifact: virtualjaguar_libretro.so
|
|
os: ubuntu-latest
|
|
cc: arm-linux-gnueabihf-gcc
|
|
cxx: arm-linux-gnueabihf-g++
|
|
make_extra: platform=rpi1
|
|
cross_arm: armhf
|
|
|
|
- platform: linux-rpi2-cortex-a7
|
|
artifact: virtualjaguar_libretro.so
|
|
os: ubuntu-latest
|
|
cc: arm-linux-gnueabihf-gcc
|
|
cxx: arm-linux-gnueabihf-g++
|
|
make_extra: platform=rpi2
|
|
cross_arm: armhf
|
|
|
|
- platform: linux-rpi3-cortex-a53
|
|
artifact: virtualjaguar_libretro.so
|
|
os: ubuntu-latest
|
|
cc: arm-linux-gnueabihf-gcc
|
|
cxx: arm-linux-gnueabihf-g++
|
|
make_extra: platform=rpi3
|
|
cross_arm: armhf
|
|
|
|
- platform: linux-rpi3_64-cortex-a53
|
|
artifact: virtualjaguar_libretro.so
|
|
os: ubuntu-latest
|
|
cc: aarch64-linux-gnu-gcc
|
|
cxx: aarch64-linux-gnu-g++
|
|
make_extra: platform=rpi3_64
|
|
cross_arm: arm64
|
|
|
|
- platform: linux-rpi4-cortex-a72
|
|
artifact: virtualjaguar_libretro.so
|
|
os: ubuntu-latest
|
|
cc: arm-linux-gnueabihf-gcc
|
|
cxx: arm-linux-gnueabihf-g++
|
|
make_extra: platform=rpi4
|
|
cross_arm: armhf
|
|
|
|
- platform: linux-rpi4_64-cortex-a72
|
|
artifact: virtualjaguar_libretro.so
|
|
os: ubuntu-latest
|
|
cc: aarch64-linux-gnu-gcc
|
|
cxx: aarch64-linux-gnu-g++
|
|
make_extra: platform=rpi4_64
|
|
cross_arm: arm64
|
|
|
|
- platform: linux-rpi5-cortex-a76
|
|
artifact: virtualjaguar_libretro.so
|
|
os: ubuntu-latest
|
|
cc: arm-linux-gnueabihf-gcc
|
|
cxx: arm-linux-gnueabihf-g++
|
|
make_extra: platform=rpi5
|
|
cross_arm: armhf
|
|
|
|
- platform: linux-rpi5_64-cortex-a76
|
|
artifact: virtualjaguar_libretro.so
|
|
os: ubuntu-latest
|
|
cc: aarch64-linux-gnu-gcc
|
|
cxx: aarch64-linux-gnu-g++
|
|
make_extra: platform=rpi5_64
|
|
cross_arm: arm64
|
|
|
|
# macOS
|
|
- platform: macos-arm64
|
|
artifact: virtualjaguar_libretro.dylib
|
|
os: macos-latest
|
|
cc: clang
|
|
cxx: clang++
|
|
|
|
# Cross-compiled from the macos-latest (arm64) runner via
|
|
# `clang -arch x86_64`. Avoids the increasingly scarce
|
|
# macos-13 (Intel) runner pool that left v2.2.0's release
|
|
# job queued for an hour. BLITTER_SIMD=sse2 is required
|
|
# because the Makefile's auto-detect uses host arch.
|
|
- platform: macos-x86_64
|
|
artifact: virtualjaguar_libretro.dylib
|
|
os: macos-latest
|
|
cc: clang -arch x86_64
|
|
cxx: clang++ -arch x86_64
|
|
make_extra: 'BLITTER_SIMD=sse2'
|
|
|
|
# Windows
|
|
- platform: windows-x86_64
|
|
artifact: virtualjaguar_libretro.dll
|
|
os: windows-latest
|
|
cc: gcc
|
|
cxx: g++
|
|
shell: 'msys2 {0}'
|
|
msystem: 'MINGW64'
|
|
msys2_packages: 'mingw-w64-x86_64-gcc make'
|
|
|
|
- platform: windows-i686
|
|
artifact: virtualjaguar_libretro.dll
|
|
os: windows-latest
|
|
cc: gcc
|
|
cxx: g++
|
|
shell: 'msys2 {0}'
|
|
msystem: 'MINGW32'
|
|
msys2_packages: 'mingw-w64-i686-gcc make'
|
|
|
|
# Emscripten (WebAssembly)
|
|
- platform: emscripten-wasm
|
|
artifact: virtualjaguar_libretro_emscripten.bc
|
|
os: ubuntu-latest
|
|
emscripten: true
|
|
|
|
# Android NDK
|
|
- platform: android-arm64-v8a
|
|
artifact: libs/arm64-v8a/libretro.so
|
|
os: ubuntu-latest
|
|
android: true
|
|
android_abi: 'arm64-v8a'
|
|
android_simd: 'neon'
|
|
|
|
- platform: android-armeabi-v7a
|
|
artifact: libs/armeabi-v7a/libretro.so
|
|
os: ubuntu-latest
|
|
android: true
|
|
android_abi: 'armeabi-v7a'
|
|
android_simd: 'neon'
|
|
|
|
- platform: android-x86_64
|
|
artifact: libs/x86_64/libretro.so
|
|
os: ubuntu-latest
|
|
android: true
|
|
android_abi: 'x86_64'
|
|
android_simd: 'sse2'
|
|
|
|
- platform: android-x86
|
|
artifact: libs/x86/libretro.so
|
|
os: ubuntu-latest
|
|
android: true
|
|
android_abi: 'x86'
|
|
android_simd: 'sse2'
|
|
|
|
# iOS / tvOS
|
|
- platform: ios-arm64
|
|
artifact: virtualjaguar_libretro_ios.dylib
|
|
os: macos-latest
|
|
make_platform: 'ios-arm64'
|
|
|
|
- platform: tvos-arm64
|
|
artifact: virtualjaguar_libretro_tvos.dylib
|
|
os: macos-latest
|
|
make_platform: 'tvos-arm64'
|
|
|
|
name: build-${{ matrix.config.platform }}
|
|
# Only the unsigned iOS/tvOS rows of the develop nightly lane run on the Mac Studio self-hosted
|
|
# runner. macOS rows sign + notarize (fixed-name keychain, 25-45 min notary waits); tags and
|
|
# release/** (whose ci-core intermediates C/C++ CI reuses) always build on clean hosted runners.
|
|
# Kill switch: repository variable VJ_STUDIO_RUNNER=off.
|
|
runs-on: ${{ matrix.config.os == 'macos-latest' && matrix.config.make_platform && github.ref == 'refs/heads/develop' && vars.VJ_STUDIO_RUNNER != 'off' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && fromJSON('["self-hosted","studio","vj"]') || matrix.config.os }}
|
|
|
|
defaults:
|
|
run:
|
|
shell: ${{ matrix.config.shell || 'bash' }}
|
|
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Install multilib
|
|
if: matrix.config.multilib
|
|
run: |
|
|
sudo dpkg --add-architecture i386
|
|
sudo apt-get update
|
|
sudo apt-get install -y gcc-multilib g++-multilib
|
|
|
|
# Cross toolchain for the Raspberry Pi rows. Also provides the matching
|
|
# objcopy: the host x86_64 objcopy in the packaging step below cannot
|
|
# read an ARM ELF (Ubuntu's binutils is single-target), so the strip /
|
|
# split-debug step needs the prefixed one.
|
|
- name: Install ARM cross toolchain
|
|
if: matrix.config.cross_arm
|
|
run: |
|
|
set -e
|
|
sudo apt-get update
|
|
if [ "${{ matrix.config.cross_arm }}" = armhf ]; then
|
|
sudo apt-get install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
|
|
else
|
|
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
|
|
fi
|
|
|
|
- name: Set up MSYS2
|
|
if: runner.os == 'Windows'
|
|
uses: msys2/setup-msys2@v2
|
|
with:
|
|
msystem: ${{ matrix.config.msystem || 'MINGW64' }}
|
|
update: false
|
|
install: ${{ matrix.config.msys2_packages || 'mingw-w64-x86_64-gcc make' }}
|
|
|
|
- name: Set up Emscripten
|
|
if: matrix.config.emscripten
|
|
uses: mymindstorm/setup-emsdk@v16
|
|
|
|
- name: Set up Android NDK
|
|
if: matrix.config.android
|
|
uses: nttld/setup-ndk@v1
|
|
id: setup-ndk
|
|
with:
|
|
ndk-version: r26d
|
|
|
|
- name: Build
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.make_platform }}
|
|
env:
|
|
RELEASE_DEBUG_INFO: '1'
|
|
TEST_EXPORTS: '0'
|
|
run: make -j4 CC="${{ matrix.config.cc }}" CXX="${{ matrix.config.cxx }}" ${{ matrix.config.make_extra || '' }}
|
|
|
|
- name: Build (platform)
|
|
if: matrix.config.make_platform
|
|
env:
|
|
RELEASE_DEBUG_INFO: '1'
|
|
TEST_EXPORTS: '0'
|
|
run: make -j4 platform=${{ matrix.config.make_platform }}
|
|
|
|
- name: Build (Emscripten)
|
|
if: matrix.config.emscripten
|
|
env:
|
|
RELEASE_DEBUG_INFO: '1'
|
|
TEST_EXPORTS: '0'
|
|
run: emmake make -j4 platform=emscripten
|
|
|
|
- name: Build (Android NDK)
|
|
if: matrix.config.android
|
|
env:
|
|
RELEASE_DEBUG_INFO: '1'
|
|
TEST_EXPORTS: '0'
|
|
run: bash scripts/build-ndk-ci.sh "${{ steps.setup-ndk.outputs.ndk-path }}" "${{ matrix.config.android_abi }}" "${{ matrix.config.android_simd }}"
|
|
|
|
- name: Verify compile-only Mach-O production exports
|
|
if: ${{ matrix.config.platform == 'macos-x86_64' || matrix.config.platform == 'ios-arm64' || matrix.config.platform == 'tvos-arm64' }}
|
|
run: |
|
|
LEAKS=$(nm -gU "${{ matrix.config.artifact }}" | grep -v ' _retro_' || true)
|
|
if [ -n "$LEAKS" ]; then
|
|
echo "::error::Production dylib leaked non-retro_* symbols:"
|
|
echo "$LEAKS"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Preserve unstripped compile-only core for PR validation
|
|
if: ${{ matrix.config.emscripten || matrix.config.android || matrix.config.platform == 'macos-x86_64' || matrix.config.make_platform }}
|
|
env:
|
|
TEST_EXPORTS: '0'
|
|
RELEASE_DEBUG_INFO: '1'
|
|
run: bash scripts/stage-ci-core.sh "${{ matrix.config.platform }}" "${{ matrix.config.artifact }}" ci-core
|
|
|
|
- name: Upload production CI intermediate
|
|
if: ${{ matrix.config.emscripten || matrix.config.android || matrix.config.platform == 'macos-x86_64' || matrix.config.make_platform }}
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ci-core-${{ matrix.config.platform }}
|
|
overwrite: true
|
|
path: ci-core/
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
# ----------------------------------------------------------------
|
|
# Package: emit
|
|
# dist/<core-name> (stripped, shipped binary)
|
|
# dist/<core-name>-debug.tar.gz (split debug symbols)
|
|
# The shipped binary keeps a debug-link / dSYM reference so a
|
|
# debugger can find the symbols when the .tar.gz is extracted
|
|
# alongside it.
|
|
# ----------------------------------------------------------------
|
|
- name: Package (Linux native build)
|
|
if: ${{ runner.os == 'Linux' && !matrix.config.emscripten && !matrix.config.android }}
|
|
run: |
|
|
set -e
|
|
mkdir -p dist
|
|
SRC="${{ matrix.config.artifact }}"
|
|
EXT=$(echo "$SRC" | grep -o '\.[^.]*$')
|
|
OUT="virtualjaguar_libretro-${{ matrix.config.platform }}${EXT}"
|
|
DBG="${OUT}.debug"
|
|
cp "$SRC" "dist/${OUT}"
|
|
# A cross-built ARM core needs the cross objcopy -- Ubuntu's
|
|
# binutils is single-target, so the host one cannot read an ARM ELF
|
|
# and the split-debug step would fail (or, worse, be skipped).
|
|
case "${{ matrix.config.cross_arm }}" in
|
|
armhf) OBJCOPY=arm-linux-gnueabihf-objcopy ;;
|
|
arm64) OBJCOPY=aarch64-linux-gnu-objcopy ;;
|
|
*) OBJCOPY=objcopy ;;
|
|
esac
|
|
"$OBJCOPY" --only-keep-debug "dist/${OUT}" "dist/${DBG}"
|
|
"$OBJCOPY" --strip-debug --strip-unneeded "dist/${OUT}"
|
|
( cd dist && "$OBJCOPY" --add-gnu-debuglink="${DBG}" "${OUT}" )
|
|
tar -C dist -czf "dist/virtualjaguar_libretro-${{ matrix.config.platform }}-debug.tar.gz" "${DBG}"
|
|
rm "dist/${DBG}"
|
|
|
|
- name: Package (Android)
|
|
if: matrix.config.android
|
|
run: |
|
|
set -e
|
|
mkdir -p dist
|
|
SRC="${{ matrix.config.artifact }}"
|
|
OUT="virtualjaguar_libretro-${{ matrix.config.platform }}.so"
|
|
DBG="${OUT}.debug"
|
|
cp "$SRC" "dist/${OUT}"
|
|
# NDK ships its own objcopy/strip; use the unprefixed system one
|
|
# (the artifact is already linked). llvm-objcopy works on all ABIs.
|
|
OBJCOPY="${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-objcopy"
|
|
if [ ! -x "$OBJCOPY" ]; then OBJCOPY=objcopy; fi
|
|
"$OBJCOPY" --only-keep-debug "dist/${OUT}" "dist/${DBG}"
|
|
"$OBJCOPY" --strip-debug --strip-unneeded "dist/${OUT}"
|
|
( cd dist && "$OBJCOPY" --add-gnu-debuglink="${DBG}" "${OUT}" )
|
|
tar -C dist -czf "dist/virtualjaguar_libretro-${{ matrix.config.platform }}-debug.tar.gz" "${DBG}"
|
|
rm "dist/${DBG}"
|
|
|
|
- name: Package (Emscripten)
|
|
if: matrix.config.emscripten
|
|
run: |
|
|
set -e
|
|
mkdir -p dist
|
|
SRC="${{ matrix.config.artifact }}"
|
|
OUT="virtualjaguar_libretro-${{ matrix.config.platform }}.bc"
|
|
cp "$SRC" "dist/${OUT}"
|
|
# LLVM bitcode keeps debug info inline; ship the .bc twice with
|
|
# the second copy gzipped under -debug for symmetry with the
|
|
# other platforms.
|
|
gzip -c "dist/${OUT}" > "dist/virtualjaguar_libretro-${{ matrix.config.platform }}-debug.tar.gz"
|
|
|
|
- name: Package (macOS / iOS / tvOS)
|
|
if: ${{ runner.os == 'macOS' }}
|
|
run: |
|
|
set -e
|
|
mkdir -p dist
|
|
SRC="${{ matrix.config.artifact }}"
|
|
EXT=$(echo "$SRC" | grep -o '\.[^.]*$')
|
|
OUT="virtualjaguar_libretro-${{ matrix.config.platform }}${EXT}"
|
|
cp "$SRC" "dist/${OUT}"
|
|
# dsymutil produces a .dSYM bundle next to the binary
|
|
dsymutil "dist/${OUT}" -o "dist/${OUT}.dSYM"
|
|
# Strip debug info from the shipped binary (-S = local syms,
|
|
# -x = remove non-globals); keeps externals so dlsym still works
|
|
strip -S -x "dist/${OUT}"
|
|
tar -C dist -czf "dist/virtualjaguar_libretro-${{ matrix.config.platform }}-debug.tar.gz" "${OUT}.dSYM"
|
|
rm -rf "dist/${OUT}.dSYM"
|
|
|
|
- name: Package (Windows MSYS2)
|
|
if: ${{ runner.os == 'Windows' }}
|
|
run: |
|
|
set -e
|
|
mkdir -p dist
|
|
SRC="${{ matrix.config.artifact }}"
|
|
OUT="virtualjaguar_libretro-${{ matrix.config.platform }}.dll"
|
|
DBG="${OUT}.debug"
|
|
cp "$SRC" "dist/${OUT}"
|
|
objcopy --only-keep-debug "dist/${OUT}" "dist/${DBG}"
|
|
objcopy --strip-debug --strip-unneeded "dist/${OUT}"
|
|
( cd dist && objcopy --add-gnu-debuglink="${DBG}" "${OUT}" )
|
|
tar -C dist -czf "dist/virtualjaguar_libretro-${{ matrix.config.platform }}-debug.tar.gz" "${DBG}"
|
|
rm "dist/${DBG}"
|
|
|
|
# ----------------------------------------------------------------
|
|
# cue2cdi -- standalone CUE/BIN -> DiscJuggler CDI converter
|
|
# (test/tools/cue2cdi.c, `make cue2cdi`). Shipped for the three
|
|
# common desktop platforms only; dropping the binary into dist/
|
|
# rides the existing artifact -> stage -> release/nightly flow, so
|
|
# it appears in both tagged releases and the rolling nightly and
|
|
# lands in SHA256SUMS automatically. macOS ships one universal
|
|
# (arm64 + x86_64) binary built from the arm64 job; Windows from
|
|
# the MINGW64 job.
|
|
# ----------------------------------------------------------------
|
|
- name: Build cue2cdi (shipped desktop platforms)
|
|
if: ${{ matrix.config.platform == 'linux-x86_64' || matrix.config.platform == 'macos-arm64' || matrix.config.platform == 'windows-x86_64' }}
|
|
run: |
|
|
set -e
|
|
mkdir -p dist
|
|
case "${{ matrix.config.platform }}" in
|
|
linux-x86_64)
|
|
make cue2cdi CC="${{ matrix.config.cc }}"
|
|
cp test/tools/cue2cdi dist/cue2cdi-linux-x86_64
|
|
strip dist/cue2cdi-linux-x86_64
|
|
;;
|
|
macos-arm64)
|
|
clang -O2 -Wall -std=c99 -arch arm64 -arch x86_64 \
|
|
-o dist/cue2cdi-macos-universal test/tools/cue2cdi.c
|
|
strip -S -x dist/cue2cdi-macos-universal
|
|
;;
|
|
windows-x86_64)
|
|
gcc -O2 -Wall -std=c99 -o dist/cue2cdi-windows-x86_64.exe test/tools/cue2cdi.c
|
|
strip dist/cue2cdi-windows-x86_64.exe
|
|
;;
|
|
esac
|
|
|
|
# ----------------------------------------------------------------
|
|
# macOS desktop cores and cue2cdi: Developer ID sign + notarize (#989).
|
|
# Runs after strip (it would invalidate the signature) and before the
|
|
# upload. Bare Mach-O files cannot be stapled; they are notarized in
|
|
# a zip and Gatekeeper checks the ticket online. iOS/tvOS dylibs are
|
|
# NOT signed here (Developer ID does not apply to them; Provenance
|
|
# signs them when it embeds them). Without the APPLE_* secrets the
|
|
# action only warns. Needs a macOS runner: macos-arm64 also carries
|
|
# the universal cue2cdi built above.
|
|
# ----------------------------------------------------------------
|
|
- name: Developer ID sign and notarize (macOS core${{ matrix.config.platform == 'macos-arm64' && ' + cue2cdi' || '' }})
|
|
if: ${{ matrix.config.platform == 'macos-arm64' || matrix.config.platform == 'macos-x86_64' }}
|
|
uses: ./.github/actions/macos-sign
|
|
with:
|
|
paths: |
|
|
dist/virtualjaguar_libretro-${{ matrix.config.platform }}.dylib
|
|
${{ matrix.config.platform == 'macos-arm64' && 'dist/cue2cdi-macos-universal' || '' }}
|
|
notarize: 'true'
|
|
apple-developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }}
|
|
apple-developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }}
|
|
apple-notary-key-id: ${{ secrets.APPLE_NOTARY_KEY_ID }}
|
|
apple-notary-issuer-id: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
|
|
apple-notary-key-p8-base64: ${{ secrets.APPLE_NOTARY_KEY_P8_BASE64 }}
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: virtualjaguar_libretro-${{ matrix.config.platform }}
|
|
path: dist/
|
|
if-no-files-found: error
|
|
|
|
# ----------------------------------------------------------------
|
|
# PS Vita (vitasdk) - cross-compiled .a static archive
|
|
# ----------------------------------------------------------------
|
|
vita-build:
|
|
name: build-vita
|
|
needs: [guard]
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: vitasdk/vitasdk:latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Build
|
|
env:
|
|
RELEASE_DEBUG_INFO: '1'
|
|
TEST_EXPORTS: '0'
|
|
run: make -j4 platform=vita
|
|
|
|
- name: Preserve unstripped production core for PR validation
|
|
env:
|
|
TEST_EXPORTS: '0'
|
|
RELEASE_DEBUG_INFO: '1'
|
|
run: bash scripts/stage-ci-core.sh vita virtualjaguar_libretro_vita.a ci-core
|
|
|
|
- name: Upload production CI intermediate
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ci-core-vita
|
|
overwrite: true
|
|
path: ci-core/
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
- name: Package
|
|
run: |
|
|
set -e
|
|
mkdir -p dist
|
|
OUT="virtualjaguar_libretro-vita.a"
|
|
cp virtualjaguar_libretro_vita.a "dist/${OUT}"
|
|
# Static archive — extract debug info via objcopy on the archive
|
|
# itself. arm-vita-eabi-objcopy is in the vitasdk image.
|
|
OBJCOPY=arm-vita-eabi-objcopy
|
|
"$OBJCOPY" --only-keep-debug "dist/${OUT}" "dist/${OUT}.debug" || cp "dist/${OUT}" "dist/${OUT}.debug"
|
|
"$OBJCOPY" --strip-debug "dist/${OUT}" || true
|
|
tar -C dist -czf "dist/virtualjaguar_libretro-vita-debug.tar.gz" "${OUT}.debug"
|
|
rm "dist/${OUT}.debug"
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: virtualjaguar_libretro-vita
|
|
path: dist/
|
|
if-no-files-found: error
|
|
|
|
# ----------------------------------------------------------------
|
|
# Nintendo Switch (devkitPro libnx) - .a static archive
|
|
# ----------------------------------------------------------------
|
|
switch-build:
|
|
name: build-switch
|
|
needs: [guard]
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: devkitpro/devkita64:latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Build
|
|
env:
|
|
DEVKITPRO: /opt/devkitpro
|
|
RELEASE_DEBUG_INFO: '1'
|
|
TEST_EXPORTS: '0'
|
|
run: make -j4 platform=libnx
|
|
|
|
- name: Preserve unstripped production core for PR validation
|
|
env:
|
|
TEST_EXPORTS: '0'
|
|
RELEASE_DEBUG_INFO: '1'
|
|
run: bash scripts/stage-ci-core.sh switch virtualjaguar_libretro_libnx.a ci-core
|
|
|
|
- name: Upload production CI intermediate
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ci-core-switch
|
|
overwrite: true
|
|
path: ci-core/
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
- name: Package
|
|
run: |
|
|
set -e
|
|
mkdir -p dist
|
|
OUT="virtualjaguar_libretro-switch.a"
|
|
cp virtualjaguar_libretro_libnx.a "dist/${OUT}"
|
|
OBJCOPY=/opt/devkitpro/devkitA64/bin/aarch64-none-elf-objcopy
|
|
"$OBJCOPY" --only-keep-debug "dist/${OUT}" "dist/${OUT}.debug" || cp "dist/${OUT}" "dist/${OUT}.debug"
|
|
"$OBJCOPY" --strip-debug "dist/${OUT}" || true
|
|
tar -C dist -czf "dist/virtualjaguar_libretro-switch-debug.tar.gz" "${OUT}.debug"
|
|
rm "dist/${OUT}.debug"
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: virtualjaguar_libretro-switch
|
|
path: dist/
|
|
if-no-files-found: error
|
|
|
|
# ----------------------------------------------------------------
|
|
# Desktop jagcd tools (pinned chdman + jagcd-to-chd + jagcd-chd-check).
|
|
# Same three hosts as cue2cdi. Runs in parallel with the core matrix;
|
|
# the release/nightly jobs wait on it so the zips land in SHA256SUMS
|
|
# and on the GitHub release next to the cores.
|
|
# ----------------------------------------------------------------
|
|
jagcd-tools:
|
|
name: jagcd tools (chdman)
|
|
needs: [guard]
|
|
uses: ./.github/workflows/chdman-tools.yml
|
|
secrets: inherit
|
|
|
|
# ----------------------------------------------------------------
|
|
# Standalone app packages (#839): macOS zip, Windows zip/setup,
|
|
# Linux AppImage/deb. One reusable workflow also used by app.yml for
|
|
# PRs. It uploads only final, contract-named files as `app-package-*`
|
|
# artifacts, so the release/nightly jobs' flatten-and-checksum steps
|
|
# attach them next to the cores with no further logic. On a tag whose
|
|
# version is not the app's (a core-only release) it builds nothing.
|
|
# ----------------------------------------------------------------
|
|
app-packages:
|
|
name: App packages
|
|
needs: [guard]
|
|
uses: ./.github/workflows/app-package.yml
|
|
with:
|
|
release_tag: ${{ github.ref_type == 'tag' && github.ref_name || (github.ref == 'refs/heads/develop' && 'nightly' || 'prerelease') }}
|
|
sign: true
|
|
secrets: inherit
|
|
|
|
# ----------------------------------------------------------------
|
|
# Release: collect all artifacts, generate SHA256SUMS, build the
|
|
# release body from docs/RELEASE_NOTES_v<TAG>.md (artifact list
|
|
# and embedded SHA256 checksums always appended), then create the
|
|
# tagged release. See SECURITY.md for binary verification docs.
|
|
# ----------------------------------------------------------------
|
|
release:
|
|
needs: [build, vita-build, switch-build, jagcd-tools, app-packages]
|
|
# Tag pushes only. Everything below assumes GITHUB_REF_NAME is a vX.Y.Z
|
|
# tag (release-notes lookup, release title, `gh release create <TAG>`), so
|
|
# this guard is what keeps a develop push from creating a release literally
|
|
# named "develop". The nightly job handles that case instead.
|
|
#
|
|
# The status checks spell out what the implicit success() gate meant before
|
|
# app-packages existed (every need succeeded), with one addition: the app
|
|
# job may be `skipped`, which is what a tag without an app to package
|
|
# produces. A FAILED app job still blocks the release, like a failed
|
|
# tools build: re-run the failed jobs and the release follows.
|
|
if: |
|
|
startsWith(github.ref, 'refs/tags/v')
|
|
&& !cancelled()
|
|
&& needs.build.result == 'success'
|
|
&& needs.vita-build.result == 'success'
|
|
&& needs.switch-build.result == 'success'
|
|
&& needs.jagcd-tools.result == 'success'
|
|
&& (needs.app-packages.result == 'success' || needs.app-packages.result == 'skipped')
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Download all artifacts
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
path: artifacts/
|
|
|
|
- name: Stage release files
|
|
run: |
|
|
set -e
|
|
mkdir -p release
|
|
# Flatten one level: each artifact directory contains the
|
|
# binary + matching -debug.tar.gz; copy them all into release/.
|
|
# librashader-<triple> are intermediates of the app packages (the shader runtime built
|
|
# from source, librashader.yml): already inside the packages, not release assets.
|
|
find artifacts/ -mindepth 2 -type f -not -path 'artifacts/librashader-*' -not -path 'artifacts/ci-core-*' -exec cp {} release/ \;
|
|
# Include the RetroArch core .info file so frontend maintainers
|
|
# / users can drop it into RetroArch's info/ dir. The same file
|
|
# is what gets PR'd to libretro/libretro-super after the release.
|
|
if [ -f dist/info/virtualjaguar_libretro.info ]; then
|
|
cp dist/info/virtualjaguar_libretro.info release/
|
|
fi
|
|
echo "==> Staged release files:"
|
|
ls -la release/
|
|
|
|
- name: Generate SHA256SUMS
|
|
run: |
|
|
set -e
|
|
cd release
|
|
# Deterministic ordering, two-space format compatible with
|
|
# `sha256sum -c SHA256SUMS.txt` on Linux + Mac (with coreutils).
|
|
#
|
|
# Build it OUTSIDE the directory and move it in. Redirecting straight
|
|
# to ./SHA256SUMS.txt races: the shell starts both pipeline stages at
|
|
# once, so `sort`'s redirect can create the (empty) file before
|
|
# `sha256sum`'s glob expands, leaving the manifest listing itself with
|
|
# the empty-file hash — which then fails `sha256sum --check` on that
|
|
# one line. v2.3.2 happened to win the race; nightlies would not.
|
|
sha256sum -- * | sort -k 2 > "$RUNNER_TEMP/SHA256SUMS.txt"
|
|
mv "$RUNNER_TEMP/SHA256SUMS.txt" SHA256SUMS.txt
|
|
echo "==> SHA256SUMS.txt:"
|
|
cat SHA256SUMS.txt
|
|
|
|
- name: Prepare release body
|
|
id: prep_body
|
|
run: |
|
|
set -e
|
|
TAG="${GITHUB_REF_NAME}"
|
|
NOTES_FILE="docs/RELEASE_NOTES_${TAG}.md"
|
|
BODY_FILE="$RUNNER_TEMP/release_body.md"
|
|
if [ -f "$NOTES_FILE" ]; then
|
|
echo "==> Using curated release notes: $NOTES_FILE"
|
|
cp "$NOTES_FILE" "$BODY_FILE"
|
|
else
|
|
echo "==> No curated $NOTES_FILE found; generating notes from commits"
|
|
gh api repos/${{ github.repository }}/releases/generate-notes \
|
|
-f tag_name="${TAG}" --jq '.body' > "$BODY_FILE" 2>/dev/null || true
|
|
fi
|
|
# Append artifact list and embedded SHA256 checksums so users can
|
|
# verify binaries directly from the release page without needing to
|
|
# download SHA256SUMS.txt first. See SECURITY.md for details.
|
|
{
|
|
echo
|
|
echo "---"
|
|
echo
|
|
echo "## Artifacts in this release"
|
|
echo
|
|
echo '```'
|
|
find release -mindepth 1 -maxdepth 1 -type f -printf "%f\n" | sort
|
|
echo '```'
|
|
echo
|
|
echo "## SHA-256 checksums"
|
|
echo
|
|
echo "Verify with \`sha256sum --check SHA256SUMS.txt\` (Linux) or"
|
|
echo "\`shasum -a 256 --check SHA256SUMS.txt\` (macOS)."
|
|
echo "See [SECURITY.md](https://github.com/${{ github.repository }}/blob/${{ github.event.repository.default_branch }}/SECURITY.md)"
|
|
echo "for full verification instructions and antivirus false-positive guidance."
|
|
echo
|
|
echo '```'
|
|
cat release/SHA256SUMS.txt
|
|
echo '```'
|
|
} >> "$BODY_FILE"
|
|
echo "body_file=$BODY_FILE" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Create release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
TAG="${GITHUB_REF_NAME}"
|
|
gh release create "${TAG}" \
|
|
--title "${TAG}" \
|
|
--notes-file "${{ steps.prep_body.outputs.body_file }}" \
|
|
release/*
|
|
|
|
# ----------------------------------------------------------------
|
|
# Nightly: same artifacts, published to a single rolling `nightly`
|
|
# prerelease that is moved onto each new develop tip, plus a pinned
|
|
# tracking issue that always shows the current build.
|
|
#
|
|
# Gating: `needs` covers the build jobs only, so this publishes a
|
|
# commit that COMPILES on all 24 targets. It does NOT wait on
|
|
# c-cpp.yml / acid-test.yml / regression-test.yml, which run
|
|
# independently on the same push — a commit that builds but fails a
|
|
# test can still ship a nightly. The release body says so; do not
|
|
# upgrade that wording to "passes all CI" without also switching this
|
|
# to a `workflow_run` trigger (see artifacts.yml for that idiom).
|
|
# ----------------------------------------------------------------
|
|
nightly:
|
|
name: Update rolling prerelease
|
|
needs: [build, vita-build, switch-build, jagcd-tools, app-packages]
|
|
# The app packages (app-packages) are waited for and not required either:
|
|
# a macOS notarization outage must not withhold the core nightly.
|
|
# Wait for the tools build, but do not REQUIRE it. chdman is an
|
|
# auxiliary desktop helper; the nightly's product is the 24 core
|
|
# binaries. Gating the publish on it meant a missing pkg-config
|
|
# package on a runner image silently withheld three days of good core
|
|
# builds (2026-08-18). Tagged releases still require it below, because
|
|
# there the tools zips are part of the deliverable.
|
|
if: |
|
|
always()
|
|
&& (github.ref == 'refs/heads/develop'
|
|
|| startsWith(github.ref, 'refs/heads/release/'))
|
|
&& needs.build.result == 'success'
|
|
&& needs.vita-build.result == 'success'
|
|
&& needs.switch-build.result == 'success'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write # move the tag, create/update the release
|
|
issues: write # update + pin the tracking issue
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
# One rolling tag per lane. Deliberately NOT a per-branch or per-PR
|
|
# tag that gets cleaned up on release: one rolling tag has a lifecycle
|
|
# the project already understands, needs no teardown step anyone can
|
|
# forget, and cannot orphan tags when a release branch is renamed or
|
|
# abandoned (issue #507). Neither name matches the v* filter, so a
|
|
# CI-pushed tag can never re-enter the release path.
|
|
TAG: ${{ github.ref == 'refs/heads/develop' && 'nightly' || 'prerelease' }}
|
|
LANE: ${{ github.ref == 'refs/heads/develop' && 'Nightly' || 'Release candidate' }}
|
|
# Must match issue #236's title exactly (including the emoji / [Release]
|
|
# prefix added by hand). Label lookup is preferred; this is the
|
|
# fallback if the nightly-tracker label is ever removed.
|
|
ISSUE_TITLE: '🐈⬛ [Release] Nightly builds — download, install, and what they are'
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
# Full history: the release body diffs against the commit the
|
|
# `nightly` tag pointed at on the previous run.
|
|
fetch-depth: 0
|
|
|
|
- name: Download all artifacts
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
path: artifacts/
|
|
|
|
- name: Stage release files
|
|
run: |
|
|
set -e
|
|
mkdir -p release
|
|
# (librashader-<triple> are intermediates of the app packages, not release assets.)
|
|
find artifacts/ -mindepth 2 -type f -not -path 'artifacts/librashader-*' -not -path 'artifacts/ci-core-*' -exec cp {} release/ \;
|
|
# The .info file's display_version names the last TAGGED release, so
|
|
# shipping it verbatim would label a nightly "v2.3.2". Rewrite it to
|
|
# the build's actual identity — this also makes it obvious that a
|
|
# nightly .info is not the file to PR to libretro-super.
|
|
if [ -f dist/info/virtualjaguar_libretro.info ]; then
|
|
sed "s/^display_version = \".*\"/display_version = \"$(sed -n 's/^CORE_BASE_VERSION[[:space:]]*:*=[[:space:]]*\(.*\)/\1/p' Makefile | head -1)-nightly-${GITHUB_SHA:0:7}\"/" \
|
|
dist/info/virtualjaguar_libretro.info > release/virtualjaguar_libretro.info
|
|
fi
|
|
echo "==> Staged nightly files:"
|
|
ls -la release/
|
|
|
|
- name: Generate SHA256SUMS
|
|
run: |
|
|
set -e
|
|
cd release
|
|
# Written outside the directory then moved in — see the release job's
|
|
# copy of this step for why the obvious redirect races.
|
|
sha256sum -- * | sort -k 2 > "$RUNNER_TEMP/SHA256SUMS.txt"
|
|
mv "$RUNNER_TEMP/SHA256SUMS.txt" SHA256SUMS.txt
|
|
cat SHA256SUMS.txt
|
|
|
|
- name: Prepare rolling release body
|
|
id: prep_body
|
|
run: |
|
|
set -e
|
|
SHA="${GITHUB_SHA}"
|
|
SHORT_SHA="${SHA:0:7}"
|
|
BODY_FILE="$RUNNER_TEMP/rolling_body.md"
|
|
RUN_URL="${GITHUB_SERVER_URL}/${REPO}/actions/runs/${GITHUB_RUN_ID}"
|
|
BASE_VERSION=$(sed -n 's/^CORE_BASE_VERSION[[:space:]]*:*=[[:space:]]*\(.*\)/\1/p' Makefile | head -1)
|
|
|
|
# Where the rolling tag pointed BEFORE this run — captured here
|
|
# because the next step moves it.
|
|
PREV_SHA=$(gh api "repos/${REPO}/git/refs/tags/${TAG}" --jq '.object.sha' 2>/dev/null || echo "")
|
|
|
|
CHANGES=""
|
|
if [ -n "$PREV_SHA" ] && [ "$PREV_SHA" != "$SHA" ] \
|
|
&& git cat-file -e "${PREV_SHA}^{commit}" 2>/dev/null; then
|
|
CHANGES=$(git log --oneline --no-merges "${PREV_SHA}..${SHA}" | head -40)
|
|
fi
|
|
[ -n "$CHANGES" ] || CHANGES=$(git log --oneline --no-merges -10)
|
|
|
|
{
|
|
echo "### ${LANE} \`${SHORT_SHA}\` — \`${BASE_VERSION}\` (\`${GITHUB_REF_NAME}\`)"
|
|
echo
|
|
echo "| | |"
|
|
echo "|---|---|"
|
|
echo "| **Commit** | [\`${SHORT_SHA}\`](${GITHUB_SERVER_URL}/${REPO}/commit/${SHA}) |"
|
|
echo "| **Built** | $(date -u '+%Y-%m-%d %H:%M UTC') |"
|
|
echo "| **CI run** | [build log](${RUN_URL}) |"
|
|
echo "| **Branch** | \`${GITHUB_REF_NAME}\` |"
|
|
echo
|
|
P="> Automatic build of the \`${GITHUB_REF_NAME}\` tip, replaced on every push."
|
|
P="$P It compiled on all 24 targets; it has **not** been through the"
|
|
P="$P test suite or any manual QA. For a stable build use the"
|
|
P="$P [latest release](${GITHUB_SERVER_URL}/${REPO}/releases/latest)."
|
|
if [ "${TAG}" = "prerelease" ]; then
|
|
P="$P This is a RELEASE CANDIDATE from a release branch, not a release:"
|
|
P="$P it exists so the 24-platform matrix runs on the code about to"
|
|
P="$P ship, instead of first meeting it at the tag."
|
|
fi
|
|
echo "$P"
|
|
echo
|
|
echo "The core logs its identity as \`${BASE_VERSION} ${SHORT_SHA}\` at startup — quote that line when reporting a bug against this build."
|
|
echo
|
|
echo "### Changes since the previous ${LANE,,}"
|
|
echo
|
|
echo '```'
|
|
echo "${CHANGES}"
|
|
echo '```'
|
|
echo
|
|
echo "### Artifacts"
|
|
echo
|
|
echo '```'
|
|
find release -mindepth 1 -maxdepth 1 -type f -printf "%f\n" | sort
|
|
echo '```'
|
|
echo
|
|
echo "Each platform ships alongside a \`-debug.tar.gz\` of its split debug symbols; extract it next to the binary to get symbolised backtraces."
|
|
echo
|
|
echo "### SHA-256 checksums"
|
|
echo
|
|
P="Verify with \`sha256sum --check SHA256SUMS.txt\` (Linux) or"
|
|
P="$P \`shasum -a 256 --check SHA256SUMS.txt\` (macOS). See"
|
|
P="$P [SECURITY.md](${GITHUB_SERVER_URL}/${REPO}/blob/${{ github.event.repository.default_branch }}/SECURITY.md)."
|
|
echo "$P"
|
|
echo
|
|
echo '```'
|
|
cat release/SHA256SUMS.txt
|
|
echo '```'
|
|
} > "$BODY_FILE"
|
|
echo "body_file=$BODY_FILE" >> "$GITHUB_OUTPUT"
|
|
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Move the rolling tag
|
|
run: |
|
|
set -e
|
|
# -F (typed) for force so it serialises as a JSON boolean; -f would
|
|
# send the string "true", which the refs API rejects with 422.
|
|
gh api "repos/${REPO}/git/refs/tags/${TAG}" \
|
|
-X PATCH -f sha="${GITHUB_SHA}" -F force=true >/dev/null 2>&1 || \
|
|
gh api "repos/${REPO}/git/refs" \
|
|
-X POST -f ref="refs/tags/${TAG}" -f sha="${GITHUB_SHA}" >/dev/null
|
|
|
|
- name: Publish rolling prerelease
|
|
run: |
|
|
set -e
|
|
TITLE="${LANE} ${{ steps.prep_body.outputs.short_sha }}"
|
|
# Update in place rather than delete-and-recreate: asset names are
|
|
# deterministic per platform, so --clobber replaces them without ever
|
|
# leaving /releases/tag/nightly 404ing mid-run, and the release keeps
|
|
# its id (so anyone watching it stays subscribed).
|
|
if gh release view "${TAG}" --repo "${REPO}" >/dev/null 2>&1; then
|
|
gh release edit "${TAG}" --repo "${REPO}" \
|
|
--title "${TITLE}" \
|
|
--notes-file "${{ steps.prep_body.outputs.body_file }}" \
|
|
--target "${GITHUB_SHA}" \
|
|
--prerelease --draft=false
|
|
gh release upload "${TAG}" --repo "${REPO}" release/* --clobber
|
|
# `gh release edit` leaves published_at at the original create
|
|
# date, so the Releases list kept showing "Published Aug 1" while
|
|
# assets/body moved every develop push — looking abandoned under
|
|
# newer stable tags. Draft→publish is the only API that rewrites
|
|
# that timestamp; assets stay attached and the id is unchanged.
|
|
# Window is one round-trip.
|
|
REL_ID=$(gh api "repos/${REPO}/releases/tags/${TAG}" --jq .id)
|
|
gh api -X PATCH "repos/${REPO}/releases/${REL_ID}" -F draft=true >/dev/null
|
|
gh api -X PATCH "repos/${REPO}/releases/${REL_ID}" \
|
|
-F draft=false -F prerelease=true >/dev/null
|
|
else
|
|
gh release create "${TAG}" --repo "${REPO}" \
|
|
--title "${TITLE}" \
|
|
--notes-file "${{ steps.prep_body.outputs.body_file }}" \
|
|
--prerelease \
|
|
--target "${GITHUB_SHA}" \
|
|
release/*
|
|
fi
|
|
|
|
- name: Update pinned tracking issue
|
|
# develop only: issue #236 documents the NIGHTLY, and its title, label
|
|
# and prose are all nightly-specific. A release-candidate publish has
|
|
# no tracking issue and must not rewrite that one (issue #507).
|
|
if: github.ref == 'refs/heads/develop'
|
|
# Never fail the publish over the issue: the binaries are already out.
|
|
continue-on-error: true
|
|
env:
|
|
# The app section is listed only when this nightly actually carries
|
|
# app packages (the job skips commits without app/, and a failed
|
|
# package build does not block the core nightly).
|
|
APP_BUILT: ${{ needs.app-packages.result == 'success' }}
|
|
run: |
|
|
set -e
|
|
SHORT_SHA="${{ steps.prep_body.outputs.short_sha }}"
|
|
BODY_FILE="$RUNNER_TEMP/issue_body.md"
|
|
{
|
|
echo "## Latest nightly"
|
|
echo
|
|
echo "| | |"
|
|
echo "|---|---|"
|
|
echo "| **Build** | [\`${SHORT_SHA}\`](${GITHUB_SERVER_URL}/${REPO}/commit/${GITHUB_SHA}) |"
|
|
echo "| **Built** | $(date -u '+%Y-%m-%d %H:%M UTC') |"
|
|
echo "| **Download** | [\`nightly\` release](${GITHUB_SERVER_URL}/${REPO}/releases/tag/nightly) |"
|
|
echo "| **CI run** | [build log](${GITHUB_SERVER_URL}/${REPO}/actions/runs/${GITHUB_RUN_ID}) |"
|
|
if [ "$APP_BUILT" = "true" ]; then
|
|
echo "| **App** | [Virtual Jaguar desktop app](#the-virtual-jaguar-app-nightly) (macOS, Windows, Linux) |"
|
|
fi
|
|
echo
|
|
if [ "$APP_BUILT" = "true" ]; then
|
|
DL="${GITHUB_SERVER_URL}/${REPO}/releases/download/nightly"
|
|
RAW="https://raw.githubusercontent.com/${REPO}/master/tools"
|
|
echo "### The Virtual Jaguar app (nightly)"
|
|
echo
|
|
echo "The standalone desktop app, built from this same commit. These links always point at the newest nightly."
|
|
echo
|
|
echo "| System | Download |"
|
|
echo "|---|---|"
|
|
echo "| macOS (Apple Silicon + Intel) | [VirtualJaguar-nightly-macos-universal.zip](${DL}/VirtualJaguar-nightly-macos-universal.zip) |"
|
|
echo "| Windows x64 | [portable .zip](${DL}/VirtualJaguar-nightly-windows-x64.zip) · [installer .exe](${DL}/VirtualJaguar-nightly-windows-x64-setup.exe) |"
|
|
echo "| Windows ARM64 | [portable .zip](${DL}/VirtualJaguar-nightly-windows-arm64.zip) · [installer .exe](${DL}/VirtualJaguar-nightly-windows-arm64-setup.exe) |"
|
|
echo "| Linux x86_64 | [AppImage](${DL}/VirtualJaguar-nightly-linux-x86_64.AppImage) · [.deb](${DL}/virtualjaguar_nightly_amd64.deb) |"
|
|
echo "| Linux aarch64 | [AppImage](${DL}/VirtualJaguar-nightly-linux-aarch64.AppImage) · [.deb](${DL}/virtualjaguar_nightly_arm64.deb) |"
|
|
echo "| Linux x86_64 (Flatpak) | [.flatpak](${DL}/VirtualJaguar-nightly-linux-x86_64.flatpak) |"
|
|
echo
|
|
echo "Or install / update it with one line (pass \`--what both\` for the app and the RetroArch core):"
|
|
echo
|
|
echo '```sh'
|
|
echo "curl -fsSL ${RAW}/install.sh | sh -s -- --what app --channel nightly"
|
|
echo '```'
|
|
echo
|
|
echo '```powershell'
|
|
echo "\$env:VJ_WHAT='app'; \$env:VJ_CHANNEL='nightly'; irm ${RAW}/install.ps1 | iex"
|
|
echo '```'
|
|
echo
|
|
P="Nightly macOS builds (the app, the cores and the command-line tools) are Developer ID"
|
|
P="$P signed and notarized by Apple when the release signing secrets are configured;"
|
|
P="$P without them macOS needs right-click > Open the first time."
|
|
P="$P Windows binaries are not code-signed yet, so SmartScreen may warn."
|
|
echo "$P"
|
|
echo
|
|
echo "---"
|
|
echo
|
|
fi
|
|
echo "---"
|
|
echo
|
|
echo "### What these are"
|
|
echo
|
|
P="Every push to \`develop\` rebuilds the core for all 24 supported platforms"
|
|
P="$P and replaces the single"
|
|
P="$P [\`nightly\`](${GITHUB_SERVER_URL}/${REPO}/releases/tag/nightly) prerelease."
|
|
P="$P There is only ever one nightly — the current tip."
|
|
echo "$P"
|
|
echo
|
|
echo "| | Stable | Nightly |"
|
|
echo "|---|---|---|"
|
|
echo "| Source | tagged \`vX.Y.Z\` on \`master\` | every push to \`develop\` |"
|
|
echo "| Gate | full CI + release checklist | compiles on all 24 targets |"
|
|
echo "| Frequency | periodic | continuous |"
|
|
echo "| Use it for | playing | testing a fix before it ships |"
|
|
echo
|
|
P="Nightlies are **not** gated on the test suite, the acid suite, or the"
|
|
P="$P screenshot regression run — those workflows report separately on the"
|
|
P="$P same commit. A nightly can build cleanly and still be broken."
|
|
echo "$P"
|
|
echo
|
|
echo "### Installing"
|
|
echo
|
|
echo "1. Download \`virtualjaguar_libretro-<platform>.<ext>\` for your system from the [\`nightly\` release](${GITHUB_SERVER_URL}/${REPO}/releases/tag/nightly)."
|
|
P="2. **Rename it**, dropping the \`-<platform>\` part —"
|
|
P="$P \`virtualjaguar_libretro-linux-x86_64.so\` becomes"
|
|
P="$P \`virtualjaguar_libretro.so\`. RetroArch pairs a core with its metadata"
|
|
P="$P by exact basename, so a core left under the download name loads with"
|
|
P="$P no name, no version and no core options."
|
|
echo "$P"
|
|
echo "3. Drop it into RetroArch's \`cores/\` directory, replacing the existing \`virtualjaguar_libretro.<ext>\`."
|
|
echo "4. Copy \`virtualjaguar_libretro.info\` into RetroArch's \`info/\` directory to see the nightly version string in the core list."
|
|
echo
|
|
echo "RetroArch's own Core Updater always serves the **stable** buildbot core. Installing a nightly by hand means the updater may overwrite it later."
|
|
echo
|
|
echo "### Reporting a bug in a nightly"
|
|
echo
|
|
echo "The core logs its exact identity at startup:"
|
|
echo
|
|
echo '```'
|
|
echo "Virtual Jaguar vX.Y.Z <gitrev>"
|
|
echo '```'
|
|
echo
|
|
P="Include that line — it is the only way to tell which nightly you ran."
|
|
P="$P A RetroArch log is worth more than a description: the core has a"
|
|
P="$P watchdog that names the failing subsystem (\`gpu_pc_escape\`,"
|
|
P="$P \`cd_seek_wedge\`, \`video_stall\`, …) at the moment it breaks."
|
|
echo "$P"
|
|
echo
|
|
echo "### Verifying a download"
|
|
echo
|
|
P="Each release lists SHA-256 checksums, and every platform has a matching"
|
|
P="$P \`-debug.tar.gz\` of split debug symbols. See"
|
|
P="$P [SECURITY.md](${GITHUB_SERVER_URL}/${REPO}/blob/${{ github.event.repository.default_branch }}/SECURITY.md)"
|
|
P="$P for verification steps and antivirus false-positive guidance."
|
|
echo "$P"
|
|
echo
|
|
echo "---"
|
|
echo
|
|
echo "> Auto-updated by CI on every nightly build. Edits here are overwritten."
|
|
} > "$BODY_FILE"
|
|
|
|
# Find the existing tracker without the search API, which is
|
|
# eventually-consistent — a miss opens a DUPLICATE issue, and then
|
|
# another on every run after that, so the lookup has to be exact.
|
|
#
|
|
# Label first: filtering is server-side and stays exact no matter how
|
|
# many issues the repo accumulates. Exact-title match is the fallback
|
|
# for the very first run (before the label exists) and for the case
|
|
# where the label was removed by hand. Both must miss to duplicate.
|
|
LABEL=nightly-tracker
|
|
gh label create "$LABEL" --repo "${REPO}" --color 0E8A16 \
|
|
--description "Auto-updated nightly build tracker" >/dev/null 2>&1 || true
|
|
|
|
NUM=$(gh issue list --repo "${REPO}" --state all --label "$LABEL" \
|
|
--limit 1 --json number --jq '.[0].number // empty' 2>/dev/null || echo "")
|
|
if [ -z "$NUM" ]; then
|
|
NUM=$(gh issue list --repo "${REPO}" --state all --limit 500 \
|
|
--json number,title \
|
|
--jq "map(select(.title == \"${ISSUE_TITLE}\")) | .[0].number // empty")
|
|
fi
|
|
|
|
if [ -n "$NUM" ]; then
|
|
gh issue edit "$NUM" --repo "${REPO}" --body-file "$BODY_FILE" \
|
|
--add-label "$LABEL" >/dev/null
|
|
else
|
|
NUM=$(gh issue create --repo "${REPO}" \
|
|
--title "${ISSUE_TITLE}" \
|
|
--body-file "$BODY_FILE" \
|
|
| grep -oE '[0-9]+$')
|
|
gh issue edit "$NUM" --repo "${REPO}" --add-label "$LABEL" >/dev/null 2>&1 || \
|
|
echo "::warning::could not label tracking issue #${NUM}; title match is now the only lookup"
|
|
echo "==> Created tracking issue #${NUM}"
|
|
fi
|
|
|
|
# Pinning has no REST endpoint; pinIssue is GraphQL-only and caps at
|
|
# 3 pinned issues per repo. Re-pinning an already-pinned issue errors
|
|
# harmlessly, but so does hitting the cap or lacking permission — and
|
|
# a pinned issue is the whole point, so warn rather than swallow it.
|
|
ID=$(gh api "repos/${REPO}/issues/${NUM}" --jq '.node_id')
|
|
# shellcheck disable=SC2016 # $id is a GraphQL variable; it must NOT
|
|
# be expanded by the shell — gh binds it from the -f id= argument.
|
|
gh api graphql -f query='mutation($id:ID!){pinIssue(input:{issueId:$id}){issue{number}}}' \
|
|
-f id="$ID" >/dev/null 2>&1 || \
|
|
echo "::warning::could not pin issue #${NUM} (already pinned, 3-pin cap, or permission)"
|
|
|
|
echo "==> Tracking issue: ${GITHUB_SERVER_URL}/${REPO}/issues/${NUM}"
|