#!/usr/bin/env python3 """Scan xenia source for #include directives and ensure case-aliases exist in xwin's flat SDK/CRT include dirs. xwin's default splat adds lowercase aliases for headers that ship with uppercase names (e.g. Windows.h), but does NOT add aliases in the reverse direction. xenia (and its third_party deps) include some headers using historical Microsoft casing (, ) that don't match the lowercase filenames the SDK ships with. This helper creates the missing symlinks. Idempotent: re-running is safe. """ from __future__ import annotations import os import re import sys from pathlib import Path INCLUDE_RE = re.compile(r'^\s*#\s*include\s*<([A-Za-z][A-Za-z0-9_/.\-]*\.h)>', re.MULTILINE) SOURCE_ROOTS = [ "src", "third_party/SDL2/src", "third_party/SDL2/include", "third_party/discord-rpc/src", "third_party/fmt", "third_party/imgui", ] SOURCE_EXTS = {".h", ".hpp", ".inc", ".c", ".cc", ".cpp", ".cxx"} def collect_includes(repo_root: Path) -> set[str]: """Return set of all -style include targets found in source.""" seen: set[str] = set() for root in SOURCE_ROOTS: base = repo_root / root if not base.exists(): continue for path in base.rglob("*"): if path.suffix.lower() not in SOURCE_EXTS: continue try: txt = path.read_text(encoding="utf-8", errors="ignore") except OSError: continue for m in INCLUDE_RE.finditer(txt): seen.add(m.group(1)) return seen def index_dir(d: Path) -> dict[str, str]: """Return mapping lowercased-basename -> actual basename for files in d.""" if not d.is_dir(): return {} idx: dict[str, str] = {} for entry in d.iterdir(): idx.setdefault(entry.name.lower(), entry.name) return idx def fix_dir(target_dir: Path, want: set[str]) -> tuple[int, int, list[str]]: """For each header name in `want` (basename only), create a symlink to the case-folded sibling if the exact case doesn't exist. Returns (created, already_ok, missing-from-sdk).""" if not target_dir.is_dir(): return 0, 0, [] idx = index_dir(target_dir) created = 0 already_ok = 0 missing: list[str] = [] for name in want: # Only look at single-segment names; subdir/foo.h handled separately if "/" in name: continue actual = idx.get(name.lower()) if actual is None: missing.append(name) continue if actual == name: already_ok += 1 continue link = target_dir / name if link.exists() or link.is_symlink(): already_ok += 1 continue try: link.symlink_to(actual) created += 1 except OSError as e: print(f" ! failed to symlink {link} -> {actual}: {e}", file=sys.stderr) return created, already_ok, missing def main() -> int: if len(sys.argv) != 3: print("usage: xwin-case-symlinks.py ", file=sys.stderr) return 2 repo_root = Path(sys.argv[1]).resolve() xwin_root = Path(sys.argv[2]).resolve() if not (xwin_root / "crt" / "include").exists(): print(f"error: {xwin_root}/crt/include missing", file=sys.stderr) return 1 want = collect_includes(repo_root) print(f"Collected {len(want)} unique #include <...> targets") sdk_dirs = [ xwin_root / "crt" / "include", xwin_root / "sdk" / "include" / "ucrt", xwin_root / "sdk" / "include" / "um", xwin_root / "sdk" / "include" / "shared", xwin_root / "sdk" / "include" / "winrt", ] total_created = 0 for d in sdk_dirs: created, ok, _missing = fix_dir(d, want) if created: print(f" {d}: created {created} case-symlinks") total_created += created print(f"Done: created {total_created} case-symlinks total") return 0 if __name__ == "__main__": sys.exit(main())