Files
NX2navi/tools/check_platform.py
chrg6 56db0d485c Make check_platform.py skip the branches that only a Windows build compiles, so that it passes on the runtime and guards it again.
tools/check_platform.py scanned the raw text of each shared runtime file and knew nothing of preprocessor branches, so the Win32 use that input.c, mods_probe.c, rtss_limit.c, vk_cache_file.h, vk_dlaa.c, vk_dlssg.c, vk_dump.c, vk_ffx.cpp, vk_ffx4.cpp, vk_fg.cpp, vk_fg4.cpp and vk_pipeline.c keep behind #ifdef _WIN32 made it fail on every run, at f1a2a64e already: a check that always fails guards nothing. It now leaves out the lines of a Windows-only branch (#ifdef _WIN32, #if defined(_WIN32), #if _WIN32 up to their #else, #elif or #endif, the #else of #ifndef _WIN32 or #if !defined(_WIN32), and conditionals nested in them), found on the text with its comments blanked. Every other branch is still scanned, #elif defined(_WIN32) and #if defined(_WIN32) && X among them, which can fail the check but never lets a use through. docs/building.md lists the check and its test under "Checks".

Reproduced before the change: the check exited with 1 and these 12 FAIL lines on 2f640fb8; it now exits with 0. Checks that passed: the new tests/check_platform_test.py (guarded uses pass, unguarded ones fail, the repository's runtime passes; it fails before the change) and tools/check_content.py --staged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:30 -05:00

137 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""Fails when runtime code outside runtime/platform/ uses the Win32 API directly.
Runtime code goes through runtime/platform/platform.h. The Win32 back ends of the window system are Win32 by design and
are not scanned: files named *_win.c / *_win.cpp (runtime/ and runtime/ui/), whose SDL counterparts (*_sdl.*) serve
other hosts. Nor are the branches of a shared file that only a Windows build compiles (windows_only_lines): a file
with a portable path of its own may reach a Windows-only library there (the upscalers' DLLs, RivaTuner's shared
memory). Any other file that still uses Win32 goes into ALLOWED with the reason; a file in ALLOWED that no longer
matches is noted, so the entry gets removed.
Usage: python tools/check_platform.py [--root DIR] (DIR holds runtime/; default: the repository root)
"""
import argparse
import bisect
import re
import sys
from pathlib import Path
# Per-file allowlist of shared files that still use Win32, with the reason. Empty: every shared file is portable.
ALLOWED = {}
INCLUDE = re.compile(r'^\s*#\s*include\s*[<"](windows|winsock2|ws2tcpip|synchapi|memoryapi|processthreadsapi|'
r'fileapi|handleapi|libloaderapi|winbase|winnt)\.h[>"]', re.M)
CALLS = [
"CreateFile[AW]?", "ReadFile", "WriteFile", "CloseHandle", "DeleteFile[AW]?", "MoveFileEx[AW]?", "CreateDirectory[AW]?",
"FindFirstFile[AW]?", "GetFileAttributes[AW]?", "GetFileSizeEx", "SetFilePointerEx", "SetEndOfFile",
"CreatePipe", "CreateProcess[AW]?", "TerminateProcess", "GetModuleFileName[AW]?", "GetEnvironmentVariable[AW]?",
"VirtualAlloc(Ex)?", "VirtualFree", "VirtualProtect", "VirtualQuery", "MapViewOfFile[A-Za-z0-9]*", "CreateFileMapping[AW]?",
"GetWriteWatch", "ResetWriteWatch", "AddVectoredExceptionHandler", "RemoveVectoredExceptionHandler",
"CreateThread", "_beginthread(ex)?", "GetCurrentThread(Id)?", "SetThreadAffinityMask", "SetThreadDescription",
"SetThreadPriority", "WaitForSingleObject(Ex)?", "WaitForMultipleObjects", "CreateEvent[AW]?", "CreateSemaphore[AW]?",
"CreateMutex[AW]?", "CreateWaitableTimer(Ex)?[AW]?", "SetWaitableTimer(Ex)?", "Sleep(Ex)?", "SwitchToThread",
"QueryPerformanceCounter", "QueryPerformanceFrequency", "GetTickCount(64)?", "GetSystemTimePreciseAsFileTime",
"GetSystemInfo", "GetLogicalProcessorInformation(Ex)?", "SRWLOCK", "CONDITION_VARIABLE", "CRITICAL_SECTION",
"InitializeCriticalSection[A-Za-z]*", "EnterCriticalSection", "LeaveCriticalSection",
"(Acquire|Release)SRWLock(Exclusive|Shared)", "WakeAllConditionVariable", "WakeConditionVariable",
"SleepConditionVariableSRW", "InitOnceExecuteOnce", "INIT_ONCE", "Interlocked[A-Za-z0-9]*", "TlsAlloc", "FlsAlloc",
"LoadLibrary[AW]?", "GetProcAddress", "FreeLibrary", "HANDLE", "DWORD", "LPVOID", "WINAPI",
"SuspendThread", "ResumeThread", "GetThreadContext", "SetThreadContext", "OpenThread", "CreateFiber(Ex)?",
"ConvertThreadToFiber(Ex)?", "ConvertFiberToThread", "SwitchToFiber", "DeleteFiber", "BCrypt[A-Za-z]*",
"_aligned_malloc", "_aligned_free", "_stricmp", "_strnicmp", "_strdup", "_wfopen", "_fseeki64", "_ftelli64",
"localtime_s", "strtok_s", "GetModuleHandle(Ex)?[AW]?", "ExitProcess", "SetEnvironmentVariable[AW]?", "_putenv(_s)?",
"GetLastError", "MultiByteToWideChar", "WideCharToMultiByte", "HWND", "LARGE_INTEGER", "FILETIME",
"MAX_PATH", "GetActiveProcessorCount", "timeBeginPeriod",
"WSAStartup", "WSAPoll", "WSAIoctl", "WSAGetLastError", "closesocket", "ioctlsocket", "SOCKET",
"sockaddr(_in6?|_storage)?",
]
CALL_RE = re.compile(r"\b(" + "|".join(CALLS) + r")\b")
def strip(text):
"""Blank comments and string literals, keeping line numbers."""
def blank(m):
return re.sub(r"[^\n]", " ", m.group(0))
text = re.sub(r"/\*.*?\*/", blank, text, flags=re.S)
text = re.sub(r"//[^\n]*", blank, text)
return re.sub(r'"(?:\\.|[^"\\\n])*"', blank, text)
DIRECTIVE = re.compile(r"#\s*(if|ifdef|ifndef|elif|else|endif)\b")
WIN_IF = re.compile(r"#\s*(ifdef\s+_WIN32|if\s+defined\s*\(\s*_WIN32\s*\)|if\s+defined\s+_WIN32|if\s+_WIN32)$")
WIN_IFNOT = re.compile(r"#\s*(ifndef\s+_WIN32|if\s+!\s*defined\s*\(\s*_WIN32\s*\)|if\s+!\s*defined\s+_WIN32)$")
def windows_only_lines(code):
"""The numbers (from 0) of the lines of code (comments blanked: strip) that only a Windows build compiles: the
branch of #ifdef _WIN32, #if defined(_WIN32) or #if _WIN32 up to its #else, #elif or #endif, the #else branch of
#ifndef _WIN32 or #if !defined(_WIN32), and whatever a conditional inside such a branch holds. The directives
themselves are not among them. Any other branch counts as compiled everywhere, #elif defined(_WIN32) and
#if defined(_WIN32) && X too: such a branch is scanned, which can fail the check but never lets a use through."""
lines, stack = set(), [] # stack: [kind ('win', 'notwin', 'other'), whether its open branch is Windows-only]
for n, line in enumerate(code.split("\n")):
s = line.strip()
m = DIRECTIVE.match(s)
if not m:
if any(w for _, w in stack):
lines.add(n)
continue
d = m.group(1)
if d in ("if", "ifdef", "ifndef"):
kind = "win" if WIN_IF.match(s) else "notwin" if WIN_IFNOT.match(s) else "other"
stack.append([kind, kind == "win"])
elif d == "else" and stack:
stack[-1][1] = stack[-1][0] == "notwin"
elif d == "elif" and stack:
stack[-1][1] = False
elif d == "endif" and stack:
stack.pop()
return lines
def scan(path):
raw = path.read_text(encoding="utf-8", errors="replace")
code = strip(raw) # the same lines as raw
skip = windows_only_lines(code)
starts = [0] + [i + 1 for i, c in enumerate(raw) if c == "\n"]
def guarded(pos):
return bisect.bisect_right(starts, pos) - 1 in skip
# an include's match can begin on the line before it (^\s* takes that line's newline): its end is on its own line
hits = [("include", m.group(0).strip()) for m in INCLUDE.finditer(raw) if not guarded(m.end() - 1)]
hits += [("call", m.group(1)) for m in CALL_RE.finditer(code) if not guarded(m.start())]
return hits
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--root", default=str(Path(__file__).resolve().parent.parent))
ap.add_argument("--runtime", default=None, help="runtime dir (default ROOT/runtime)")
a = ap.parse_args()
rt = Path(a.runtime) if a.runtime else Path(a.root) / "runtime"
bad, remaining = [], {}
files = [p for pat in ("*.c", "*.h", "*.cpp", "ui/*.cpp", "ui/*.h") for p in rt.glob(pat)]
for p in sorted(files):
if p.stem.endswith("_win"):
continue
hits = scan(p)
if p.name in ALLOWED:
remaining[p.name] = len(hits)
elif hits:
bad.append((p.name, hits))
for name, hits in bad:
kinds = sorted({h[1] for h in hits})
print(f"FAIL runtime/{name}: {len(hits)} Win32 uses outside runtime/platform/ ({', '.join(kinds[:8])})")
stale = [n for n, c in remaining.items() if c == 0]
for n in stale:
print(f"note runtime/{n}: allowlisted but no Win32 use left; remove it from ALLOWED")
print("remaining Win32 uses per allowlisted file:")
for n, c in sorted(remaining.items(), key=lambda kv: (-kv[1], kv[0])):
print(f" {c:5d} runtime/{n} ({ALLOWED[n]})")
print(f"total {sum(remaining.values())} in {sum(1 for c in remaining.values() if c)} files")
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())