mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-30 12:25:49 +00:00
208 lines
8.2 KiB
Python
208 lines
8.2 KiB
Python
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def object_document(value, path, name):
|
|
if not isinstance(value, dict):
|
|
raise ValueError(f"{path}: invalid {name} (expected an object)")
|
|
return value
|
|
|
|
|
|
def object_entries(value, path, name):
|
|
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
|
|
raise ValueError(f"{path}: invalid {name} (expected a list of objects)")
|
|
return value
|
|
|
|
|
|
def target_document(value, path):
|
|
target = object_document(value, path, "CMake target")
|
|
if "name" in target and not isinstance(target["name"], str):
|
|
raise ValueError(f"{path}: invalid CMake target name (expected a string)")
|
|
link = object_document(target.get("link", {}), path, "CMake target link")
|
|
object_entries(link.get("commandFragments", []), path, "CMake link command fragments")
|
|
object_entries(target.get("artifacts", []), path, "CMake target artifacts")
|
|
object_entries(target.get("sources", []), path, "CMake target sources")
|
|
return target
|
|
|
|
|
|
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 = object_document(json.loads(index.read_text(encoding="utf-8")), index, "CMake File API index")
|
|
reply = object_document(document.get("reply", {}), index, "CMake File API reply")
|
|
codemodel = reply.get("codemodel-v2")
|
|
if codemodel is None:
|
|
raise ValueError(f"{index}: missing codemodel-v2 reply")
|
|
codemodel = object_document(codemodel, index, "CMake codemodel reference")
|
|
if not codemodel:
|
|
raise ValueError(f"{index}: missing codemodel-v2 reply")
|
|
model_path = reply_dir / codemodel["jsonFile"]
|
|
model = object_document(json.loads(model_path.read_text(encoding="utf-8")), model_path, "CMake codemodel")
|
|
configurations = model.get("configurations", [])
|
|
if len(configurations) != 1:
|
|
raise ValueError(f"{index}: expected one CMake configuration, found {len(configurations)}")
|
|
configuration = object_document(configurations[0], model_path, "CMake configuration")
|
|
targets = {}
|
|
for target in configuration.get("targets", []):
|
|
target = object_document(target, model_path, "CMake target reference")
|
|
target_path = reply_dir / target["jsonFile"]
|
|
targets[target["id"]] = target_document(
|
|
json.loads(target_path.read_text(encoding="utf-8")), target_path)
|
|
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())
|