Files
BFM-decomp/tools/compile_only.py
T
Drew T 6732771fe0 feat(phase-33): B7 the ROM-free CI — no-rom.yml (audits + compile-only), tools/audit_public.py (the first-push gate), tools/compile_only.py, tools/public_rewrite/purge_set.txt
- audit_public: purge paths from purge_set.txt (the C1 rewrite's own input) + a DERIVED ROM-hash set (1,801 manifest
  rows + 218 check.*.sha + redump Track 1) + 50 MiB cap; controls: the current tree FAILS naming exactly the purge set
  (255 rows), a clean subset OK, a renamed EXE copy caught by content; zero-length files exempt (the empty-file SHA1 is
  also the zero-length SC04/SC05 FILE_029/1.6 payloads')
- compile_only: Makefile flags parsed at run time; TUs from <alias>_SRC_DIR with nested-binary pruning; skips derived
  (70 LINKED tiles, 47 INCLUDE_ASM TUs), -O0 TUs compiled at -O0; PR scope 54/54 in 1.6 s; fleet 4,170/4,170 in 123 s
  at -j32, failed 0; unknown alias refused (R43)
- no-rom.yml: audits (8 commands, each re-run under the system python without the venv -> rc 0) + compile-only (apt
  binutils-mipsel + cpp-mipsel-linux-gnu, cc1 from the sha256-checked tarball, maspsx submodule; PR scope on push/PR,
  --all weekly + dispatch); the audits job is RED until C3 by design
- SETUP P33 B7 section + 4 inventory rows (R21); CURRENT_PHASE log + checkpoint -> NEXT = B8
2026-09-06 20:50:11 -06:00

185 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""compile_only.py — compile every eligible C translation unit with the pinned toolchain, WITHOUT the ROM (P33 B7; CI).
tools/compile_only.py main resident ov_SC01_077 md_MAIN_013 # the PR scope (~124 TUs, ~1 min)
tools/compile_only.py --all [-j N] # every binary (weekly / workflow_dispatch)
tools/compile_only.py --list main # print the plan, compile nothing
What it proves: that the committed C still goes through cpp → gcc-2.7.2 cc1 → maspsx → GNU as with the Makefile's
exact flags (parsed from the Makefile at run time, never retyped — R51). It does NOT prove byte-identity: that needs
the disc (`make check-all`, done locally; docs/verification.md). A TU that fails here is a real regression (a syntax
error, a lost declaration, a header break) and is named.
Which TUs (all DERIVED, R32/R33 — the coverage line says how many of the found TUs each rule removed):
* a binary's TUs = the Makefile's C_SRCS rule: `find <alias>_SRC_DIR -name '*.c' -not -name '.*'`, pruning every
OTHER binary's source dir that nests inside it (main = src/ minus src/<other>/ …);
* SKIP linked — main's LINKED-region fallback tiles (`progress._main_linked_segs_from_makefile()`, src/<seg>.c): they
are INCLUDE_ASM stubs whose .s lives in the ROM-derived asm/ tree;
* SKIP include_asm — any TU with a line-start `INCLUDE_ASM(` / `INCLUDE_RODATA(` (same reason: `.include`s asm/);
* -O0 TUs (`corpus.o0_sources()`, the Makefile's per-object `CC1FLAGS := … -O0` rules) are COMPILED at -O0, not skipped.
The jump-table rodata pad stage (tools/jtbl_rodata_pads.py) is byte-layout work between maspsx and as and needs the
raw image for `--derive`; it is not part of "does it compile" and is left out here.
Toolchain: mipsel-linux-gnu-cpp / -as on PATH (apt binutils-mipsel-linux-gnu), tools/bin/gcc-2.7.2-psx/cc1 (the tracked
tarball, extracted by tools/bootstrap.sh), tools/maspsx (submodule) under the running Python. No venv needed.
"""
import argparse
import os
import pathlib
import re
import shutil
import subprocess
import sys
import tempfile
import time
from concurrent.futures import ThreadPoolExecutor
REPO = pathlib.Path(__file__).resolve().parent.parent
MK = REPO / "Makefile"
sys.path.insert(0, str(REPO / "tools"))
def mk_var(text, name):
m = re.search(r"^%s\s*:?=\s*(.*)$" % re.escape(name), text, re.M)
if not m:
sys.exit(f"compile_only: cannot find `{name} :=` in the Makefile (R32)")
return m.group(1).strip()
def src_dirs():
"""alias -> source dir, from `<alias>_SRC_DIR := …` in the Makefile and every config/*.mk it includes."""
texts = [MK.read_text()] + [p.read_text() for p in sorted((REPO / "config").glob("*.mk"))]
out = {}
for t in texts:
for m in re.finditer(r"^(\w+)_SRC_DIR\s*:?=\s*(\S+)\s*$", t, re.M):
out[m.group(1)] = m.group(2)
if "main" not in out or "resident" not in out:
sys.exit("compile_only: main/resident _SRC_DIR not found (R32)")
return out
def tus_of(alias, dirs):
d = REPO / dirs[alias]
if not d.is_dir():
sys.exit(f"compile_only: {alias}: source dir {dirs[alias]} does not exist (R43)")
nested = [REPO / v for a, v in dirs.items() if a != alias and (REPO / v).is_relative_to(d) and (REPO / v) != d]
out = []
for p in sorted(d.rglob("*.c")):
if p.name.startswith("."):
continue
if any(p.is_relative_to(n) for n in nested):
continue
out.append(p)
return out
INCLUDE_ASM_RE = re.compile(r"^\s*INCLUDE_(ASM|RODATA)\(", re.M)
def plan(aliases):
import corpus
import progress
dirs = src_dirs()
linked = {f"src/{s}.c" for s in progress._main_linked_segs_from_makefile()}
o0 = set(corpus.o0_sources())
rows = [] # (alias, rel, action) action in compile-O2 / compile-O0 / skip:linked / skip:include_asm
for a in aliases:
for p in tus_of(a, dirs):
rel = p.relative_to(REPO).as_posix()
if a == "main" and rel in linked:
rows.append((a, rel, "skip:linked"))
elif INCLUDE_ASM_RE.search(p.read_text(errors="replace")):
rows.append((a, rel, "skip:include_asm"))
elif rel in o0:
rows.append((a, rel, "compile-O0"))
else:
rows.append((a, rel, "compile-O2"))
return rows
def toolchain(text):
cpp = shutil.which(mk_var(text, "MIPS_PREFIX") + "cpp")
as_ = shutil.which(mk_var(text, "MIPS_PREFIX") + "as")
cc1 = REPO / mk_var(text, "CC1_PSX")
maspsx = REPO / mk_var(text, "MASPSX")
for what, p in (("cpp", cpp), ("as", as_), ("cc1", cc1), ("maspsx", maspsx)):
if not p or not pathlib.Path(p).exists():
sys.exit(f"compile_only: {what} not found ({p}) — see tools/bootstrap.sh")
return dict(
cpp=[cpp] + mk_var(text, "CPPFLAGS").split(),
cc1=[str(cc1)], cc1flags=mk_var(text, "CC1FLAGS").split(),
maspsx=[sys.executable, str(maspsx), f"--aspsx-version={mk_var(text, 'ASPSX_VERSION')}"] + mk_var(text, "MASPSX_FLAGS").split(),
as_=[as_] + mk_var(text, "ASFLAGS").split(),
)
def compile_one(tc, rel, action, outdir):
src = REPO / rel
obj = outdir / (rel.replace("/", "__") + ".o")
flags = list(tc["cc1flags"])
if action == "compile-O0":
flags = ["-O0" if f == "-O2" else f for f in flags]
cmd = " | ".join([
" ".join(tc["cpp"] + [str(src)]),
" ".join(tc["cc1"] + flags),
" ".join(tc["maspsx"]),
" ".join(tc["as_"] + ["-o", str(obj)]),
])
r = subprocess.run(["bash", "-o", "pipefail", "-c", cmd], cwd=REPO, capture_output=True, text=True)
ok = r.returncode == 0 and obj.exists() and obj.stat().st_size > 0
err = "\n".join(l for l in r.stderr.splitlines() if "used $at without" not in l)[-1500:]
return rel, ok, err
def main(argv):
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("aliases", nargs="*")
ap.add_argument("--all", action="store_true")
ap.add_argument("--list", action="store_true")
ap.add_argument("-j", type=int, default=os.cpu_count() or 2)
a = ap.parse_args(argv)
text = MK.read_text()
dirs = src_dirs()
aliases = sorted(dirs) if a.all else a.aliases
if not aliases:
ap.error("name binaries or pass --all")
for x in aliases:
if x not in dirs:
sys.exit(f"compile_only: unknown binary alias {x!r} (R43); known: {len(dirs)} — e.g. main resident ov_SC01_077 md_MAIN_013")
rows = plan(aliases)
todo = [r for r in rows if r[2].startswith("compile")]
skipped = {}
for _, _, act in rows:
if act.startswith("skip"):
skipped[act] = skipped.get(act, 0) + 1
if a.list:
for al, rel, act in rows:
print(f"{act:18} {al:14} {rel}")
print(f"compile_only: {len(aliases)} binaries, {len(rows)} TUs found; to compile {len(todo)} "
f"({sum(1 for r in todo if r[2] == 'compile-O0')} at -O0); skipped "
+ (", ".join(f"{k[5:]} {v}" for k, v in sorted(skipped.items())) or "none"))
if a.list:
return 0
tc = toolchain(text)
outdir = pathlib.Path(tempfile.mkdtemp(prefix="compile_only.", dir=str(REPO / ".run"))) if (REPO / ".run").is_dir() \
else pathlib.Path(tempfile.mkdtemp(prefix="compile_only."))
t0 = time.time()
fails = []
with ThreadPoolExecutor(max_workers=max(1, a.j)) as ex:
for rel, ok, err in ex.map(lambda r: compile_one(tc, r[1], r[2], outdir), todo):
if not ok:
fails.append((rel, err))
print(f" FAIL {rel}\n{err}")
shutil.rmtree(outdir, ignore_errors=True)
n_ok = len(todo) - len(fails)
print(f"compile_only: compiled {n_ok} of {len(todo)} eligible TUs ({len(rows)} found in {len(aliases)} binaries) "
f"in {time.time() - t0:.1f} s at -j{a.j}; failed {len(fails)}")
if not todo:
print("compile_only: nothing eligible — refusing to report success on an empty plan (R43)")
return 1
return 1 if fails else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))