mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-24 00:02:45 +02:00
F10 pad slider persistence (D489), glass collision hull (D490), A/B input harnesses (1964 virtual pad, GE_INPUTSCRIPT, live one-pad launcher), adjacent global struct reads (D491), vibration percent, D441 complete pointer-width census with high-arena tier-1 build, fix batches 1-5 and an ABI warning ratchet, playtest verification of D493-D502. Refs: D489 D490 D491 D492-D502 D441
225 lines
7.9 KiB
Python
225 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""D441 P4: ABI warning ratchet for CI.
|
|
|
|
Re-runs every .c/.cpp entry of BUILD_DIR/compile_commands.json with
|
|
-c -> -fsyntax-only, -o <x> removed, -Werror dropped, and
|
|
-Wpointer-to-int-cast -Wint-to-pointer-cast -Wint-conversion -Wno-error
|
|
appended (same method as scratch/d441/collect.py), deduped the same way
|
|
on (file, line, col, flag, msg). Counts per (repo-relative file, flag)
|
|
and compares against the baseline JSON {"file": {"flag": count}}:
|
|
|
|
python tools_pc/abi_ratchet.py build-pc # fail if any count rises
|
|
python tools_pc/abi_ratchet.py build-pc --update # rewrite the baseline
|
|
python tools_pc/abi_ratchet.py build-pc --baseline other.json
|
|
|
|
Exit codes: 0 = OK (every count flat or lower), 1 = ratchet violation
|
|
(a count rose, or a new file/flag appeared with count > 0), 2 = usage or
|
|
environment error (missing compile_commands.json / baseline). Stdlib only.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT = os.path.dirname(HERE)
|
|
ROOT_FW = ROOT.replace('\\', '/')
|
|
DEFAULT_BASELINE = os.path.join(HERE, 'abi_baseline.json')
|
|
|
|
FLAGS = ['-Wpointer-to-int-cast', '-Wint-to-pointer-cast', '-Wint-conversion']
|
|
FLAG_RE = re.compile(
|
|
r'^(?P<path>.+?):(?P<line>\d+):(?P<col>\d+): warning: (?P<msg>.*?) '
|
|
r'\[(?P<flag>-W(?:pointer-to-int-cast|int-to-pointer-cast|int-conversion))\]\s*$')
|
|
|
|
if os.name == 'nt':
|
|
ENV = dict(os.environ)
|
|
ENV['PATH'] = r'C:\msys64\mingw64\bin;' + ENV.get('PATH', '')
|
|
if not ENV.get('ComSpec'):
|
|
ENV['ComSpec'] = r'C:\Windows\system32\cmd.exe'
|
|
if not ENV.get('PATHEXT'):
|
|
ENV['PATHEXT'] = '.COM;.EXE;.BAT;.CMD'
|
|
if not ENV.get('TMP'):
|
|
ENV['TMP'] = ENV.get('TEMP', os.environ.get('SystemRoot', r'C:\Windows')) + r'\Temp'
|
|
ENV['TEMP'] = ENV['TMP']
|
|
else:
|
|
ENV = None # inherit
|
|
|
|
|
|
def transform(entry):
|
|
"""-o <x> removed, -c -> -fsyntax-only, -Werror dropped, ratchet flags appended."""
|
|
toks = shlex.split(entry['command'], posix=False)
|
|
new, skip = [], False
|
|
for t in toks:
|
|
if skip:
|
|
skip = False
|
|
continue
|
|
if t == '-o':
|
|
skip = True
|
|
continue
|
|
if t == '-c':
|
|
new.append('-fsyntax-only')
|
|
continue
|
|
if t == '-Werror' or t.startswith('-Werror='):
|
|
continue
|
|
new.append(t)
|
|
new += FLAGS + ['-Wno-error']
|
|
return new
|
|
|
|
|
|
def norm_path(p, tu_file, cwd):
|
|
"""Repo-relative forward-slash path (same rules as collect.py)."""
|
|
p = p.replace('\\', '/')
|
|
if p.startswith(ROOT_FW + '/'):
|
|
return p[len(ROOT_FW) + 1:]
|
|
if os.path.isabs(p):
|
|
return p
|
|
bases = []
|
|
for b in (os.path.dirname(tu_file), cwd):
|
|
if b not in bases:
|
|
bases.append(b)
|
|
fallback = None
|
|
for b in bases:
|
|
cand = os.path.normpath(os.path.join(b, p)).replace('\\', '/')
|
|
if not cand.startswith(ROOT_FW + '/'):
|
|
continue
|
|
rel = cand[len(ROOT_FW) + 1:]
|
|
if fallback is None:
|
|
fallback = rel
|
|
if os.path.exists(os.path.join(ROOT, *rel.split('/'))):
|
|
return rel
|
|
return fallback if fallback is not None else p
|
|
|
|
|
|
def run_tu(entry):
|
|
"""Returns (tu_file, rows, error); rows are (file, line, col, flag, msg)."""
|
|
tu = entry['file']
|
|
cwd = entry.get('directory', ROOT)
|
|
cmd = transform(entry)
|
|
env = ENV
|
|
if env is not None and os.path.isabs(cmd[0]):
|
|
# Put the compiler's own bin dir first so cc1 loads its matching DLLs.
|
|
# On GitHub-hosted runners setup-msys2 installs under D:\a\_temp while
|
|
# the image also ships an unrelated C:\msys64.
|
|
env = dict(env)
|
|
env['PATH'] = os.path.dirname(cmd[0]) + ';' + env['PATH']
|
|
try:
|
|
p = subprocess.run(cmd, cwd=cwd, capture_output=True,
|
|
text=True, env=env, timeout=600)
|
|
except Exception as ex: # noqa: BLE001 - report and continue
|
|
return tu, [], repr(ex)
|
|
rows = []
|
|
for line in (p.stderr or '').splitlines():
|
|
m = FLAG_RE.match(line)
|
|
if m:
|
|
rows.append((norm_path(m.group('path'), tu, cwd),
|
|
int(m.group('line')), int(m.group('col')),
|
|
m.group('flag'), m.group('msg')))
|
|
err = 'rc=%d' % p.returncode if p.returncode != 0 else None
|
|
return tu, rows, err
|
|
|
|
|
|
def collect(build_dir):
|
|
cc_json = os.path.join(build_dir, 'compile_commands.json')
|
|
if not os.path.isfile(cc_json):
|
|
print('ERROR: %s not found — run ./build-pc.sh ntsc-final first '
|
|
'(CMake exports compile_commands.json at configure time).'
|
|
% cc_json, file=sys.stderr)
|
|
sys.exit(2)
|
|
db = json.load(open(cc_json, encoding='utf-8'))
|
|
entries = [e for e in db if e['file'].endswith(('.c', '.cpp'))]
|
|
print('compile_commands entries: %d total, %d .c/.cpp selected'
|
|
% (len(db), len(entries)))
|
|
|
|
t0 = time.time()
|
|
results = []
|
|
with ThreadPoolExecutor(max_workers=4) as ex:
|
|
for r in ex.map(run_tu, entries):
|
|
results.append(r)
|
|
dt = time.time() - t0
|
|
|
|
seen = set()
|
|
errors = []
|
|
for tu, rows, err in results:
|
|
if err:
|
|
errors.append((tu, err))
|
|
seen.update(rows)
|
|
|
|
counts = {}
|
|
for (f, _ln, _col, flag, _msg) in seen:
|
|
counts.setdefault(f, {})
|
|
counts[f][flag] = counts[f].get(flag, 0) + 1
|
|
|
|
print('elapsed: %.1fs' % dt)
|
|
if errors:
|
|
print('TU ERRORS (%d):' % len(errors), file=sys.stderr)
|
|
for tu, err in errors:
|
|
try:
|
|
rel = os.path.relpath(tu, ROOT)
|
|
except ValueError:
|
|
rel = tu
|
|
print(' %s %s' % (rel, err), file=sys.stderr)
|
|
sys.exit(2)
|
|
return counts
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description='ABI warning ratchet (D441)')
|
|
ap.add_argument('build_dir', help='directory containing compile_commands.json')
|
|
ap.add_argument('--baseline', default=DEFAULT_BASELINE,
|
|
help='baseline JSON path (default: %s)' % DEFAULT_BASELINE)
|
|
ap.add_argument('--update', action='store_true',
|
|
help='rewrite the baseline from the current tree and exit 0')
|
|
args = ap.parse_args()
|
|
|
|
counts = collect(args.build_dir)
|
|
total_now = sum(c for fl in counts.values() for c in fl.values())
|
|
|
|
if args.update:
|
|
with open(args.baseline, 'w', encoding='utf-8', newline='\n') as fh:
|
|
json.dump(counts, fh, indent=2, sort_keys=True)
|
|
fh.write('\n')
|
|
print('baseline updated: %s (%d files, %d warnings)'
|
|
% (args.baseline, len(counts), total_now))
|
|
return
|
|
|
|
if not os.path.isfile(args.baseline):
|
|
print('ERROR: baseline %s not found — run with --update first.'
|
|
% args.baseline, file=sys.stderr)
|
|
sys.exit(2)
|
|
base = json.load(open(args.baseline, encoding='utf-8'))
|
|
total_base = sum(c for fl in base.values() for c in fl.values())
|
|
|
|
violations = []
|
|
lowered = 0
|
|
for f in sorted(set(counts) | set(base)):
|
|
for flag in sorted(set(counts.get(f, {})) | set(base.get(f, {}))):
|
|
old = base.get(f, {}).get(flag, 0)
|
|
new = counts.get(f, {}).get(flag, 0)
|
|
if new > old:
|
|
violations.append((f, flag, old, new))
|
|
elif new < old:
|
|
lowered += 1
|
|
|
|
if violations:
|
|
print('ABI RATCHET VIOLATION: %d file/flag count(s) rose' % len(violations))
|
|
for f, flag, old, new in violations:
|
|
print(' %s %s: %d -> %d' % (f, flag, old, new))
|
|
sys.exit(1)
|
|
|
|
if lowered:
|
|
print('abi ratchet OK: %d warnings now vs %d baseline '
|
|
'(%d file/flag count(s) lower; run --update to ratchet down)'
|
|
% (total_now, total_base, lowered))
|
|
else:
|
|
print('abi ratchet OK: %d warnings now vs %d baseline'
|
|
% (total_now, total_base))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|