From 93ece71c8c2a7c1542a57fe988293a8b3aee749d Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Thu, 1 Oct 2026 11:36:09 +0300 Subject: [PATCH] tools: give tcc the .c.inc fragments a bios module includes A BIOS module's glue.c includes overlay_dispatch_preamble.c.inc and psx_bios_module_glue.c.inc by name. The tcc path compiles against a BOM-stripped copy of the include directory, and that copy held only *.h, so every tcc module build stopped at "include file 'overlay_dispatch_preamble.c.inc' not found" and the runtime fell back to the bundled BIOS. Copy .inc files into the tcc include copy as well. Game overlay shards were not affected: compile_overlays inlines the preamble text instead of including it. --- recompiler/CMakeLists.txt | 10 ++ .../tests/test_tcc_module_include_dir.py | 99 +++++++++++++++++++ tools/compile_overlays.py | 5 +- 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 recompiler/tests/test_tcc_module_include_dir.py diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 56e8c7aa..2e9af723 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -741,6 +741,16 @@ if(BUILD_TESTING) --emitter $ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + # The tcc build of a BIOS module sees a BOM-stripped copy of the + # include directory. That copy must hold the .c.inc fragments the + # module's glue.c includes, or the tcc build stops at "not found". + # Pure unit test: no compiler, no built target. + add_test( + NAME tcc_module_include_dir + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_tcc_module_include_dir.py + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + # tools/coverage_vault.py's history compaction and streaming JSON # reader. Pure unit test — loads the module by path, needs no built # target and no generated BIOS. It sat in tools/ with no add_test() diff --git a/recompiler/tests/test_tcc_module_include_dir.py b/recompiler/tests/test_tcc_module_include_dir.py new file mode 100644 index 00000000..42e24865 --- /dev/null +++ b/recompiler/tests/test_tcc_module_include_dir.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""The include directory handed to tcc holds every file a BIOS module includes. + +tcc 0.9.27 does not skip a UTF-8 BOM, so compile_overlays._bom_free_incdir() +gives it a BOM-stripped copy of each include directory instead of the real one. +tools/bios_module_build.py writes a glue.c that includes two .c.inc fragments +from runtime/include by name. If the copy leaves them out, the tcc build of +every BIOS module stops at "include file ... not found" and the runtime falls +back to the bundled BIOS. + +Needs no compiler and no built target. + + python3 recompiler/tests/test_tcc_module_include_dir.py +""" + +import ast +import os +import re +import shutil +import sys +import tempfile + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +TOOLS = os.path.join(ROOT, 'tools') +RUNTIME_INCLUDE = os.path.join(ROOT, 'runtime', 'include') +BOM = b'\xef\xbb\xbf' + + +def load_bom_free_incdir(): + """compile_overlays._bom_free_incdir and its memo, without importing the + whole tool: the test then runs on any Python 3 and needs none of the + tool's other imports.""" + path = os.path.join(TOOLS, 'compile_overlays.py') + with open(path, encoding='utf-8') as f: + tree = ast.parse(f.read(), filename=path) + keep = [] + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name == '_bom_free_incdir': + keep.append(node) + elif isinstance(node, ast.Assign) and any( + isinstance(t, ast.Name) and t.id == '_TCC_BOMFREE_INC' for t in node.targets): + keep.append(node) + assert len(keep) == 2, 'compile_overlays.py no longer defines _bom_free_incdir and its memo' + scope = {'os': os, 'tempfile': tempfile} + exec(compile(ast.Module(body=keep, type_ignores=[]), path, 'exec'), scope) + return scope['_bom_free_incdir'] + + +def module_glue_includes(): + """Quoted include names bios_module_build.py writes into a module.""" + with open(os.path.join(TOOLS, 'bios_module_build.py'), encoding='utf-8') as f: + text = f.read() + return sorted(set(re.findall(r'#include \\?"([A-Za-z0-9_./]+)\\?"', text))) + + +def main(): + bom_free_incdir = load_bom_free_incdir() + made = [] + try: + # A synthetic directory: headers and fragments are copied without + # their BOM, anything else is left out. + src = tempfile.mkdtemp(prefix='tcc_inc_src_') + made.append(src) + files = { + 'a.h': BOM + b'#define A 1\n', + 'b.c.inc': BOM + b'static int b;\n', + 'c.inc': b'static int c;\n', + 'notes.txt': b'not a C input\n', + } + for name, data in files.items(): + with open(os.path.join(src, name), 'wb') as f: + f.write(data) + out = bom_free_incdir(src) + made.append(out) + assert sorted(os.listdir(out)) == ['a.h', 'b.c.inc', 'c.inc'], sorted(os.listdir(out)) + for name in ('a.h', 'b.c.inc', 'c.inc'): + with open(os.path.join(out, name), 'rb') as f: + data = f.read() + assert not data.startswith(BOM), f'{name} kept its BOM' + want = files[name][3:] if files[name].startswith(BOM) else files[name] + assert data == want, f'{name} content changed' + + # The real directory: everything a module's glue.c includes resolves. + wanted = module_glue_includes() + assert 'overlay_dispatch_preamble.c.inc' in wanted, wanted + assert 'psx_bios_module_glue.c.inc' in wanted, wanted + real = bom_free_incdir(RUNTIME_INCLUDE) + made.append(real) + missing = [n for n in wanted if not os.path.isfile(os.path.join(real, n))] + assert not missing, f'tcc include copy lacks: {missing}' + print(f'OK: tcc include copy holds {len(wanted)} module includes: {", ".join(wanted)}') + return 0 + finally: + for d in made: + shutil.rmtree(d, ignore_errors=True) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/compile_overlays.py b/tools/compile_overlays.py index 7f9aade8..240a1255 100644 --- a/tools/compile_overlays.py +++ b/tools/compile_overlays.py @@ -4950,7 +4950,10 @@ def _bom_free_incdir(d: str) -> str: return _TCC_BOMFREE_INC[d] out = tempfile.mkdtemp(prefix='tcc_inc_') for name in os.listdir(d): - if not name.endswith('.h'): + # Headers, and the .c.inc fragments a BIOS module's glue.c includes by + # name (overlay_dispatch_preamble.c.inc, psx_bios_module_glue.c.inc). + # tcc sees only this copy, so a file left out here is "not found". + if not name.endswith(('.h', '.inc')): continue with open(os.path.join(d, name), 'rb') as f: data = f.read()