From 66edc7463f927f1876e716c6d37c1d4c14a0f622 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:59:10 +0200 Subject: [PATCH] fix(tools): audit stubs from shared library sources --- tools/import_audit.py | 13 ++++++++ tools/tests/test_import_audit.py | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/tools/import_audit.py b/tools/import_audit.py index 276ce39f8..c50d119e5 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -204,17 +204,30 @@ def source_stubs(source): if not Path(source).is_dir(): raise AuditError(f"{source}: not a directory") stubs = {} + shared = {} for library in sorted(item for item in Path(source).iterdir() if item.is_dir()): aliases = {} for file in library.rglob("*.cpp"): for nid, name in nid_names.source_aliases(file.read_text(errors="ignore")): aliases[name] = nid group = progress.scan_library(library) + if "shared_sources" in group: + shared[library.name] = group["shared_sources"] done = {nid_of(name, aliases) for name in group["done_names"]} for name in group["todo_names"]: nid = nid_of(name, aliases) if nid not in done: stubs.setdefault((library.name + ".prx", nid), name) + for library, owner in shared.items(): + chain = [library] + while owner in shared: + if owner in chain: + raise AuditError(f"{source}: shared source cycle: {' -> '.join(chain + [owner])}") + chain.append(owner) + owner = shared[owner] + for (provider, nid), name in list(stubs.items()): + if provider == owner + ".prx": + stubs[(library + ".prx", nid)] = name return stubs diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 5534297e0..b812c64e2 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -191,6 +191,57 @@ class ImportAuditTests(unittest.TestCase): records, _ = import_audit.audit([(nid, "libB.prx")], exports, stubs, {"libA.prx", "libB.prx"}, set()) self.assertEqual(records[0]["class"], "implemented") + def test_cmake_shared_library_preserves_stub_names_and_implemented_exports(self): + work = self.work + owner, library = "libZOwner", "libAOwner.native" + pending, runtime, live = "scePending", "printf_nid_postfix", "sceLive" + alias_nid, alias_name = "AAAAAAAAAAA", "sceOwnerUnknown" + write_source(work.source, owner, STUB_SOURCE.format(name=pending) + STUB_SOURCE.format(name=runtime) + + f'APS5_EXPORT("{alias_nid}", {alias_name});\n' + STUB_SOURCE.format(name=alias_name) + + DONE_SOURCE.format(name=live)) + shared = work.source / library + shared.mkdir() + (shared / "CMakeLists.txt").write_text(f'include(${{CMAKE_CURRENT_SOURCE_DIR}}/../{owner}/OwnerLibrary.cmake)\n') + expected = {nid_names.compute_nid(pending): pending, nid_names.compute_nid("printf"): runtime, alias_nid: alias_name} + live_nid = nid_names.compute_nid(live) + (work.libs / (library + ".prx")).write_bytes(exporting(*expected, live_nid)) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [(nid, library + ".prx") for nid in [*expected, live_nid]]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + records = {record["nid"]: record for record in json.loads(result.read_text())["imports"]} + self.assertEqual({nid: (records[nid]["class"], records[nid]["name"]) for nid in expected}, + {nid: ("stub", name) for nid, name in expected.items()}) + self.assertEqual((records[live_nid]["class"], records[live_nid]["name"]), ("implemented", None)) + + def test_source_include_shared_library_preserves_stubs(self): + source = self.work.source + write_source(source, "libOwner", STUB_SOURCE.format(name="scePending")) + write_source(source, "libShared", '#include "prx/libOwner/Export.cpp"\n') + nid = nid_names.compute_nid("scePending") + self.assertEqual(import_audit.source_stubs(source), { + ("libOwner.prx", nid): "scePending", ("libShared.prx", nid): "scePending"}) + + def test_transitive_shared_library_preserves_stubs(self): + source = self.work.source + write_source(source, "libZOwner", STUB_SOURCE.format(name="scePending")) + for library, owner in (("libA", "libB"), ("libB", "libZOwner")): + shared = source / library + shared.mkdir() + (shared / "CMakeLists.txt").write_text(f'include(${{CMAKE_CURRENT_SOURCE_DIR}}/../{owner}/OwnerLibrary.cmake)\n') + nid = nid_names.compute_nid("scePending") + self.assertEqual(import_audit.source_stubs(source), { + (library + ".prx", nid): "scePending" for library in ("libA", "libB", "libZOwner")}) + + def test_shared_source_cycle_fails(self): + source = self.work.source + for library, owner in (("libA", "libB"), ("libB", "libA")): + shared = source / library + shared.mkdir() + (shared / "CMakeLists.txt").write_text(f'include(${{CMAKE_CURRENT_SOURCE_DIR}}/../{owner}/OwnerLibrary.cmake)\n') + with self.assertRaisesRegex(import_audit.AuditError, "shared source cycle.*libA.*libB"): + import_audit.source_stubs(source) + def test_formatted_export_aliases_remain_stubs(self): work = self.work nid = "AAAAAAAAAAA"