Files

186 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.
Phase 35 T2: delegated to corpus.src_dirs() — ONE oracle for every tool (R33); a twin's dir is its primary's."""
import corpus
try:
out = dict(corpus.src_dirs())
except Exception as e:
sys.exit(f"compile_only: {e} (R32)")
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:]))