#!/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())