Files
virtualjaguar-libretro/.github/workflows/c-cpp.yml
T

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='*'