mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 12:47:58 -10:00
886 lines
39 KiB
YAML
886 lines
39 KiB
YAML
name: C/C++ CI
|
|
|
|
on:
|
|
push:
|
|
branches: [ master, develop ]
|
|
pull_request:
|
|
branches: [ develop, master ]
|
|
workflow_dispatch:
|
|
|
|
# Cancel in-progress runs when a new commit lands on the same PR.
|
|
# Pushes to master/develop run to completion (no cancellation) so we
|
|
# always have a green-or-red record on the integration branches.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
jobs:
|
|
core-plan:
|
|
name: Production core routing and safety tests
|
|
runs-on: ${{ contains(fromJSON(vars.VJ_TEMP_RUNNERS || '{"refs":[]}').refs, github.ref) && fromJSON(vars.VJ_TEMP_RUNNERS)['ubuntu-latest'] || 'ubuntu-latest' }}
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
reuse: ${{ steps.plan.outputs.reuse }}
|
|
matrix: ${{ steps.plan.outputs.matrix }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: '22'
|
|
- name: Ensure the workflow contract parser is available
|
|
run: |
|
|
if ! command -v ruby >/dev/null 2>&1; then
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends ruby
|
|
fi
|
|
- name: Verify compiler, SDK and production ABI contracts
|
|
run: ruby .github/scripts/tests/core-workflow-contract.rb
|
|
- run: node --test .github/scripts/tests/release-packages.test.cjs .github/scripts/tests/release-cores.test.cjs
|
|
- id: plan
|
|
uses: actions/github-script@v9
|
|
with:
|
|
script: |
|
|
const { planCores } = require('./.github/scripts/release-cores.cjs');
|
|
await planCores({ github, context, core });
|
|
|
|
core-reuse:
|
|
name: Verify and retain shared production cores
|
|
needs: [core-plan]
|
|
if: needs.core-plan.outputs.reuse == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 360
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
pull-requests: read
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: '22'
|
|
- name: Require all exact-head production jobs and artifacts
|
|
id: verify
|
|
uses: actions/github-script@v9
|
|
with:
|
|
script: |
|
|
const { waitForCores } = require('./.github/scripts/release-cores.cjs');
|
|
await waitForCores({ github, context, core });
|
|
- name: Download the unstripped production intermediates
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
github-token: ${{ github.token }}
|
|
run-id: ${{ steps.verify.outputs.run_id }}
|
|
pattern: ci-core-*
|
|
path: shared-cores
|
|
- name: Verify exact commit, production ABI and binary checksums
|
|
env:
|
|
CORE_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
node -e 'require("./.github/scripts/release-cores.cjs").verifyDownloads("shared-cores", process.env.CORE_HEAD_SHA)'
|
|
- name: Retain macOS x86_64 (Clang, cross) artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: 'macOS x86_64 (Clang, cross)'
|
|
path: shared-cores/ci-core-macos-x86_64/virtualjaguar_libretro.dylib
|
|
if-no-files-found: error
|
|
overwrite: true
|
|
- name: Retain Emscripten (WASM) artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: 'Emscripten (WASM)'
|
|
path: shared-cores/ci-core-emscripten-wasm/virtualjaguar_libretro_emscripten.bc
|
|
if-no-files-found: error
|
|
overwrite: true
|
|
- name: Retain Android arm64-v8a artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: 'Android arm64-v8a'
|
|
path: shared-cores/ci-core-android-arm64-v8a/libretro.so
|
|
if-no-files-found: error
|
|
overwrite: true
|
|
- name: Retain Android armeabi-v7a artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: 'Android armeabi-v7a'
|
|
path: shared-cores/ci-core-android-armeabi-v7a/libretro.so
|
|
if-no-files-found: error
|
|
overwrite: true
|
|
- name: Retain Android x86_64 artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: 'Android x86_64'
|
|
path: shared-cores/ci-core-android-x86_64/libretro.so
|
|
if-no-files-found: error
|
|
overwrite: true
|
|
- name: Retain Android x86 artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: 'Android x86'
|
|
path: shared-cores/ci-core-android-x86/libretro.so
|
|
if-no-files-found: error
|
|
overwrite: true
|
|
- name: Retain iOS arm64 artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: 'iOS arm64'
|
|
path: shared-cores/ci-core-ios-arm64/virtualjaguar_libretro_ios.dylib
|
|
if-no-files-found: error
|
|
overwrite: true
|
|
- name: Retain tvOS arm64 artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: 'tvOS arm64'
|
|
path: shared-cores/ci-core-tvos-arm64/virtualjaguar_libretro_tvos.dylib
|
|
if-no-files-found: error
|
|
overwrite: true
|
|
- name: Retain PS Vita artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: 'PS Vita'
|
|
path: shared-cores/ci-core-vita/virtualjaguar_libretro_vita.a
|
|
if-no-files-found: error
|
|
overwrite: true
|
|
- name: Retain Nintendo Switch artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: 'Nintendo Switch'
|
|
path: shared-cores/ci-core-switch/virtualjaguar_libretro_libnx.a
|
|
if-no-files-found: error
|
|
overwrite: true
|
|
|
|
build:
|
|
needs: [core-plan]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
config: ${{ fromJSON(needs.core-plan.outputs.matrix) }}
|
|
|
|
name: build-${{ matrix.config.displayTargetName }}
|
|
env:
|
|
# Compile-only rows match Release's production configuration; native test mode switches stay unchanged.
|
|
RELEASE_DEBUG_INFO: ${{ matrix.config.release_platform && '1' || '0' }}
|
|
runs-on: ${{ 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
|
|
|
|
- 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 }}
|
|
run: make -j4 CC="${{ matrix.config.cc }}" CXX="${{ matrix.config.cxx }}" ${{ matrix.config.make_extra || '' }}
|
|
|
|
- name: Build (platform)
|
|
if: matrix.config.make_platform
|
|
run: make -j4 platform=${{ matrix.config.make_platform }}
|
|
|
|
- name: Build (Emscripten)
|
|
if: matrix.config.emscripten
|
|
run: emmake make -j4 platform=emscripten
|
|
|
|
- name: Build (Android NDK)
|
|
if: matrix.config.android
|
|
run: bash scripts/build-ndk-ci.sh "${{ steps.setup-ndk.outputs.ndk-path }}" "${{ matrix.config.android_abi }}" "${{ matrix.config.android_simd }}"
|
|
|
|
# Mach-O symbol gating: production builds must export retro_* only.
|
|
# Runs before the test step (which would relink with TEST_EXPORTS=1
|
|
# and widen the symbol set).
|
|
- name: Verify Mach-O symbol gating (production)
|
|
if: ${{ runner.os == 'macOS' && !matrix.config.emscripten && !matrix.config.android }}
|
|
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
|
|
COUNT=$(nm -gU "${{ matrix.config.artifact }}" | wc -l | tr -d ' ')
|
|
echo "==> Production exports: ${COUNT} (all retro_*)"
|
|
|
|
# Tier-1 private ROM corpus (~22 MB) from a private S3-compatible bucket.
|
|
# Without it `make test` runs green while SKIPPING the audio pair, the
|
|
# clipping sentinels and every other check that needs a commercial ROM --
|
|
# a green run that tested none of the things that catch regressions.
|
|
#
|
|
# Cached on the manifest hash, so a hit costs no egress at all and only a
|
|
# manifest change re-downloads. Secrets are absent on fork PRs by design;
|
|
# the step is best-effort there and `make test` falls back to skipping,
|
|
# exactly as it does today.
|
|
- name: Cache Tier-1 ROMs
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: test/roms/tier1
|
|
key: vj-roms-tier1-${{ hashFiles('test/roms/tier1.manifest') }}
|
|
|
|
- name: Fetch Tier-1 ROMs
|
|
id: roms
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
|
|
continue-on-error: true
|
|
env:
|
|
# Two backends; rom-store.sh picks whichever is configured (GitHub
|
|
# wins when VJ_ROM_GH_REPO is set). Set one group of secrets, not both.
|
|
#
|
|
# GitHub backend: VJ_ROM_GH_TOKEN must be a fine-grained PAT with
|
|
# read-only Contents on the ROM repo. The built-in GITHUB_TOKEN cannot
|
|
# be used -- it is scoped to THIS repository and cannot read another
|
|
# private one, which fails as a 404 rather than a permission error.
|
|
VJ_ROM_GH_REPO: ${{ secrets.VJ_ROM_GH_REPO }}
|
|
GH_TOKEN: ${{ secrets.VJ_ROM_GH_TOKEN }}
|
|
# S3 backend (Cloudflare R2 etc.)
|
|
VJ_ROM_ENDPOINT: ${{ secrets.VJ_ROM_ENDPOINT }}
|
|
VJ_ROM_BUCKET: ${{ secrets.VJ_ROM_BUCKET }}
|
|
VJ_ROM_KEY: ${{ secrets.VJ_ROM_KEY }}
|
|
VJ_ROM_SECRET: ${{ secrets.VJ_ROM_SECRET }}
|
|
run: |
|
|
set +e
|
|
bash scripts/rom-store.sh fetch
|
|
rc=$?
|
|
if [ "$rc" -eq 0 ]; then
|
|
echo "roms=1" >> "$GITHUB_OUTPUT"
|
|
elif [ "$rc" -eq 77 ]; then
|
|
echo "::notice::Tier-1 ROM store not configured -- ROM-gated checks will skip."
|
|
echo "roms=0" >> "$GITHUB_OUTPUT"
|
|
else
|
|
# A configured store that FAILS is not the same as an absent one and
|
|
# must be visible, even though this step does not block the build.
|
|
echo "::warning::Tier-1 ROM fetch FAILED (rc=$rc) -- ROM-gated checks will skip."
|
|
echo "roms=0" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
exit 0
|
|
|
|
# Host/native toolchains only — skips cross-compile rows (e.g. aarch64 on x86 runner).
|
|
- name: Run cheat engine unit tests
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
|
|
env:
|
|
# Only set when the fetch actually produced a verified tree. Pointing
|
|
# this at an empty directory would make find-rom.sh miss everything
|
|
# and turn every ROM-gated check into a silent skip.
|
|
ROMS_PRIVATE_ROOT: ${{ steps.roms.outputs.roms == '1' && 'test/roms/tier1' || '' }}
|
|
run: make test CC="${{ matrix.config.cc }}"
|
|
|
|
# Standalone libchdr + committed fixtures. No core dlopen, so this
|
|
# also runs on Windows MSYS2 where `make test` is skipped.
|
|
- name: CHD fixture unit tests
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross }}
|
|
run: |
|
|
# Compile with $CC directly. `make` of these targets after `make test`
|
|
# flips the ABI stamp and deletes the core library.
|
|
CC="${{ matrix.config.cc }}"
|
|
INCFLAGS="-I. -Isrc -Isrc/core -Isrc/tom -Isrc/jerry -Isrc/cd -Isrc/bios -Isrc/m68000 -Ilibretro-common/include -Ideps/libchdr/include -Ideps/libchdr/deps/lzma-25.01/include -Ideps/libchdr/deps/miniz-3.1.1 -Ideps/libchdr/deps/zstd-1.5.7"
|
|
LIBCHDR="-D_7ZIP_ST -DWANT_RAW_DATA_SECTOR=1 -DWANT_SUBCODE=1 -DVERIFY_BLOCK_CRC=1"
|
|
$CC -O2 -Wall -std=c99 $INCFLAGS $LIBCHDR \
|
|
-o tools/jagcd/jagcd-chd-check tools/jagcd/jagcd-chd-check.c deps/libchdr/unity.c
|
|
$CC -O2 -Wall -Wno-unused-function -Wno-unused-variable -std=c99 $INCFLAGS $LIBCHDR \
|
|
-o test/test_chd_unit test/test_chd_unit.c deps/libchdr/unity.c
|
|
./tools/jagcd/jagcd-chd-check test/roms/synth_jagcd.chd
|
|
if ./tools/jagcd/jagcd-chd-check test/roms/synth_jagcd_nosession.chd; then
|
|
echo "jagcd-chd-check: expected exit 1 on synth_jagcd_nosession.chd"
|
|
exit 1
|
|
else
|
|
rc=$?
|
|
if [ "$rc" -ne 1 ]; then
|
|
echo "jagcd-chd-check: unexpected $rc"
|
|
exit "$rc"
|
|
fi
|
|
fi
|
|
# exit 2 = virtual pregaps present (a warning, still a valid CHD).
|
|
rc=0
|
|
./tools/jagcd/jagcd-chd-check test/roms/synth_jagcd_vpregap.chd >/dev/null || rc=$?
|
|
if [ "$rc" -ne 2 ]; then
|
|
echo "jagcd-chd-check: expected exit 2 on synth_jagcd_vpregap.chd, got $rc"
|
|
exit 1
|
|
fi
|
|
./test/test_chd_unit
|
|
|
|
- name: Run SIMD blitter tests
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross }}
|
|
run: |
|
|
ARCH=$(uname -m)
|
|
CC="${{ matrix.config.cc }}"
|
|
# -DBLITTER_SIMD_<ARCH> is not optional: blitter_simd.h inlines
|
|
# the matching arch header, so an arch .c compiled without its
|
|
# define pulls in the scalar ops and then redefines every one.
|
|
case "$ARCH" in
|
|
x86_64|i686|i386) SIMD_SRC=src/tom/blitter_simd_sse2.c; EXTRA="-msse2 -DBLITTER_SIMD_SSE2" ;;
|
|
aarch64|arm64) SIMD_SRC=src/tom/blitter_simd_neon.c; EXTRA="-DBLITTER_SIMD_NEON" ;;
|
|
*) SIMD_SRC=src/tom/blitter_simd_scalar.c; EXTRA="-DBLITTER_SIMD_SCALAR" ;;
|
|
esac
|
|
|
|
# -I libretro-common/include is required, not decorative: blitter_simd.h
|
|
# does `#include <boolean.h>`, and libretro-common owns that header.
|
|
# This compile used to resolve it by accident through src/core, which
|
|
# carried a stale duplicate copy; with that removed the include path has
|
|
# to name the real location. Makefile.common always passed this -I, so
|
|
# only these by-hand compiles were affected.
|
|
echo "==> Testing ${SIMD_SRC}..."
|
|
$CC -O2 -Wall ${EXTRA} -I src -I src/core -I src/tom -I libretro-common/include \
|
|
-o test_blitter_simd test/test_blitter_simd.c ${SIMD_SRC}
|
|
./test_blitter_simd
|
|
|
|
echo "==> Cross-checking against scalar..."
|
|
$CC -O2 -Wall -DBLITTER_SIMD_SCALAR -I src -I src/core -I src/tom -I libretro-common/include \
|
|
-o test_blitter_scalar test/test_blitter_simd.c src/tom/blitter_simd_scalar.c
|
|
./test_blitter_scalar
|
|
|
|
echo "==> DSP 40-bit MAC accumulator regression (dsp_acc40.h)..."
|
|
$CC -O2 -Wall -I src -I src/jerry -o test_dsp_mac40 test/test_dsp_mac40.c
|
|
./test_dsp_mac40
|
|
|
|
- name: Run memory map test
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
|
|
run: |
|
|
CC="${{ matrix.config.cc }}"
|
|
if [ "$(uname)" = "Linux" ]; then
|
|
LDFLAGS="-ldl"
|
|
else
|
|
LDFLAGS=""
|
|
fi
|
|
$CC -O2 -Wall -o test/tools/test_memory_map test/tools/test_memory_map.c $LDFLAGS
|
|
./test/tools/test_memory_map ./${{ matrix.config.artifact }}
|
|
|
|
- name: Run DSP instruction set tests
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
|
|
run: |
|
|
CC="${{ matrix.config.cc }}"
|
|
if [ "$(uname)" = "Linux" ]; then LDFLAGS="-ldl"; else LDFLAGS=""; fi
|
|
$CC -O2 -Wall -o test/test_dsp_ops test/test_dsp_ops.c $LDFLAGS
|
|
$CC -O2 -Wall -o test/test_dsp_unit test/test_dsp_unit.c $LDFLAGS
|
|
./test/test_dsp_ops
|
|
./test/test_dsp_unit
|
|
|
|
- name: Run GPU instruction set tests
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
|
|
run: |
|
|
CC="${{ matrix.config.cc }}"
|
|
if [ "$(uname)" = "Linux" ]; then LDFLAGS="-ldl"; else LDFLAGS=""; fi
|
|
$CC -O2 -Wall -o test/test_gpu_ops test/test_gpu_ops.c $LDFLAGS
|
|
./test/test_gpu_ops
|
|
|
|
$CC -O2 -Wall -o test/test_op_gpu_object test/test_op_gpu_object.c $LDFLAGS
|
|
./test/test_op_gpu_object ./${{ matrix.config.artifact }}
|
|
|
|
- name: Run 68K instruction set tests
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
|
|
run: |
|
|
CC="${{ matrix.config.cc }}"
|
|
if [ "$(uname)" = "Linux" ]; then LDFLAGS="-ldl"; else LDFLAGS=""; fi
|
|
$CC -O2 -Wall -o test/test_m68k_ops test/test_m68k_ops.c $LDFLAGS
|
|
./test/test_m68k_ops
|
|
|
|
- name: Run EEPROM lifecycle test
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
|
|
run: |
|
|
CC="${{ matrix.config.cc }}"
|
|
if [ "$(uname)" = "Linux" ]; then LDFLAGS="-ldl"; else LDFLAGS=""; fi
|
|
INCFLAGS="-I. -Isrc -Isrc/core -Isrc/tom -Isrc/jerry -Ilibretro-common/include"
|
|
$CC -O2 -Wall -o /tmp/gen_eeprom_test_rom test/tools/gen_eeprom_test_rom.c
|
|
/tmp/gen_eeprom_test_rom /tmp/eeprom_lifecycle_test.j64
|
|
$CC -O2 -Wall -std=c99 $INCFLAGS \
|
|
-o test/test_eeprom_lifecycle test/test_eeprom_lifecycle.c \
|
|
test/harness/harness.c $LDFLAGS -lm
|
|
./test/test_eeprom_lifecycle ./${{ matrix.config.artifact }} /tmp/eeprom_lifecycle_test.j64
|
|
|
|
- name: Cache pinned rcheevos E2E build
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: build/rcheevos-static
|
|
key: rcheevos-e2e-fd57e900-${{ runner.os }}-${{ matrix.config.cc }}-${{ matrix.config.displayTargetName }}
|
|
|
|
- name: RetroAchievements rc_libretro E2E test
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
|
|
env:
|
|
# Immutable pin (rcheevos v12.3.0); tarball via .../archive/${SHA}.tar.gz
|
|
RCHEEVOS_REF: fd57e900758a9e3de343e7b0316b8a6059dce228
|
|
CC: ${{ matrix.config.cc }}
|
|
run: bash test/tools/test_rcheevos_e2e.sh ./${{ matrix.config.artifact }}
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ${{ matrix.config.displayTargetName }}
|
|
path: ${{ matrix.config.artifact }}
|
|
if-no-files-found: error
|
|
|
|
# TEST_EXPORTS=1 -> production mode switch. TEST_EXPORTS adds
|
|
# -DVJ_TRACE, so it changes object *content*, not just the export list;
|
|
# the Makefile flushes every object on a mode change, and a stale one
|
|
# fails the link with undefined vjtrace_* symbols. The build + `make
|
|
# test` steps above already exercise plain -> TEST_EXPORTS=1; this is the
|
|
# reverse, which is the direction that regressed (blit_memo.o was missing
|
|
# from the flush list). Runs last so it cannot hand a production-ABI
|
|
# library to the dlsym-based steps or to the uploaded artifact.
|
|
- name: Verify TEST_EXPORTS -> production mode switch relinks
|
|
if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross && runner.os != 'Windows' }}
|
|
run: make -j4 CC="${{ matrix.config.cc }}" CXX="${{ matrix.config.cxx }}" ${{ matrix.config.make_extra || '' }}
|
|
|
|
# Independent lightweight checks share one checkout and runner allocation.
|
|
# !cancelled() keeps later checks running after a failure; any failure still fails the job.
|
|
source-validation:
|
|
name: Source validation (lists, EditorConfig, cppcheck, C89)
|
|
runs-on: ${{ contains(fromJSON(vars.VJ_TEMP_RUNNERS || '{"refs":[]}').refs, github.ref) && fromJSON(vars.VJ_TEMP_RUNNERS)['ubuntu-latest'] || 'ubuntu-latest' }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
# src/core/version_fallback.h holds a deliberate copy of
|
|
# CORE_BASE_VERSION for build systems that cannot run
|
|
# scripts/gen-version-h.sh before compiling (the Provenance SwiftPM
|
|
# manifest). Nothing rewrites it at build time on purpose -- the
|
|
# generator runs at Makefile *parse* time, so a generator that edited a
|
|
# tracked file would dirty the tree on every build and feed a false
|
|
# "-dirty" into scripts/build-id.sh and the VJ_EXPECT_BUILD guard.
|
|
#
|
|
# So the copy is checked rather than fixed, the same way the
|
|
# docs/cd-boot-matrix.md drift guard works: fail loudly, never quietly
|
|
# correct. version-bump.yml updates both files on a release bump; this
|
|
# catches a hand edit that touched only one.
|
|
- name: CORE_BASE_VERSION matches the Makefile
|
|
if: ${{ !cancelled() }}
|
|
run: |
|
|
set -euo pipefail
|
|
mk=$(sed -n 's/^CORE_BASE_VERSION[[:space:]]*:*=[[:space:]]*\(.*\)/\1/p' Makefile | head -1)
|
|
fb=$(sed -n 's/^#define CORE_BASE_VERSION "\(.*\)"$/\1/p' src/core/version_fallback.h | head -1)
|
|
if [ -z "$mk" ]; then echo "could not read CORE_BASE_VERSION from Makefile"; exit 1; fi
|
|
if [ -z "$fb" ]; then echo "could not read CORE_BASE_VERSION from src/core/version_fallback.h"; exit 1; fi
|
|
if [ "$mk" != "$fb" ]; then
|
|
echo "::error file=src/core/version_fallback.h::version_fallback.h says '$fb' but the Makefile says '$mk' -- update both."
|
|
exit 1
|
|
fi
|
|
echo "OK: both say $mk"
|
|
|
|
# Package.swift repeats Makefile.common's source list, because SPM needs
|
|
# the file list in the manifest and cannot read a Makefile. Same posture as
|
|
# the check above: fail loudly, never auto-fix. The downstream Provenance
|
|
# wrapper kept its own copy of this list and drifted by 27 files over seven
|
|
# releases -- this is that lesson turned into a build failure.
|
|
- name: Package.swift sources match Makefile.common
|
|
if: ${{ !cancelled() }}
|
|
run: sh scripts/check-package-sources.sh
|
|
|
|
# Same posture for the msvc-check job's inline source list, which rots the
|
|
# same way and worse: it is Windows-only, so drift shows up as a missing
|
|
# header on two jobs while ~35 others stay green (issue #679, and PR #662
|
|
# is the case in point).
|
|
- name: MSVC job sources match Makefile.common
|
|
if: ${{ !cancelled() }}
|
|
run: sh scripts/check-msvc-sources.sh
|
|
|
|
|
|
- name: Install editorconfig-checker
|
|
if: ${{ !cancelled() }}
|
|
run: |
|
|
VERSION=v3.0.3
|
|
curl -fsSL "https://github.com/editorconfig-checker/editorconfig-checker/releases/download/${VERSION}/ec-linux-amd64.tar.gz" \
|
|
| tar -xz -C /tmp
|
|
sudo install -m 0755 /tmp/bin/ec-linux-amd64 /usr/local/bin/ec
|
|
|
|
- name: Run editorconfig-checker
|
|
if: ${{ !cancelled() }}
|
|
run: ec -verbose
|
|
|
|
|
|
- name: Install cppcheck
|
|
if: ${{ !cancelled() }}
|
|
run: sudo apt-get update && sudo apt-get install -y cppcheck
|
|
|
|
- name: Run cppcheck
|
|
if: ${{ !cancelled() }}
|
|
run: |
|
|
# Scan in-tree sources only. src/m68000 is machine-generated
|
|
# UAE 68K, src/bios/jag*.c are bin2c hex tables, and
|
|
# libretro-common is a vendored subtree -- all excluded.
|
|
#
|
|
# Suppressions live in cppcheck-suppressions.txt with rationale.
|
|
# cppcheck 2.13 (Ubuntu 24.04) doesn't accept comment lines in
|
|
# the suppressions file; strip them before passing.
|
|
bash scripts/gen-version-h.sh
|
|
# POSIX character class -- not GNU grep's \s extension.
|
|
grep -vE '^[[:space:]]*(#|$)' cppcheck-suppressions.txt > /tmp/cppcheck-suppressions.txt
|
|
cppcheck \
|
|
--enable=warning,performance,portability \
|
|
--suppress=missingIncludeSystem \
|
|
--suppress=unusedFunction \
|
|
--suppressions-list=/tmp/cppcheck-suppressions.txt \
|
|
--error-exitcode=1 \
|
|
-i src/m68000 -i src/bios -i libretro-common \
|
|
-I src -I src/core -I src/tom -I src/jerry -I src/cd \
|
|
-I src/bios -I src/m68000 -I libretro-common/include \
|
|
-DINLINE=inline -D__LIBRETRO__ \
|
|
--quiet \
|
|
src/ libretro.c
|
|
|
|
|
|
- name: Check for declaration-after-statement
|
|
if: ${{ !cancelled() }}
|
|
run: |
|
|
echo "==> Checking C89 compliance (catches MSVC C89 errors)..."
|
|
scripts/c89-lint.sh
|
|
|
|
- name: Verify .info display_version matches Makefile
|
|
if: ${{ !cancelled() }}
|
|
run: bash scripts/check-info-version.sh
|
|
|
|
- name: Check for stdbool.h usage (use boolean.h instead)
|
|
if: ${{ !cancelled() }}
|
|
run: |
|
|
echo "==> Checking for direct stdbool.h includes..."
|
|
FAILED=0
|
|
for f in libretro.c $(git ls-files 'src/**/*.c' 'src/**/*.h'); do
|
|
case "$f" in
|
|
src/core/boolean.h) continue ;;
|
|
esac
|
|
if grep -n '#include.*<stdbool\.h>' "$f" 2>/dev/null; then
|
|
echo "::error file=$f::Use <boolean.h> instead of <stdbool.h> (MSVC 2005/2010 compat)"
|
|
FAILED=1
|
|
fi
|
|
done
|
|
if [ "$FAILED" = "1" ]; then
|
|
exit 1
|
|
fi
|
|
echo "==> No direct stdbool.h includes found"
|
|
|
|
spm-build:
|
|
name: SwiftPM build (macOS arm64)
|
|
runs-on: macos-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
# No Makefile target reads Package.swift, so without this job a manifest
|
|
# that does not even parse would merge unnoticed and only break the Apple
|
|
# frontend that consumes it. Building the package here is the only thing
|
|
# in CI that exercises it.
|
|
#
|
|
# This also covers the two things the source-list diff cannot see: that the
|
|
# arch guards resolve to exactly one blitter_simd_ops when all three
|
|
# variants are compiled together, and that libretro.c still compiles with
|
|
# no generated src/core/version.h (a fresh checkout has none -- the
|
|
# committed src/core/version_fallback.h is what makes that work).
|
|
- name: swift build
|
|
run: swift build -v 2>&1 | tail -40
|
|
|
|
# The blind spot that let an `unsafeFlags` slip in: SwiftPM REFUSES to
|
|
# resolve a VERSIONED dependency on a package whose targets carry unsafe
|
|
# flags, but a package built as the ROOT (which `swift build` above does)
|
|
# is exempt -- so the restriction is invisible from inside the repo. This
|
|
# consumes the package the way a frontend actually would, by version tag,
|
|
# which is the only way to see it.
|
|
- name: a versioned consumer can build against this package
|
|
run: |
|
|
set -euo pipefail
|
|
work=$(mktemp -d)
|
|
# A tag on a throwaway clone; the real repo is untouched.
|
|
# Clone under the repo name: SwiftPM derives package identity from the
|
|
# URL's last path component, not from `name:` in the manifest.
|
|
git clone --quiet --no-hardlinks . "$work/virtualjaguar-libretro"
|
|
git -C "$work/virtualjaguar-libretro" -c [email protected] -c user.name=CI tag -f 0.0.1
|
|
mkdir -p "$work/app/Sources/app"
|
|
echo 'print("ok")' > "$work/app/Sources/app/main.swift"
|
|
cat > "$work/app/Package.swift" <<PKG
|
|
// swift-tools-version:5.9
|
|
import PackageDescription
|
|
let package = Package(
|
|
name: "app",
|
|
dependencies: [.package(url: "$work/virtualjaguar-libretro", from: "0.0.1")],
|
|
targets: [.executableTarget(name: "app", dependencies: [
|
|
.product(name: "VirtualJaguar", package: "virtualjaguar-libretro")])]
|
|
)
|
|
PKG
|
|
cd "$work/app"
|
|
# `swift build`, NOT `swift package resolve`: resolve succeeds even with
|
|
# unsafe flags present -- the restriction is enforced when the dependency
|
|
# graph is loaded for a build. Verified both ways before relying on it.
|
|
if ! out=$(swift build 2>&1); then
|
|
echo "$out"
|
|
echo "::error::a versioned dependency on this package does not resolve."
|
|
echo "::error::If this says 'contains unsafe build flags', remove the .unsafeFlags"
|
|
echo "::error::from Package.swift -- they make the package unconsumable by any"
|
|
echo "::error::frontend pinning a release tag, which is the point of shipping it."
|
|
exit 1
|
|
fi
|
|
echo "OK: builds as a versioned dependency"
|
|
|
|
- name: exactly one SIMD vtable, and it is not the scalar one
|
|
run: |
|
|
set -euo pipefail
|
|
B=$(find .build -type d -name 'virtualjaguar.build' | head -1)
|
|
[ -n "$B" ] || { echo "could not locate the build directory"; exit 1; }
|
|
total=0
|
|
for a in neon sse2 scalar; do
|
|
o="$B/src/tom/blitter_simd_$a.c.o"
|
|
n=0
|
|
[ -f "$o" ] && n=$(nm "$o" | grep -c '_blitter_simd_ops' || true)
|
|
echo " blitter_simd_$a: $n"
|
|
total=$((total + n))
|
|
done
|
|
[ "$total" -eq 1 ] || { echo "::error::expected exactly 1 blitter_simd_ops definition, found $total"; exit 1; }
|
|
echo "OK: exactly one vtable"
|
|
|
|
msvc-check:
|
|
name: MSVC ${{ matrix.arch }} compilation check
|
|
runs-on: windows-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
arch: [x64, x86]
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- uses: ilammy/msvc-dev-cmd@v1
|
|
with:
|
|
arch: ${{ matrix.arch }}
|
|
|
|
- name: Generate src/core/version.h
|
|
shell: bash
|
|
run: bash scripts/gen-version-h.sh
|
|
|
|
# Sources are .c, so cl.exe compiles them in C mode -- deliberately.
|
|
# That is what the buildbot does, and it is what makes this job a
|
|
# useful C89/MSVC check. Do not "fix" it to /TP. Do not add /Za
|
|
# either: it breaks <emmintrin.h> and the Windows SDK headers.
|
|
#
|
|
# Exactly one BLITTER_SIMD_<ARCH> may be defined per translation
|
|
# unit -- blitter_simd.h inlines the matching blitter_simd_<arch>.h,
|
|
# and any arch .c compiled under a different (or absent) define
|
|
# redefines every op. So the SIMD arches get one cl.exe run each,
|
|
# mirroring how Makefile.common builds exactly one of them.
|
|
- name: Compile all sources with cl.exe (scalar blitter SIMD)
|
|
shell: cmd
|
|
run: |
|
|
cl.exe /c /W3 /O2 /DNDEBUG /D_CRT_SECURE_NO_DEPRECATE ^
|
|
/I. /Isrc /Isrc\core /Isrc\tom /Isrc\jerry /Isrc\cd /Isrc\bios /Isrc\m68000 /Isrc\debug /Ilibretro-common\include ^
|
|
/Ideps\libchdr\include /Ideps\libchdr\deps\lzma-25.01\include /Ideps\libchdr\deps\miniz-3.1.1 /Ideps\libchdr\deps\zstd-1.5.7 ^
|
|
/D__LIBRETRO__ /DINLINE="_inline" /DBLITTER_SIMD_SCALAR ^
|
|
/D_7ZIP_ST /DMINIZ_DEFLATE_APIS /DWANT_RAW_DATA_SECTOR=1 /DWANT_SUBCODE=1 /DVERIFY_BLOCK_CRC=1 ^
|
|
libretro.c ^
|
|
src\tom\blitter.c src\tom\blitter_compare.c src\tom\blitter_mmio.c src\tom\texdump.c src\tom\texreplace.c src\jerry\dac.c src\jerry\dsp.c src\core\file.c ^
|
|
src\tom\gpu.c src\core\jaguar.c src\jerry\jerry.c src\tom\tom.c src\tom\op.c ^
|
|
src\cd\cdintf.c src\cd\cdrom.c src\core\crc32.c src\core\biosdb.c src\core\event.c ^
|
|
src\jerry\eeprom.c src\core\filedb.c src\jerry\joystick.c src\core\settings.c ^
|
|
src\core\memtrack.c src\core\vjag_memory.c src\core\cheat.c ^
|
|
src\core\universalhdr.c src\jerry\wavetable.c src\jerry\voicemodem.c src\core\vj_frontend_ext.c src\debug\vjdebug.c ^
|
|
src\bios\jagbios.c ^
|
|
src\bios\jagbios_m.c ^
|
|
src\bios\jagcdbios.c src\bios\jagdevcdbios.c ^
|
|
src\m68000\m68kinterface.c ^
|
|
src\debug\gdbstub.c src\debug\gdbtarget.c src\debug\gdbsock.c ^
|
|
src\tom\blitter_simd_scalar.c ^
|
|
deps\libchdr\unity.c
|
|
|
|
# The shape Makefile.common now selects for windows_msvc2015/2017
|
|
# x64 and x86: blitter.c inlines blitter_simd_sse2.h, so cl.exe
|
|
# compiles <emmintrin.h> intrinsics inside blitter.c itself -- not
|
|
# just inside the lint-exempt blitter_simd_sse2.c. That is the whole
|
|
# risk of enabling SSE2 for MSVC, and this step is what verifies it.
|
|
# Objects go to a subdir so they don't collide with the run above.
|
|
#
|
|
# No trailing `echo` in either step: `shell: cmd` has no `set -e`,
|
|
# so the step's exit code is the LAST command's. A closing echo
|
|
# would return 0 over any cl.exe failure and make this whole job
|
|
# green-but-inert. Let cl.exe be the last command.
|
|
- name: Compile blitter with cl.exe (SSE2 blitter SIMD)
|
|
shell: cmd
|
|
run: |
|
|
mkdir msvc-sse2
|
|
cl.exe /c /W3 /O2 /DNDEBUG /D_CRT_SECURE_NO_DEPRECATE ^
|
|
/I. /Isrc /Isrc\core /Isrc\tom /Isrc\jerry /Isrc\cd /Isrc\bios /Isrc\m68000 /Ilibretro-common\include ^
|
|
/D__LIBRETRO__ /DINLINE="_inline" /DBLITTER_SIMD_SSE2 ^
|
|
/Fomsvc-sse2\ ^
|
|
src\tom\blitter.c ^
|
|
src\tom\blitter_simd_sse2.c
|
|
|
|
vita-build:
|
|
needs: [core-plan]
|
|
if: needs.core-plan.outputs.reuse != 'true'
|
|
name: build-PS Vita
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: vitasdk/vitasdk:latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Build
|
|
env:
|
|
RELEASE_DEBUG_INFO: '1'
|
|
run: make -j4 platform=vita
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: PS Vita
|
|
path: virtualjaguar_libretro_vita.a
|
|
if-no-files-found: error
|
|
|
|
switch-build:
|
|
needs: [core-plan]
|
|
if: needs.core-plan.outputs.reuse != 'true'
|
|
name: build-Nintendo Switch
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: devkitpro/devkita64:latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Build
|
|
run: make -j4 platform=libnx
|
|
env:
|
|
DEVKITPRO: /opt/devkitpro
|
|
RELEASE_DEBUG_INFO: '1'
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: Nintendo Switch
|
|
path: virtualjaguar_libretro_libnx.a
|
|
if-no-files-found: error
|
|
|
|
coverage:
|
|
name: Code coverage (gcov + codecov)
|
|
runs-on: ubuntu-latest
|
|
# Coverage runs are advisory -- they don't gate merges. The PR
|
|
# delta is what reviewers care about; codecov posts that as a
|
|
# comment automatically.
|
|
continue-on-error: true
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
- name: Install gcov + gcovr
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y gcc gcovr
|
|
- name: make coverage
|
|
run: |
|
|
bash scripts/gen-version-h.sh
|
|
make coverage CC="gcc" CXX="g++"
|
|
- name: Upload to Codecov
|
|
uses: codecov/codecov-action@v7
|
|
with:
|
|
files: ./coverage.xml
|
|
fail_ci_if_error: false
|
|
# Token optional for public repos; if set in repo secrets it
|
|
# avoids the anonymous-upload rate limit.
|
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
|
|
sanitizers:
|
|
name: AddressSanitizer + UndefinedBehaviorSanitizer
|
|
runs-on: ubuntu-latest
|
|
# Initially advisory: lets the job report findings without blocking
|
|
# PRs while we triage the existing UB the cppcheck job already
|
|
# flagged (signed shifts in src/tom/{gpu,op}.c, etc.). Flip to
|
|
# `false` to make sanitizer findings gate merges.
|
|
continue-on-error: true
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- name: Install clang
|
|
run: sudo apt-get update && sudo apt-get install -y clang
|
|
- name: Build with ASAN + UBSAN
|
|
run: |
|
|
bash scripts/gen-version-h.sh
|
|
# -shared-libasan: when building a shared library with
|
|
# -fsanitize, clang doesn't link the static asan/ubsan
|
|
# runtime into the .so (it's expected to be loaded by the
|
|
# host executable). --no-undefined in the SHARED rule then
|
|
# rejects the runtime symbols. -shared-libasan switches to
|
|
# the dynamic libclang_rt.asan-x86_64.so which the host
|
|
# process picks up at dlopen time.
|
|
SAN_FLAGS="-fsanitize=address,undefined -shared-libasan -fno-omit-frame-pointer -O1 -g -fsanitize-ignorelist=$PWD/.ubsan-ignorelist"
|
|
make -j4 TEST_EXPORTS=1 \
|
|
CC="clang $SAN_FLAGS" \
|
|
CXX="clang++ $SAN_FLAGS" \
|
|
LD="clang $SAN_FLAGS"
|
|
- name: Run test suite under sanitizers
|
|
env:
|
|
ASAN_OPTIONS: 'detect_leaks=1:abort_on_error=1:print_stacktrace=1'
|
|
UBSAN_OPTIONS: 'print_stacktrace=1:halt_on_error=1:report_error_type=1'
|
|
run: |
|
|
SAN_FLAGS="-fsanitize=address,undefined -shared-libasan -fno-omit-frame-pointer -O1 -g -fsanitize-ignorelist=$PWD/.ubsan-ignorelist"
|
|
# The shared-libasan path means the test binaries need to find
|
|
# libclang_rt.asan at runtime; expose its directory.
|
|
ASAN_LIB_DIR=$(clang -print-resource-dir)/lib/linux
|
|
export LD_LIBRARY_PATH="$ASAN_LIB_DIR:${LD_LIBRARY_PATH:-}"
|
|
make test TEST_EXPORTS=1 \
|
|
CC="clang $SAN_FLAGS" \
|
|
CXX="clang++ $SAN_FLAGS" \
|
|
LD="clang $SAN_FLAGS"
|
|
|
|
clang-tidy:
|
|
name: clang-tidy on changed files
|
|
runs-on: ubuntu-latest
|
|
# Advisory at first. The .clang-tidy check list is curated, but
|
|
# the first pass over emulator C will surface noisy true positives
|
|
# that need triage. Flip continue-on-error to false once the
|
|
# baseline is calm.
|
|
continue-on-error: true
|
|
if: github.event_name == 'pull_request'
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
- name: Install clang-tidy + bear
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y clang-tidy bear
|
|
- name: Generate compile_commands.json
|
|
run: |
|
|
bash scripts/gen-version-h.sh
|
|
# `bear -- make` records every compile invocation into
|
|
# compile_commands.json without affecting the build.
|
|
bear -- make -j4
|
|
- name: Run clang-tidy on PR-changed files
|
|
run: |
|
|
BASE="${{ github.event.pull_request.base.sha }}"
|
|
# --diff-filter=d drops deleted paths: clang-tidy cannot lint a
|
|
# file that no longer exists and fails the whole job with
|
|
# "no input files" / "expected exactly one compiler job".
|
|
#
|
|
# Every *_simd_<arch>.{c,h} fragment (blitter_simd_*, op_simd_*,
|
|
# tom_scan_simd_*, and any future src/jerry ones) is excluded.
|
|
# Only one arch is ever built, so the other arch .c files
|
|
# aren't in compile_commands.json; and the .h files are
|
|
# include-only fragments that cannot be linted standalone
|
|
# (blitter's #error unless blitter_simd.h pulled them in; the
|
|
# op/tom ones compile to nothing without their host .c's
|
|
# capability macros and includes). They are covered instead by
|
|
# c89-lint's per-arch pass, which compiles them the way the
|
|
# build does.
|
|
FILES=$(git diff --name-only --diff-filter=d "$BASE"...HEAD -- '*.c' '*.h' \
|
|
| grep -Ev '^(src/m68000/|src/bios/jag.*\.c$|libretro-common/|src/core/version\.h$|src/(tom|jerry)/[a-z0-9_]+_simd_(neon|sse2|scalar)\.(c|h)$)' \
|
|
| grep -E '^(src/|libretro\.c$)' || true)
|
|
if [ -z "$FILES" ]; then
|
|
echo "No relevant C files changed; skipping clang-tidy."
|
|
exit 0
|
|
fi
|
|
echo "==> Running clang-tidy on:"
|
|
echo "$FILES"
|
|
echo "$FILES" | xargs clang-tidy -p . --quiet --warnings-as-errors='*'
|