mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
ci(core): share compile-only release builds and preserve CI artifacts
This commit is contained in:
10 files changed
+690
-165
No files matched your search
@@ -0,0 +1,127 @@
|
||||
[
|
||||
{
|
||||
"displayTargetName": "Linux x86_64 (GCC)",
|
||||
"artifact": "virtualjaguar_libretro.so",
|
||||
"os": "ubuntu-latest",
|
||||
"cc": "gcc",
|
||||
"cxx": "g++"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "Linux x86_64 (Clang)",
|
||||
"artifact": "virtualjaguar_libretro.so",
|
||||
"os": "ubuntu-latest",
|
||||
"cc": "clang",
|
||||
"cxx": "clang++"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "Linux aarch64",
|
||||
"artifact": "virtualjaguar_libretro.so",
|
||||
"os": "ubuntu-24.04-arm",
|
||||
"cc": "gcc",
|
||||
"cxx": "g++"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "Linux i686",
|
||||
"artifact": "virtualjaguar_libretro.so",
|
||||
"os": "ubuntu-latest",
|
||||
"cc": "gcc -m32",
|
||||
"cxx": "g++ -m32",
|
||||
"multilib": true
|
||||
},
|
||||
{
|
||||
"displayTargetName": "macOS arm64 (Clang)",
|
||||
"artifact": "virtualjaguar_libretro.dylib",
|
||||
"os": "macos-latest",
|
||||
"cc": "clang",
|
||||
"cxx": "clang++"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "macOS x86_64 (Clang, cross)",
|
||||
"artifact": "virtualjaguar_libretro.dylib",
|
||||
"os": "macos-latest",
|
||||
"cc": "clang -arch x86_64",
|
||||
"cxx": "clang++ -arch x86_64",
|
||||
"make_extra": "BLITTER_SIMD=sse2",
|
||||
"cross": true,
|
||||
"release_platform": "macos-x86_64"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "Windows x86_64 (MSYS2)",
|
||||
"artifact": "virtualjaguar_libretro.dll",
|
||||
"os": "windows-latest",
|
||||
"cc": "gcc",
|
||||
"cxx": "g++",
|
||||
"shell": "msys2 {0}",
|
||||
"msystem": "MINGW64",
|
||||
"msys2_packages": "mingw-w64-x86_64-gcc make"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "Windows i686 (MSYS2)",
|
||||
"artifact": "virtualjaguar_libretro.dll",
|
||||
"os": "windows-latest",
|
||||
"cc": "gcc",
|
||||
"cxx": "g++",
|
||||
"shell": "msys2 {0}",
|
||||
"msystem": "MINGW32",
|
||||
"msys2_packages": "mingw-w64-i686-gcc make"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "Emscripten (WASM)",
|
||||
"artifact": "virtualjaguar_libretro_emscripten.bc",
|
||||
"os": "ubuntu-latest",
|
||||
"emscripten": true,
|
||||
"release_platform": "emscripten-wasm"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "Android arm64-v8a",
|
||||
"artifact": "libs/arm64-v8a/libretro.so",
|
||||
"os": "ubuntu-latest",
|
||||
"android": true,
|
||||
"android_abi": "arm64-v8a",
|
||||
"android_simd": "neon",
|
||||
"release_platform": "android-arm64-v8a"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "Android armeabi-v7a",
|
||||
"artifact": "libs/armeabi-v7a/libretro.so",
|
||||
"os": "ubuntu-latest",
|
||||
"android": true,
|
||||
"android_abi": "armeabi-v7a",
|
||||
"android_simd": "neon",
|
||||
"release_platform": "android-armeabi-v7a"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "Android x86_64",
|
||||
"artifact": "libs/x86_64/libretro.so",
|
||||
"os": "ubuntu-latest",
|
||||
"android": true,
|
||||
"android_abi": "x86_64",
|
||||
"android_simd": "sse2",
|
||||
"release_platform": "android-x86_64"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "Android x86",
|
||||
"artifact": "libs/x86/libretro.so",
|
||||
"os": "ubuntu-latest",
|
||||
"android": true,
|
||||
"android_abi": "x86",
|
||||
"android_simd": "sse2",
|
||||
"release_platform": "android-x86"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "iOS arm64",
|
||||
"artifact": "virtualjaguar_libretro_ios.dylib",
|
||||
"os": "macos-latest",
|
||||
"make_platform": "ios-arm64",
|
||||
"cross": true,
|
||||
"release_platform": "ios-arm64"
|
||||
},
|
||||
{
|
||||
"displayTargetName": "tvOS arm64",
|
||||
"artifact": "virtualjaguar_libretro_tvos.dylib",
|
||||
"os": "macos-latest",
|
||||
"make_platform": "tvos-arm64",
|
||||
"cross": true,
|
||||
"release_platform": "tvos-arm64"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,52 @@
|
||||
// Compile-only C/C++ rows share production artifacts; native/Clang/MSVC/sanitizer/test-ABI builds stay independent.
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const { plan, waitForPackages } = require('./release-packages.cjs');
|
||||
const MATRIX = require('../ci/core-matrix.json');
|
||||
const CORES = [
|
||||
...MATRIX.filter(c => c.release_platform).map(c => ({ platform: c.release_platform,
|
||||
name: c.displayTargetName, file: path.basename(c.artifact) })),
|
||||
{ platform: 'vita', name: 'PS Vita', file: 'virtualjaguar_libretro_vita.a' },
|
||||
{ platform: 'switch', name: 'Nintendo Switch', file: 'virtualjaguar_libretro_libnx.a' },
|
||||
];
|
||||
|
||||
async function planCores(options) {
|
||||
const reuse = await plan(options);
|
||||
options.core.setOutput('matrix', JSON.stringify(MATRIX.filter(c => !reuse || !c.release_platform)));
|
||||
}
|
||||
|
||||
function coreStatus(jobs, artifacts) {
|
||||
const failed = jobs.find(job => CORES.some(c => job.name === `build-${c.platform}`) &&
|
||||
job.status === 'completed' && job.conclusion !== 'success');
|
||||
if (failed) throw new Error(`Release production build did not pass: ${failed.name} (${failed.conclusion}).`);
|
||||
for (const core of CORES) {
|
||||
const name = `build-${core.platform}`;
|
||||
const job = jobs.find(j => j.name === name);
|
||||
if (!job || job.status !== 'completed') return false;
|
||||
for (const name of [`ci-core-${core.platform}`, `virtualjaguar_libretro-${core.platform}`]) {
|
||||
if (!artifacts.some(a => a.name === name && !a.expired && a.size_in_bytes > 0))
|
||||
throw new Error(`Missing, empty or expired production artifact: ${name}.`);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function waitForCores(options) {
|
||||
return waitForPackages({ ...options, checkStatus: coreStatus, subject: 'production core' });
|
||||
}
|
||||
|
||||
function verifyDownloads(root, head) {
|
||||
for (const core of CORES) {
|
||||
const dir = path.join(root, `ci-core-${core.platform}`);
|
||||
const meta = JSON.parse(fs.readFileSync(path.join(dir, 'BUILD.json'), 'utf8'));
|
||||
if (meta.head_sha !== head || meta.platform !== core.platform || meta.file !== core.file ||
|
||||
meta.test_exports !== 0 || meta.release_debug_info !== 1)
|
||||
throw new Error(`Wrong commit, platform, filename or production ABI for ${core.platform}.`);
|
||||
const data = fs.readFileSync(path.join(dir, core.file));
|
||||
if (!data.length || crypto.createHash('sha256').update(data).digest('hex') !== meta.sha256)
|
||||
throw new Error(`Production binary checksum mismatch for ${core.platform}.`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MATRIX, CORES, planCores, coreStatus, waitForCores, verifyDownloads };
|
||||
@@ -22,13 +22,14 @@ async function plan({ github, context, core }) {
|
||||
const pr = context.payload.pull_request;
|
||||
const fullName = `${context.repo.owner}/${context.repo.repo}`;
|
||||
if (context.eventName !== 'pull_request' || pr?.head.repo?.full_name !== fullName ||
|
||||
!pr.head.ref.startsWith('release/')) return;
|
||||
!pr.head.ref.startsWith('release/')) return false;
|
||||
if (!await sameTree(github, context.repo, pr.head.sha, context.sha)) {
|
||||
core.info('The PR merge tree differs from the release head: keep independent PR package builds.');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
core.setOutput('reuse', 'true');
|
||||
core.info(`Share Release push packages for ${pr.head.ref}@${pr.head.sha}; the PR merge tree is identical.`);
|
||||
core.info(`Share Release push artifacts for ${pr.head.ref}@${pr.head.sha}; the PR merge tree is identical.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectRun(runs, pr, repo) {
|
||||
@@ -58,7 +59,7 @@ async function assertCurrentMerge(github, repo, pr) {
|
||||
}
|
||||
|
||||
async function waitForPackages({ github, context, core, sleep = ms => new Promise(resolve => setTimeout(resolve, ms)),
|
||||
now = Date.now, timeoutMs = 345 * 60000 }) {
|
||||
now = Date.now, timeoutMs = 345 * 60000, checkStatus = packageStatus, subject = 'package' }) {
|
||||
const pr = context.payload.pull_request;
|
||||
const repo = context.repo, fullName = `${repo.owner}/${repo.repo}`;
|
||||
// Recheck independently: the workflow must never turn a fork or another branch into trusted Release artifacts.
|
||||
@@ -77,26 +78,26 @@ async function waitForPackages({ github, context, core, sleep = ms => new Promis
|
||||
{ ...repo, run_id: run.id, filter: 'latest', per_page: 100 });
|
||||
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts,
|
||||
{ ...repo, run_id: run.id, per_page: 100 });
|
||||
if (packageStatus(jobs, artifacts)) {
|
||||
if (checkStatus(jobs, artifacts)) {
|
||||
// A rerun can replace jobs while the API pages are being read. Never approve an older attempt's evidence.
|
||||
const { data: current } = await github.rest.actions.getWorkflowRun({ ...repo, run_id: run.id });
|
||||
if (current.run_attempt === run.run_attempt && current.head_sha === pr.head.sha) {
|
||||
await assertCurrentMerge(github, repo, pr);
|
||||
core.info(`All package checks and six artifact groups verified: ${run.html_url}`);
|
||||
await core.summary.addRaw(`Shared Release packages: [run ${run.id}](${run.html_url}), head \`${pr.head.sha}\`.\n`).write();
|
||||
core.info(`All ${subject} checks and required artifact groups verified: ${run.html_url}`);
|
||||
await core.summary.addRaw(`Shared Release ${subject} artifacts: [run ${run.id}](${run.html_url}), head \`${pr.head.sha}\`.\n`).write();
|
||||
return;
|
||||
}
|
||||
} else if (run.status === 'completed') {
|
||||
throw new Error(`Release run ${run.id} ended without all required package checks/artifacts.`);
|
||||
throw new Error(`Release run ${run.id} ended without all required ${subject} checks/artifacts.`);
|
||||
}
|
||||
}
|
||||
const status = run ? `Waiting for package checks in Release run ${run.id} (attempt ${run.run_attempt}).` :
|
||||
const status = run ? `Waiting for ${subject} checks in Release run ${run.id} (attempt ${run.run_attempt}).` :
|
||||
`Waiting for the Release push run for ${pr.head.sha}.`;
|
||||
if (status !== previous) core.info(status);
|
||||
previous = status;
|
||||
await sleep(60000);
|
||||
}
|
||||
throw new Error('Timed out waiting for this head\'s Release packages; no package check was bypassed.');
|
||||
throw new Error(`Timed out waiting for this head's Release ${subject} artifacts; no check was bypassed.`);
|
||||
}
|
||||
|
||||
module.exports = { ARTIFACTS, PACKAGE_JOBS, sameTree, plan, selectRun, packageStatus, waitForPackages };
|
||||
@@ -0,0 +1,36 @@
|
||||
# Reject toolchain/architecture drift before omitting any compile-only PR builds.
|
||||
require 'json'
|
||||
require 'yaml'
|
||||
root = File.expand_path('../../..', __dir__)
|
||||
read_workflow = ->(name) { YAML.safe_load(File.read("#{root}/.github/workflows/#{name}.yml"), aliases: true).fetch('jobs') }
|
||||
cpp = read_workflow.call('c-cpp')
|
||||
release = read_workflow.call('release')
|
||||
matrix = JSON.parse(File.read("#{root}/.github/ci/core-matrix.json"))
|
||||
rows = release.fetch('build').fetch('strategy').fetch('matrix').fetch('config')
|
||||
shared = matrix.select { |config| config['release_platform'] }
|
||||
raise 'Expected eight shared matrix rows' unless shared.length == 8
|
||||
shared.each do |config|
|
||||
peer = rows.find { |row| row['platform'] == config['release_platform'] }
|
||||
raise "Missing Release row: #{config['release_platform']}" unless peer
|
||||
%w[artifact os cc cxx make_extra emscripten android android_abi android_simd make_platform].each do |key|
|
||||
raise "#{config['displayTargetName']}: mismatched #{key}" unless config[key] == peer[key]
|
||||
end
|
||||
end
|
||||
%w[vita-build switch-build].each do |job|
|
||||
raise "#{job}: SDK container differs" unless cpp.fetch(job).fetch('container') == release.fetch(job).fetch('container')
|
||||
end
|
||||
steps = ->(job) { job.fetch('steps') }
|
||||
%w[Set\ up\ Emscripten Set\ up\ Android\ NDK].each do |name|
|
||||
a = steps.call(cpp.fetch('build')).find { |step| step['name'] == name }
|
||||
b = steps.call(release.fetch('build')).find { |step| step['name'] == name }
|
||||
raise "#{name}: SDK setup/version differs" unless a['uses'] == b['uses'] && a['with'] == b['with']
|
||||
end
|
||||
%w[build vita-build switch-build].each do |name|
|
||||
steps.call(release.fetch(name)).select { |step| step['name']&.match?(/\ABuild(?: \(|\z)/) }.each do |step|
|
||||
env = step.fetch('env')
|
||||
raise "#{name}: canonical core is not production" unless env['TEST_EXPORTS'] == '0' && env['RELEASE_DEBUG_INFO'] == '1'
|
||||
end
|
||||
end
|
||||
raise 'Native Clang row removed' unless matrix.any? { |row| row['cc'] == 'clang' && !row['release_platform'] }
|
||||
%w[msvc-check sanitizers coverage clang-tidy].each { |job| cpp.fetch(job) }
|
||||
puts 'Core workflow contract: eight matching toolchains, two matching SDK containers; native compiler, sanitizer and ABI coverage retained.'
|
||||
@@ -0,0 +1,155 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { execFileSync, spawnSync } = require('node:child_process');
|
||||
const { MATRIX, CORES, planCores, coreStatus, verifyDownloads } = require('../release-cores.cjs');
|
||||
const root = path.resolve(__dirname, '../../..');
|
||||
const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim();
|
||||
function complete() {
|
||||
return { jobs: CORES.map(c => ({ name: `build-${c.platform}`, status: 'completed', conclusion: 'success' })),
|
||||
artifacts: CORES.flatMap(c => [`ci-core-${c.platform}`, `virtualjaguar_libretro-${c.platform}`]
|
||||
.map(name => ({ name, expired: false, size_in_bytes: 123 }))) };
|
||||
}
|
||||
async function route(same = true, fork = false) {
|
||||
const out = {};
|
||||
await planCores({ context: { repo: { owner: 'o', repo: 'r' }, eventName: 'pull_request', sha: 'merge',
|
||||
payload: { pull_request: { head: { sha: 'head', ref: 'release/v1', repo: { full_name: fork ? 'fork/r' : 'o/r' } } } } },
|
||||
github: { rest: { git: { getCommit: async ({ commit_sha }) => ({ data: { tree: {
|
||||
sha: same || commit_sha === 'head' ? 'same' : 'different' } } }) } } },
|
||||
core: { setOutput: (k,v) => { out[k] = v; }, info: () => {} } });
|
||||
return { ...out, matrix: JSON.parse(out.matrix) };
|
||||
}
|
||||
test('matching release PR retains all seven native compiler/test rows and shares only eight matrix rows plus consoles', async () => {
|
||||
const r = await route(); assert.equal(r.reuse, 'true'); assert.equal(r.matrix.length, 7);
|
||||
assert.equal(MATRIX.length, 15); assert.equal(CORES.length, 10);
|
||||
assert.deepEqual(r.matrix, MATRIX.filter(c => !c.release_platform));
|
||||
for (const name of ['Linux x86_64 (GCC)', 'Linux x86_64 (Clang)', 'Linux aarch64', 'Linux i686',
|
||||
'macOS arm64 (Clang)', 'Windows x86_64 (MSYS2)', 'Windows i686 (MSYS2)'])
|
||||
assert(r.matrix.some(c => c.displayTargetName === name));
|
||||
});
|
||||
test('different merge trees and forks keep all fifteen original matrix configurations', async () => {
|
||||
for (const r of [await route(false), await route(true, true)]) {
|
||||
assert.equal(r.reuse, 'false'); assert.deepEqual(r.matrix, MATRIX);
|
||||
}
|
||||
});
|
||||
test('all ten production jobs and BOTH raw and distributable artifact groups are required', () => {
|
||||
const f = complete(); assert(coreStatus(f.jobs, f.artifacts));
|
||||
for (const c of CORES) {
|
||||
const fewer = f.jobs.filter(j => j.name !== `build-${c.platform}`);
|
||||
assert.equal(coreStatus(fewer, f.artifacts), false);
|
||||
for (const name of [`ci-core-${c.platform}`, `virtualjaguar_libretro-${c.platform}`])
|
||||
assert.throws(() => coreStatus(f.jobs, f.artifacts.filter(a => a.name !== name)), /Missing/);
|
||||
}
|
||||
});
|
||||
test('failures, cancelled jobs, skipped jobs, stale and empty artifacts cannot approve a core', () => {
|
||||
for (const conclusion of ['failure', 'cancelled', 'skipped', 'timed_out']) {
|
||||
const f = complete(); f.jobs[0].conclusion = conclusion;
|
||||
assert.throws(() => coreStatus(f.jobs, f.artifacts), /did not pass/);
|
||||
}
|
||||
for (const change of [a => { a.expired = true; }, a => { a.size_in_bytes = 0; }]) {
|
||||
const f = complete(); change(f.artifacts[0]); assert.throws(() => coreStatus(f.jobs, f.artifacts), /expired/);
|
||||
}
|
||||
});
|
||||
function staged() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vj-core-contract-'));
|
||||
for (const c of CORES) {
|
||||
const src = path.join(dir, c.platform, c.file); fs.mkdirSync(path.dirname(src), { recursive: true });
|
||||
fs.writeFileSync(src, `fixture production bytes for ${c.platform}`);
|
||||
execFileSync('bash', [path.join(root, 'scripts/stage-ci-core.sh'), c.platform, src, path.join(dir, `ci-core-${c.platform}`)],
|
||||
{ cwd: root, env: { ...process.env, GITHUB_SHA: head, TEST_EXPORTS: '0', RELEASE_DEBUG_INFO: '1' } });
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
test('real staging shell and download verifier preserve production bytes and exact provenance', () => {
|
||||
const dir = staged(); verifyDownloads(dir, head);
|
||||
for (const c of CORES)
|
||||
assert.deepEqual(fs.readFileSync(path.join(dir, c.platform, c.file)), fs.readFileSync(path.join(dir, `ci-core-${c.platform}`, c.file)));
|
||||
});
|
||||
test('wrong head, platform, filename, test ABI and build flags are rejected after download', () => {
|
||||
const dir = staged(), c = CORES[0], file = path.join(dir, `ci-core-${c.platform}`, 'BUILD.json');
|
||||
const meta = JSON.parse(fs.readFileSync(file));
|
||||
for (const delta of [{ head_sha: 'old' }, { platform: 'wrong' }, { file: 'other.so' }, { test_exports: 1 }, { release_debug_info: 0 }]) {
|
||||
fs.writeFileSync(file, JSON.stringify({ ...meta, ...delta }));
|
||||
assert.throws(() => verifyDownloads(dir, head), /Wrong commit/);
|
||||
}
|
||||
fs.writeFileSync(file, JSON.stringify(meta)); verifyDownloads(dir, head);
|
||||
});
|
||||
test('tampered or empty production bytes fail checksum verification', () => {
|
||||
const dir = staged(), c = CORES[0], file = path.join(dir, `ci-core-${c.platform}`, c.file);
|
||||
for (const bytes of ['changed', '']) {
|
||||
fs.writeFileSync(file, bytes); assert.throws(() => verifyDownloads(dir, head), /checksum mismatch/);
|
||||
}
|
||||
});
|
||||
test('staging rejects the wide test ABI and a wrong workflow commit', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vj-core-stage-')), c = CORES[0], src = path.join(dir, c.file);
|
||||
fs.writeFileSync(src, 'binary fixture');
|
||||
for (const delta of [{ TEST_EXPORTS: '1' }, { GITHUB_SHA: 'old' }, { RELEASE_DEBUG_INFO: '0' }]) {
|
||||
const r = spawnSync('bash', [path.join(root, 'scripts/stage-ci-core.sh'), c.platform, src, path.join(dir, 'out')],
|
||||
{ cwd: root, env: { ...process.env, GITHUB_SHA: head, TEST_EXPORTS: '0', RELEASE_DEBUG_INFO: '1', ...delta } });
|
||||
assert.notEqual(r.status, 0);
|
||||
}
|
||||
});
|
||||
test('all original compile-only artifact aliases remain in C/C++ CI, with native tests and ABI transitions', () => {
|
||||
const ci = fs.readFileSync(path.join(root, '.github/workflows/c-cpp.yml'), 'utf8');
|
||||
for (const c of CORES) { assert(ci.includes(`name: '${c.name}'`)); assert(ci.includes(`shared-cores/ci-core-${c.platform}/${c.file}`)); }
|
||||
for (const gate of ['Run cheat engine unit tests', 'Verify TEST_EXPORTS -> production mode switch relinks',
|
||||
'MSVC ${{ matrix.arch }} compilation check', 'AddressSanitizer + UndefinedBehaviorSanitizer',
|
||||
'Code coverage (gcov + codecov)', 'clang-tidy on changed files']) assert(ci.includes(gate), gate);
|
||||
});
|
||||
test('both Release publishers exclude raw intermediates; shipped core and debug artifact upload stays intact', () => {
|
||||
const release = fs.readFileSync(path.join(root, '.github/workflows/release.yml'), 'utf8');
|
||||
assert.equal((release.match(/-not -path 'artifacts\/ci-core-\*'/g) || []).length, 2);
|
||||
assert(release.includes('name: virtualjaguar_libretro-${{ matrix.config.platform }}'));
|
||||
assert(release.includes('path: dist/'));
|
||||
assert(release.includes('Verify compile-only Mach-O production exports'));
|
||||
assert(release.includes('bash scripts/build-ndk-ci.sh'));
|
||||
});
|
||||
function ndkCase(mode) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vj-ndk-check-'));
|
||||
fs.mkdirSync(path.join(dir, 'scripts')); fs.writeFileSync(path.join(dir, 'scripts/gen-version-h.sh'), '#!/bin/sh\nexit 0\n');
|
||||
const sdk = path.join(dir, 'sdk'); fs.mkdirSync(sdk);
|
||||
fs.writeFileSync(path.join(sdk, 'ndk-build'), `#!/bin/sh
|
||||
case "$*" in
|
||||
*-n*) echo 'clang -c src/tom/blitter.c ${mode === 'missing-define' ? '' : '-DBLITTER_SIMD_NEON'}' ;;
|
||||
*) mkdir -p obj/local/arm64-v8a; touch obj/local/arm64-v8a/blitter_simd_${mode === 'wrong-object' ? 'scalar' : 'neon'}.o ;;
|
||||
esac
|
||||
`, { mode: 0o755 });
|
||||
return spawnSync('bash', [path.join(root, 'scripts/build-ndk-ci.sh'), sdk, 'arm64-v8a', 'neon'], { cwd: dir, encoding: 'utf8' });
|
||||
}
|
||||
test('shared NDK builder enforces compile definition and selected SIMD object', () => {
|
||||
assert.equal(ndkCase('good').status, 0);
|
||||
assert.notEqual(ndkCase('missing-define').status, 0);
|
||||
assert.notEqual(ndkCase('wrong-object').status, 0);
|
||||
});
|
||||
test('the shared wait loop requires core evidence, not the independent app package set', async () => {
|
||||
const { waitForCores } = require('../release-cores.cjs');
|
||||
const f = complete(); let approved = false;
|
||||
const pr = { number: 1, head: { sha: 'head', ref: 'release/v1', repo: { full_name: 'o/r' } }, merge_commit_sha: 'merge' };
|
||||
const run = { id: 42, head_sha: 'head', head_branch: 'release/v1', head_repository: { full_name: 'o/r' },
|
||||
event: 'push', run_attempt: 1, status: 'in_progress', html_url: 'https://example/42' };
|
||||
const github = { rest: {
|
||||
git: { getCommit: async () => ({ data: { tree: { sha: 'tree' } } }) },
|
||||
pulls: { get: async () => ({ data: pr }) },
|
||||
actions: { listWorkflowRuns: async () => ({ data: { workflow_runs: [run] } }),
|
||||
listJobsForWorkflowRun: 'jobs', listWorkflowRunArtifacts: 'artifacts',
|
||||
getWorkflowRun: async () => ({ data: run }) },
|
||||
}, paginate: async method => method === 'jobs' ? f.jobs : f.artifacts };
|
||||
const options = { github, context: { eventName: 'pull_request', repo: { owner: 'o', repo: 'r' }, payload: { pull_request: pr } },
|
||||
core: { setOutput: () => {}, info: () => {}, summary: { addRaw: () => ({ write: async () => { approved = true; } }) } } };
|
||||
await waitForCores(options); assert(approved);
|
||||
approved = false; f.jobs[0].conclusion = 'failure';
|
||||
await assert.rejects(waitForCores(options), /did not pass/); assert.equal(approved, false);
|
||||
});
|
||||
test('release staging excludes raw CI cores and runtimes while keeping shipped binaries and debug archives', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vj-release-stage-'));
|
||||
for (const name of ['ci-core-vita/private-raw.a', 'librashader-aarch64/runtime.so',
|
||||
'virtualjaguar_libretro-vita/shipped.a', 'virtualjaguar_libretro-vita/shipped-debug.tar.gz']) {
|
||||
const p = path.join(dir, 'artifacts', name); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, 'fixture');
|
||||
}
|
||||
const out = execFileSync('find', ['artifacts/', '-mindepth', '2', '-type', 'f', '-not', '-path', 'artifacts/librashader-*',
|
||||
'-not', '-path', 'artifacts/ci-core-*', '-print'], { cwd: dir, encoding: 'utf8' });
|
||||
assert(out.includes('shipped.a')); assert(out.includes('shipped-debug.tar.gz'));
|
||||
assert(!out.includes('private-raw')); assert(!out.includes('runtime.so'));
|
||||
});
|
||||
+147
-148
@@ -15,123 +15,151 @@ concurrency:
|
||||
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:
|
||||
# Linux
|
||||
- displayTargetName: 'Linux x86_64 (GCC)'
|
||||
artifact: 'virtualjaguar_libretro.so'
|
||||
os: ubuntu-latest
|
||||
cc: 'gcc'
|
||||
cxx: 'g++'
|
||||
|
||||
- displayTargetName: 'Linux x86_64 (Clang)'
|
||||
artifact: 'virtualjaguar_libretro.so'
|
||||
os: ubuntu-latest
|
||||
cc: 'clang'
|
||||
cxx: 'clang++'
|
||||
|
||||
- displayTargetName: 'Linux aarch64'
|
||||
artifact: 'virtualjaguar_libretro.so'
|
||||
os: ubuntu-24.04-arm
|
||||
cc: 'gcc'
|
||||
cxx: 'g++'
|
||||
|
||||
- displayTargetName: 'Linux i686'
|
||||
artifact: 'virtualjaguar_libretro.so'
|
||||
os: ubuntu-latest
|
||||
cc: 'gcc -m32'
|
||||
cxx: 'g++ -m32'
|
||||
multilib: true
|
||||
|
||||
# macOS
|
||||
- displayTargetName: 'macOS arm64 (Clang)'
|
||||
artifact: 'virtualjaguar_libretro.dylib'
|
||||
os: macos-latest
|
||||
cc: 'clang'
|
||||
cxx: 'clang++'
|
||||
|
||||
# Cross-compiled from macos-latest (arm64) -- avoids the
|
||||
# scarce macos-13 Intel runner pool. See release.yml for
|
||||
# rationale.
|
||||
- displayTargetName: 'macOS x86_64 (Clang, cross)'
|
||||
artifact: 'virtualjaguar_libretro.dylib'
|
||||
os: macos-latest
|
||||
cc: 'clang -arch x86_64'
|
||||
cxx: 'clang++ -arch x86_64'
|
||||
make_extra: 'BLITTER_SIMD=sse2'
|
||||
cross: true
|
||||
|
||||
# Windows (MinGW)
|
||||
- displayTargetName: 'Windows x86_64 (MSYS2)'
|
||||
artifact: 'virtualjaguar_libretro.dll'
|
||||
os: windows-latest
|
||||
cc: 'gcc'
|
||||
cxx: 'g++'
|
||||
shell: 'msys2 {0}'
|
||||
msystem: 'MINGW64'
|
||||
msys2_packages: 'mingw-w64-x86_64-gcc make'
|
||||
|
||||
- displayTargetName: 'Windows i686 (MSYS2)'
|
||||
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)
|
||||
- displayTargetName: 'Emscripten (WASM)'
|
||||
artifact: 'virtualjaguar_libretro_emscripten.bc'
|
||||
os: ubuntu-latest
|
||||
emscripten: true
|
||||
|
||||
# Android NDK
|
||||
- displayTargetName: 'Android arm64-v8a'
|
||||
artifact: 'libs/arm64-v8a/libretro.so'
|
||||
os: ubuntu-latest
|
||||
android: true
|
||||
android_abi: 'arm64-v8a'
|
||||
android_simd: 'neon'
|
||||
|
||||
- displayTargetName: 'Android armeabi-v7a'
|
||||
artifact: 'libs/armeabi-v7a/libretro.so'
|
||||
os: ubuntu-latest
|
||||
android: true
|
||||
android_abi: 'armeabi-v7a'
|
||||
android_simd: 'neon'
|
||||
|
||||
- displayTargetName: 'Android x86_64'
|
||||
artifact: 'libs/x86_64/libretro.so'
|
||||
os: ubuntu-latest
|
||||
android: true
|
||||
android_abi: 'x86_64'
|
||||
android_simd: 'sse2'
|
||||
|
||||
- displayTargetName: 'Android x86'
|
||||
artifact: 'libs/x86/libretro.so'
|
||||
os: ubuntu-latest
|
||||
android: true
|
||||
android_abi: 'x86'
|
||||
android_simd: 'sse2'
|
||||
|
||||
# iOS / tvOS
|
||||
- displayTargetName: 'iOS arm64'
|
||||
artifact: 'virtualjaguar_libretro_ios.dylib'
|
||||
os: macos-latest
|
||||
make_platform: 'ios-arm64'
|
||||
cross: true
|
||||
|
||||
- displayTargetName: 'tvOS arm64'
|
||||
artifact: 'virtualjaguar_libretro_tvos.dylib'
|
||||
os: macos-latest
|
||||
make_platform: 'tvos-arm64'
|
||||
cross: true
|
||||
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:
|
||||
@@ -181,43 +209,7 @@ jobs:
|
||||
|
||||
- name: Build (Android NDK)
|
||||
if: matrix.config.android
|
||||
run: |
|
||||
# ndk-build uses jni/Android.mk (not the project Makefile),
|
||||
# so the parse-time version.h regen in Makefile doesn't run.
|
||||
bash scripts/gen-version-h.sh
|
||||
|
||||
NDK="${{ steps.setup-ndk.outputs.ndk-path }}"
|
||||
ABI="${{ matrix.config.android_abi }}"
|
||||
SIMD="${{ matrix.config.android_simd }}"
|
||||
UPPER=$(echo "$SIMD" | tr '[:lower:]' '[:upper:]')
|
||||
|
||||
# Dry run first, while nothing is built, so make actually prints
|
||||
# the compile lines. blitter.c MUST carry -DBLITTER_SIMD_<ARCH>:
|
||||
# without it blitter.c inlines the scalar ops and still links
|
||||
# happily against the neon/sse2 vtable, so a missing define is a
|
||||
# silent slow path that no build failure would reveal.
|
||||
"$NDK/ndk-build" APP_ABI="$ABI" -n > ndk-dry.log 2>&1
|
||||
BLIT_LINES=$(grep -c '[/ ]blitter\.c' ndk-dry.log || true)
|
||||
if [ "$BLIT_LINES" = "0" ]; then
|
||||
echo "::error::ndk-build -n printed no compile line for blitter.c; this check needs rework"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep '[/ ]blitter\.c' ndk-dry.log | grep -q -- "-DBLITTER_SIMD_${UPPER}"; then
|
||||
echo "::error::blitter.c is not compiled with -DBLITTER_SIMD_${UPPER} on ${ABI}"
|
||||
grep '[/ ]blitter\.c' ndk-dry.log | head -3
|
||||
exit 1
|
||||
fi
|
||||
echo "==> ${ABI}: blitter.c carries -DBLITTER_SIMD_${UPPER}"
|
||||
|
||||
"$NDK/ndk-build" APP_ABI="$ABI" -j4
|
||||
|
||||
# And the arch .c that define has to agree with.
|
||||
FOUND=$(find "obj/local/$ABI" -name 'blitter_simd_*.o' -exec basename {} \; | sort | tr '\n' ' ')
|
||||
if [ "$FOUND" != "blitter_simd_${SIMD}.o " ]; then
|
||||
echo "::error::expected blitter_simd_${SIMD}.o on ${ABI}, found: ${FOUND:-<none>}"
|
||||
exit 1
|
||||
fi
|
||||
echo "==> ${ABI}: built ${FOUND}"
|
||||
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
|
||||
@@ -727,6 +719,8 @@ jobs:
|
||||
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:
|
||||
@@ -735,6 +729,8 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
RELEASE_DEBUG_INFO: '1'
|
||||
run: make -j4 platform=vita
|
||||
|
||||
- name: Upload artifact
|
||||
@@ -745,6 +741,8 @@ jobs:
|
||||
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:
|
||||
@@ -756,6 +754,7 @@ jobs:
|
||||
run: make -j4 platform=libnx
|
||||
env:
|
||||
DEVKITPRO: /opt/devkitpro
|
||||
RELEASE_DEBUG_INFO: '1'
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
@@ -226,24 +226,28 @@ jobs:
|
||||
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
|
||||
@@ -311,30 +315,56 @@ jobs:
|
||||
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: |
|
||||
# ndk-build uses jni/Android.mk (not the project Makefile),
|
||||
# so the parse-time version.h regen in Makefile doesn't run.
|
||||
bash scripts/gen-version-h.sh
|
||||
${{ steps.setup-ndk.outputs.ndk-path }}/ndk-build \
|
||||
APP_ABI=${{ matrix.config.android_abi }} -j4
|
||||
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
|
||||
@@ -510,8 +540,24 @@ jobs:
|
||||
- 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
|
||||
@@ -549,8 +595,24 @@ jobs:
|
||||
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
|
||||
@@ -642,7 +704,7 @@ jobs:
|
||||
# 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-*' -exec cp {} release/ \;
|
||||
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.
|
||||
@@ -788,7 +850,7 @@ jobs:
|
||||
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-*' -exec cp {} release/ \;
|
||||
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
|
||||
|
||||
@@ -151,3 +151,34 @@ runner must use a unique custom label with default labels disabled, preserve the
|
||||
requested OS/architecture, and be removed together with the variable after its
|
||||
checks finish. Do not map Ubuntu 22.04 packaging jobs onto Ubuntu 24.04: that would
|
||||
change the supported glibc floor.
|
||||
|
||||
## Shared production core builds
|
||||
|
||||
For same-repository `release/**` PRs with a merge tree identical to the release head,
|
||||
C/C++ CI shares ten compile-only production builds from the exact-head Release push run:
|
||||
macOS Intel, WASM, four Android ABIs, iOS, tvOS, Vita and Switch. Other PRs, forks,
|
||||
manual runs and integration-branch pushes keep the full compiler matrix. The configurations
|
||||
live in `.github/ci/core-matrix.json`; a Ruby workflow-contract check rejects mismatched
|
||||
compiler/SDK/architecture settings before any rows are omitted.
|
||||
|
||||
The seven native C/C++ matrix rows, GCC/Clang differences, MSVC checks, coverage,
|
||||
ASan/UBSan, clang-tidy and production -> `TEST_EXPORTS=1` -> production mode-switch
|
||||
coverage stay independent. Compile-only fallback builds use the same `RELEASE_DEBUG_INFO=1`
|
||||
configuration as Release (adds debug information to the optimized production build).
|
||||
Android's compile-definition and selected-SIMD-object assertions run through
|
||||
`scripts/build-ndk-ci.sh` in both workflows. Compile-only Mach-O production exports are
|
||||
checked before Release uploads a shared binary.
|
||||
|
||||
Release retains an unstripped `ci-core-<platform>` intermediate before packaging/signing.
|
||||
`BUILD.json` records the exact commit, platform, filename, production ABI setting and
|
||||
SHA-256. C/C++ CI requires successful production jobs, both raw and distributable artifact
|
||||
groups, and verifies the downloaded metadata/checksums before retaining all ten original
|
||||
C/C++ artifact names. Failed, cancelled, skipped, stale, missing or mismatched evidence
|
||||
cannot make the shared check green. The hosted waiting job does not occupy the temporary
|
||||
runner needed by Release metadata jobs.
|
||||
|
||||
These intermediates have 14-day retention and are explicitly excluded by **both** release
|
||||
publishers' staging filters. Shipped binaries, split debug archives, their names and checksum
|
||||
collection remain unchanged. The C/C++ artifact aliases retain their normal artifact retention.
|
||||
The shared core verifier and shared app-package verifier must both pass before merging a
|
||||
release PR that uses these routes; a configured skip of duplicate build rows is not sufficient.
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# ndk-build uses jni/Android.mk (not the project Makefile),
|
||||
# so the parse-time version.h regen in Makefile doesn't run.
|
||||
bash scripts/gen-version-h.sh
|
||||
|
||||
NDK=${1:?NDK path required}
|
||||
ABI=${2:?ABI required}
|
||||
SIMD=${3:?SIMD required}
|
||||
UPPER=$(echo "$SIMD" | tr '[:lower:]' '[:upper:]')
|
||||
|
||||
# Dry run first, while nothing is built, so make actually prints
|
||||
# the compile lines. blitter.c MUST carry -DBLITTER_SIMD_<ARCH>:
|
||||
# without it blitter.c inlines the scalar ops and still links
|
||||
# happily against the neon/sse2 vtable, so a missing define is a
|
||||
# silent slow path that no build failure would reveal.
|
||||
"$NDK/ndk-build" APP_ABI="$ABI" -n > ndk-dry.log 2>&1
|
||||
BLIT_LINES=$(grep -c '[/ ]blitter\.c' ndk-dry.log || true)
|
||||
if [ "$BLIT_LINES" = "0" ]; then
|
||||
echo "::error::ndk-build -n printed no compile line for blitter.c; this check needs rework"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep '[/ ]blitter\.c' ndk-dry.log | grep -q -- "-DBLITTER_SIMD_${UPPER}"; then
|
||||
echo "::error::blitter.c is not compiled with -DBLITTER_SIMD_${UPPER} on ${ABI}"
|
||||
grep '[/ ]blitter\.c' ndk-dry.log | head -3
|
||||
exit 1
|
||||
fi
|
||||
echo "==> ${ABI}: blitter.c carries -DBLITTER_SIMD_${UPPER}"
|
||||
|
||||
"$NDK/ndk-build" APP_ABI="$ABI" -j4
|
||||
|
||||
# And the arch .c that define has to agree with.
|
||||
FOUND=$(find "obj/local/$ABI" -name 'blitter_simd_*.o' -exec basename {} \; | sort | tr '\n' ' ')
|
||||
if [ "$FOUND" != "blitter_simd_${SIMD}.o " ]; then
|
||||
echo "::error::expected blitter_simd_${SIMD}.o on ${ABI}, found: ${FOUND:-<none>}"
|
||||
exit 1
|
||||
fi
|
||||
echo "==> ${ABI}: built ${FOUND}"
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Preserve the production binary before stripping/signing; never ship these intermediates in release assets.
|
||||
set -euo pipefail
|
||||
platform=${1:?platform required}
|
||||
source_file=${2:?core binary required}
|
||||
out=${3:?output directory required}
|
||||
case "$platform" in
|
||||
macos-x86_64|emscripten-wasm|android-arm64-v8a|android-armeabi-v7a|android-x86_64|android-x86|ios-arm64|tvos-arm64|vita|switch) ;;
|
||||
*) echo "Unsupported shared core: $platform" >&2; exit 1 ;;
|
||||
esac
|
||||
[ "${TEST_EXPORTS:?explicit production ABI required}" = 0 ]
|
||||
[ "${RELEASE_DEBUG_INFO:?explicit release flags required}" = 1 ]
|
||||
[ -s "$source_file" ]
|
||||
[ "$(git rev-parse HEAD)" = "${GITHUB_SHA:?exact workflow SHA required}" ]
|
||||
mkdir -p "$out"
|
||||
file=$(basename "$source_file")
|
||||
command cp -f "$source_file" "$out/$file"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
checksum=$(sha256sum "$out/$file" | cut -d' ' -f1)
|
||||
else
|
||||
checksum=$(shasum -a 256 "$out/$file" | cut -d' ' -f1)
|
||||
fi
|
||||
printf '{"head_sha":"%s","platform":"%s","file":"%s","test_exports":0,"release_debug_info":1,"sha256":"%s"}\n' \
|
||||
"$GITHUB_SHA" "$platform" "$file" "$checksum" > "$out/BUILD.json"
|
||||
Reference in new issue
Block a user