Files
psxrecomp/runtime/tests/test_linux_tcc_bundle_smoke.py
Shokunin 3fee0ae7be Linux tcc: build without the legacy bounds-checking runtime (glibc 2.34+)
tcc 0.9.27's lib/bcheck.c uses __malloc_hook, removed in glibc 2.34, so
the pinned build failed on Ubuntu 24.04 (lib/bcheck.c:738). Shards never use
-b; the build now passes BCHECK_O= unconditionally, keeping every other
libtcc1.a object. The bundle smoke also clears LIBRARY_PATH, CC and
GCC_EXEC_PREFIX and checks shards import no bounds-checking symbols.
Verified: linux_tcc_bundle_smoke_test passes on Ubuntu 24.04 x86_64
(glibc 2.39, gcc + libc6-dev installed, so host headers exist but are not
used) and on Steam Runtime sniper (glibc 2.31).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:02:09 -07:00

176 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""Smoke test of the Linux release toolchain's TinyCC bundle.
tools/release_stage.py builds the pinned tcc 0.9.27 source and stages it with
musl's C headers as overlay_toolchain/tcc/. Its unit tests only check pins and
arguments. This one does what a player's Steam Deck does: it stages the
bundle exactly as a release would (tcc, libtcc1.a, tcc's and musl's headers,
the runtime headers the shard includes), compiles an overlay shard with the
same code shape psxrecomp-game emits, through compile_overlays.py's own tcc
path (shim preamble, call_by_address rewrite, exports), for PGXP flavor 0 and
2, using ONLY the bundle (-nostdinc / -nostdlib), then loads the .so the way
the runtime does and checks overlay_abi() and the exported function.
Linux x86_64 only, and it downloads the pinned sources once (or reuses
PSX_RELEASE_DL_CACHE). Skips (exit 77) elsewhere or without network.
python3 runtime/tests/test_linux_tcc_bundle_smoke.py
ctest -R linux_tcc_bundle_smoke_test
"""
import ctypes
import os
import platform
import shutil
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
TOOLS = os.path.join(ROOT, 'tools')
INCLUDE = os.path.join(ROOT, 'runtime', 'include')
sys.path.insert(0, TOOLS)
SKIP = 77
SHARD_BODY = r'''/* Generated by PSXRecomp - PlayStation 1 Static Recompiler */
#include "psx_runtime.h"
#include "overlay_api.h"
void func_80114000(CPUState* cpu);
void func_8008B4A8(CPUState* cpu);
void func_80114000(CPUState* cpu)
{
#if defined(PSX_ENABLE_BLOCK_CYCLES) && (defined(__GNUC__) || defined(__clang__))
__attribute__((cleanup(psx_cyc_bb_defer_cleanup))) int _psx_cyc_bb_guard = 1;
psx_cyc_bb_defer_begin();
#endif
if (cpu->pc != 0u) {
uint32_t _cont = cpu->pc; cpu->pc = 0;
switch (_cont) {
case 0x80114008u: goto block_80114008;
case 0x80114000u: break;
default: cpu->pc = _cont; psx_native_bad_entry(cpu, 0x80114000u, _cont); return;
}
}
debug_server_log_call_entry(0x80114000u);
block_80114000:
#ifdef PSX_ENABLE_BLOCK_CYCLES
if (psx_slice_block(cpu, 0x80114000u, 2u, 0)) return;
#endif
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_icache_fetch(cpu, 0x80114000u);
#endif
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_cyc_step(cpu, 0x20000000u);
#endif
{ uint32_t _pgx1 = cpu->gpr[29]; cpu->gpr[29] = cpu->gpr[29] + -24;
PGXP_ALU(0x27BDFFE8u, cpu->gpr[29], _pgx1, 0xFFFFFFE8u); }
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_cyc_step(cpu, 0xA0000000u);
#endif
{ uint32_t _pgxa = cpu->gpr[29] + 16; g_debug_last_store_pc = 0x80114004u; psx_store_cycle_barrier(); cpu->write_word(cpu->gpr[29] + 16, cpu->gpr[31]);
PGXP_STORE(0xAFBF0010u, _pgxa, cpu->gpr[31]); }
func_8008B4A8(cpu);
block_80114008:
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_icache_fetch(cpu, 0x80114008u);
#endif
{ uint32_t _pgxa = cpu->gpr[29] + 16; cpu->gpr[31] = psx_cyc_load_word(cpu, cpu->gpr[29] + 16, 31, 0x20000000u);
PGXP_LOAD(0x8FBF0010u, _pgxa, cpu->gpr[31]); }
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_cyc_bb_defer_flush();
#endif
psx_check_interrupts_at(cpu, cpu->gpr[31]);
cpu->pc = cpu->gpr[31]; return;
;
}
'''
def main():
if platform.system() != 'Linux' or platform.machine() not in ('x86_64', 'AMD64'):
print('SKIP: the Linux tcc bundle is x86_64 Linux only')
return SKIP
import release_stage as rs
import compile_overlays as co
pins = rs.TOOLCHAIN_PINS['linux']
work = tempfile.mkdtemp(prefix='psx_tcc_bundle_')
dl = os.environ.get('PSX_RELEASE_DL_CACHE') or os.path.join(work, 'dl')
try:
toolchain = os.path.join(work, 'stage', 'overlay_toolchain')
os.makedirs(toolchain)
try:
rs.stage_linux_tcc(toolchain, dl, pins, log=lambda *_: None)
except rs.StageError as e:
if 'fetch' in str(e):
print('SKIP: cannot download the pinned sources: %s' % e)
return SKIP
raise
# The runtime headers, staged as stage_toolchain stages them.
inc = os.path.join(toolchain, 'include')
os.makedirs(inc)
for h in os.listdir(INCLUDE):
if h.endswith(('.h', '.c.inc')):
shutil.copy2(os.path.join(INCLUDE, h), inc)
tcc = os.path.join(toolchain, 'tcc', 'tcc')
for need in (tcc, os.path.join(toolchain, 'tcc', 'libtcc1.a'),
os.path.join(toolchain, 'tcc', 'COPYING'),
os.path.join(toolchain, 'tcc', 'libc-include', 'COPYRIGHT'),
os.path.join(toolchain, 'tcc', 'libc-include', 'stdint.h')):
assert os.path.isfile(need), 'missing from the bundle: %s' % need
co.load_dispatch_preamble(inc)
fails = 0
for flavor in (0, 2):
src = co.patch_generated_c(SHARD_BODY, 0x80114000, 0x1000)
c_path = os.path.join(work, 'shard_f%d.c' % flavor)
with open(c_path, 'w') as f:
f.write(src)
out = os.path.join(work, 'shard_f%d.so' % flavor)
env_keep = dict(os.environ)
os.environ['PATH'] = '/nonexistent' # no host compiler can help
for v in ('CPATH', 'C_INCLUDE_PATH', 'LIBRARY_PATH', 'CC', 'GCC_EXEC_PREFIX'):
os.environ.pop(v, None)
try:
ok = co._compile_dll_tcc(c_path, out, [inc], flavor, tcc)
finally:
os.environ.clear(); os.environ.update(env_keep)
if not ok or not os.path.isfile(out):
print('FAIL flavor %d: the bundled tcc did not build the shard' % flavor)
fails += 1
continue
lib = ctypes.CDLL(out, mode=getattr(ctypes, 'RTLD_LOCAL', 0))
abi = lib.overlay_abi()
want = co_abi(inc) | (flavor << 16)
if abi != want:
print('FAIL flavor %d: overlay_abi() = 0x%X, want 0x%X' % (flavor, abi, want))
fails += 1
if not hasattr(lib, 'func_80114000'):
print('FAIL flavor %d: func_80114000 not exported' % flavor)
fails += 1
nm = os.popen('nm -D --undefined-only %s' % out).read()
if 'bound' in nm or '__bound' in nm:
print('FAIL flavor %d: the shard references bounds-checking symbols' % flavor)
fails += 1
print('ok flavor %d: shard built by the bundled tcc + musl and loaded (abi 0x%X)'
% (flavor, abi))
return 1 if fails else 0
finally:
shutil.rmtree(work, ignore_errors=True)
def co_abi(inc):
import re
with open(os.path.join(inc, 'overlay_api.h')) as f:
m = re.search(r'#define\s+PSX_OVERLAY_ABI_VERSION\s+(\d+)', f.read())
return int(m.group(1))
if __name__ == '__main__':
sys.exit(main())