Files
psxrecomp/tools/tests/test_overlay_static_sources_cmake.py
T
Alex Vanderveen 874df9b5bb release: ship bundled, compiled games from committed generated/ C
Retire the setup-host release model (CI wiped generated/ and shipped
emitters + sources + a wizard so every player regenerated and rebuilt the
title). Titles now commit generated/ and CI ships the compiled game.

- docs/ci/templates/game-release.yml replaces setup-release.yml: gate on
  committed game C (tools/ci/check_generated.sh), emit the OpenBIOS backend
  in CI (tools/ci/generate_openbios.sh), configure with
  PSXRECOMP_REQUIRE_GAME_C=ON and assert the full path was taken, package
  with tools/package_game_release.sh, verify the zip is a game not a kit.
- tools/package_game_release.sh: exe, assets, bundled OpenBIOS, configs, mod
  catalog + overlay toolchain via release_stage.py, notices; refuses sources,
  emitters, generated C, BIOS dumps. clear_generated.sh removed;
  package_setup_host.sh marked retired.
- Scaffold: gitignore templates track generated/, package_release.sh.in
  wrapper, setup_project.{sh,ps1} commit generated/ after Generate.
- Project Studio: audit flags ignored/untracked generated/, retail BIOS C
  and setup-host workflows; merge_gitignore un-ignores generated/; ops emit
  the bundled packager + workflow; generate_ci uses the new template.
- Docs: docs/ci/BUNDLED_RELEASES.md replaces HOST_ONLY_RELEASES.md.
- Tests: tools/tests/test_bundled_release_layout.py; fix pre-existing
  false positives in the packager-surface and overlay-static tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 17:11:14 -04:00

162 lines
7.5 KiB
Python

"""runtime/overlay_static_sources.cmake and hash_codegen.cmake's source-list mode.
Configures throw-away CMake projects (LANGUAGES NONE: no compiler is involved)
that call psxrecomp_overlay_static_sources() the way runtime.cmake does, and
checks what a title's configure reports and links for each state of its static
overlay shard. Also pins that `hash_codegen.cmake -DPSXRECOMP_CODEGEN_HASH_ROOT`
(what aot_overlay_pipeline.py static runs before the runtime is ever built)
computes the same hash psxrecomp-game bakes in.
Run: ctest -R overlay_static_sources_cmake
"""
import argparse
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
# ctest passes the tools explicitly; under pytest the cmake on PATH is used and
# the recompiler comes from PSXRECOMP_GAME (the CLI's override variable).
ARGS = argparse.Namespace(cmake=shutil.which('cmake') or '', generator='', make_program='',
recompiler=os.environ.get('PSXRECOMP_GAME', ''))
def cmake_list(path):
return Path(path).as_posix()
class OverlayStaticSourcesTest(unittest.TestCase):
def setUp(self):
if not ARGS.cmake:
self.skipTest('no cmake: pass --cmake or put cmake on PATH')
self.tmp = tempfile.TemporaryDirectory()
self.project = Path(self.tmp.name) / 'title'
(self.project / 'generated').mkdir(parents=True)
self.static_c = self.project / 'generated' / 'overlays_static.c'
def tearDown(self):
self.tmp.cleanup()
def profile(self, **fields):
(self.project / 'aot').mkdir(exist_ok=True)
(self.project / 'aot' / 'overlays.json').write_text(json.dumps(
dict(schema='psxrecomp AOT methods v1', game_id='TEST-00001', **fields)))
def configure(self, static_c, game_linked=True):
(self.project / 'CMakeLists.txt').write_text(f'''
cmake_minimum_required(VERSION 3.20)
project(overlay_static_probe NONE)
include("{cmake_list(ROOT / 'runtime' / 'overlay_static_sources.cmake')}")
psxrecomp_overlay_static_sources(srcs present
TARGET probe
STATIC_C "{cmake_list(static_c) if static_c else ''}"
PROFILE "${{CMAKE_CURRENT_SOURCE_DIR}}/aot/overlays.json"
GAME_LINKED {"TRUE" if game_linked else "FALSE"})
message(STATUS "PROBE_PRESENT=${{present}}")
message(STATUS "PROBE_SOURCES=${{srcs}}")
''')
build = self.project / 'build'
cmd = [ARGS.cmake, '-S', str(self.project), '-B', str(build)]
if ARGS.generator:
cmd += ['-G', ARGS.generator]
if ARGS.make_program:
cmd += [f'-DCMAKE_MAKE_PROGRAM={ARGS.make_program}']
result = subprocess.run(cmd, capture_output=True, text=True, errors='replace')
return result.returncode, result.stdout + result.stderr, build
@staticmethod
def probe(output, key):
for line in output.splitlines():
if f'{key}=' in line:
return line.split(f'{key}=', 1)[1].strip()
raise AssertionError(f'{key} not reported:\n{output}')
def test_declared_but_absent_warns_only_when_game_c_is_linked(self):
self.profile(static_output='generated/overlays_static.c')
code, out, build = self.configure(self.static_c)
self.assertEqual(code, 0, out)
self.assertEqual(self.probe(out, 'PROBE_PRESENT'), 'FALSE')
self.assertIn('links NO static', out)
self.assertIn('psxrecomp_cli.py generate', out)
# The dispatcher is globbed with CONFIGURE_DEPENDS, so a later Generate
# re-runs configure at build time rather than being ignored.
verify = (build / 'CMakeFiles' / 'VerifyGlobs.cmake').read_text()
self.assertIn(cmake_list(self.static_c), verify)
code, out, _ = self.configure(self.static_c, game_linked=False)
self.assertEqual(code, 0, out)
self.assertNotIn('links NO static', out, 'a setup host has no game C to accelerate')
def test_present_dispatcher_links_with_its_sorted_units(self):
self.profile(static_output='generated/overlays_static.c')
for name in ('overlays_static.c', 'overlays_static_0001.c', 'overlays_static_0000.c',
'overlays_static_extra.c'):
(self.project / 'generated' / name).write_text('\n')
code, out, _ = self.configure(self.static_c)
self.assertEqual(code, 0, out)
self.assertEqual(self.probe(out, 'PROBE_PRESENT'), 'TRUE')
names = [Path(p).name for p in self.probe(out, 'PROBE_SOURCES').split(';')]
self.assertEqual(names, ['overlays_static.c', 'overlays_static_0000.c',
'overlays_static_0001.c'])
self.assertNotIn('links NO static', out)
def test_profile_and_cmake_must_name_the_same_file(self):
self.profile(static_output='generated/overlays_static.c')
code, out, _ = self.configure(None)
self.assertNotEqual(code, 0)
# CMake re-wraps message() text at ~72 columns, so this phrase can
# straddle a line break; compare with whitespace collapsed.
self.assertIn('passes no GAME_OVERLAY_STATIC_C', ' '.join(out.split()))
other = self.project / 'elsewhere' / 'overlays_static.c'
code, out, _ = self.configure(other)
self.assertNotEqual(code, 0)
self.assertIn('Make them name the same file', ' '.join(out.split()))
def test_undeclared_profiles_and_capture_built_shards_are_unaffected(self):
# A release/DLL-cache title: profile without static_output, no static C.
self.profile(images=[])
code, out, _ = self.configure(None)
self.assertEqual(code, 0, out)
self.assertEqual(self.probe(out, 'PROBE_PRESENT'), 'FALSE')
# A capture-built shard (compile_overlays --static) with no profile.
(self.project / 'aot' / 'overlays.json').unlink()
(self.project / 'generated' / 'overlays_static.c').write_text('\n')
code, out, _ = self.configure(self.static_c)
self.assertEqual(code, 0, out)
self.assertEqual(self.probe(out, 'PROBE_PRESENT'), 'TRUE')
def test_malformed_profile_is_a_configure_error(self):
(self.project / 'aot').mkdir()
(self.project / 'aot' / 'overlays.json').write_text('{ "static_output": ')
code, out, _ = self.configure(self.static_c)
self.assertNotEqual(code, 0)
self.assertIn('cannot read static_output', out)
class CodegenHashHeaderTest(unittest.TestCase):
def test_source_list_mode_matches_the_hash_psxrecomp_game_bakes(self):
if not ARGS.cmake or not ARGS.recompiler:
self.skipTest('pass --cmake and --recompiler <psxrecomp-game> (or set PSXRECOMP_GAME)')
sys.path.insert(0, str(ROOT / 'tools'))
import aot_overlay_pipeline as pipeline
with tempfile.TemporaryDirectory() as tmp:
header = pipeline.write_codegen_hash_header(ARGS.cmake, Path(tmp) / 'overlay_codegen_hash.h')
written = pipeline.compiler.codegen_hash(tmp)
baked = subprocess.run([ARGS.recompiler, '--codegen-hash'], capture_output=True,
text=True).stdout.strip()
self.assertEqual(f'{written:08x}', baked.lower(), header)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--cmake', default=ARGS.cmake)
parser.add_argument('--generator', default='')
parser.add_argument('--make-program', default='')
parser.add_argument('--recompiler', default=ARGS.recompiler)
ARGS, rest = parser.parse_known_args()
unittest.main(argv=[sys.argv[0]] + rest)