Files
AnyPS5/tools/build_mingw_host_only_tests.py
T

176 lines
6.7 KiB
Python

import argparse
import json
from pathlib import Path
import re
import subprocess
import sys
def read_reply(build_dir):
reply_dir = build_dir / ".cmake" / "api" / "v1" / "reply"
indexes = list(reply_dir.glob("index-*.json"))
if not indexes:
raise ValueError(f"{reply_dir}: no CMake File API index; configure with the codemodel-v2 query first")
index = max(indexes, key=lambda path: path.stat().st_mtime_ns)
document = json.loads(index.read_text(encoding="utf-8"))
codemodel = document.get("reply", {}).get("codemodel-v2")
if not codemodel:
raise ValueError(f"{index}: missing codemodel-v2 reply")
model = json.loads((reply_dir / codemodel["jsonFile"]).read_text(encoding="utf-8"))
configurations = model.get("configurations", [])
if len(configurations) != 1:
raise ValueError(f"{index}: expected one CMake configuration, found {len(configurations)}")
targets = {}
for target in configurations[0].get("targets", []):
targets[target["id"]] = json.loads((reply_dir / target["jsonFile"]).read_text(encoding="utf-8"))
return Path(model["paths"]["source"]).resolve(), targets
def link_command(target):
link = target.get("link", {})
fragments = link.get("commandFragments", [])
return " ".join(fragment.get("fragment", "") for fragment in fragments)
def is_libc_link(name, target):
if name == "libc":
return True
command = link_command(target)
return re.search(r"(?i)(?:^|[/\\\s])libc(?:\.prx|\.dll\.a)(?:$|[\s\"'])", command) is not None
def links_guest_libc(target):
return is_libc_link(target["name"], target)
def is_product_library(target, source_root):
if target.get("type") not in {"STATIC_LIBRARY", "SHARED_LIBRARY", "MODULE_LIBRARY"}:
return False
source_dir = (source_root / target["paths"]["source"]).resolve()
try:
source_dir.relative_to(source_root / "core" / "libs")
except ValueError:
return False
try:
source_dir.relative_to(source_root / "core" / "libs" / "tests")
except ValueError:
return True
return False
def link_command_mentions_target(command, target):
link_names = set()
for artifact in target.get("artifacts", []):
name = Path(artifact.get("path", "")).name
if name and name in command:
return True
for suffix in (".dll.a", ".dylib", ".prx", ".so", ".a", ".lib"):
if name.casefold().endswith(suffix):
name = name[:-len(suffix)]
break
if name.startswith("lib") and target.get("name", "").casefold() != "libc":
name = name[3:]
if name:
link_names.add(name)
target_name = target.get("name", "")
if target_name:
link_names.add(target_name)
return any(
re.search(rf"(?i)(?:^|\s)-l{re.escape(name)}(?=$|\s)", command) is not None
for name in link_names
)
def links_product_library(target, targets, source_root):
command = link_command(target)
if not command:
return False
for linked_target in targets.values():
if is_product_library(linked_target, source_root) and link_command_mentions_target(command, linked_target):
return True
return False
def source_relative_path(source_root, source):
try:
return source.relative_to(source_root).as_posix()
except ValueError:
return None
def host_only_test_targets(source_root, targets, changed_paths=None):
selected = []
for target in targets.values():
if target.get("type") != "EXECUTABLE":
continue
target_source_dir = (source_root / target["paths"]["source"]).resolve()
try:
target_source_dir.relative_to(source_root / "core" / "libs")
except ValueError:
continue
sources = [source_root / source["path"] for source in target.get("sources", []) if not source.get("isGenerated")]
if not any("tests" in source.parts and source.is_relative_to(source_root / "core" / "libs") for source in sources):
continue
if links_guest_libc(target) or links_product_library(target, targets, source_root):
continue
selected.append(target["name"])
selected = sorted(set(selected), key=str.casefold)
if changed_paths is None:
return selected
changed = {Path(path).as_posix() for path in changed_paths if path}
broad_change = any(
path in {"CMakeLists.txt", ".github/workflows/build.yml", "tools/build_mingw_host_only_tests.py"}
or path.startswith("core/libs/") and (
path.endswith((".h", ".hpp", ".hh", ".inl")) or Path(path).name == "CMakeLists.txt"
)
for path in changed
)
if broad_change:
return selected
changed_source = {
relative
for target in targets.values()
if target.get("name") in selected
for item in target.get("sources", []) if not item.get("isGenerated")
for relative in [source_relative_path(source_root, source_root / item["path"])]
if relative is not None
}
matched_sources = changed & changed_source
if matched_sources:
return [
target["name"] for target in targets.values()
if target.get("name") in selected
and any(
source_relative_path(source_root, source_root / item["path"]) in matched_sources
for item in target.get("sources", []) if not item.get("isGenerated")
)
]
return []
def main(argv=None):
parser = argparse.ArgumentParser(description="build core/libs test executables that do not link guest libc")
parser.add_argument("build_dir", type=Path)
parser.add_argument("changed_paths", nargs="?", type=Path)
parser.add_argument("--list-only", action="store_true")
args = parser.parse_args(argv)
try:
source_root, targets = read_reply(args.build_dir.resolve())
changed_paths = args.changed_paths.read_text(encoding="utf-8").splitlines() if args.changed_paths else None
selected = host_only_test_targets(source_root, targets, changed_paths)
if not selected:
print("No changed host-only core/libs test executables require a link check")
return 0
print("Host-only core/libs test executables:")
print("\n".join(selected))
if not args.list_only:
subprocess.run(["cmake", "--build", str(args.build_dir), "--parallel", "3", "--target", *selected], check=True)
except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError, subprocess.CalledProcessError) as error:
print(f"{parser.prog}: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())