# Makefile — Brave Fencer Musashi decompilation (SLUS-00726, USA) # ============================================================================= # Phase 4 deliverable. The ONLY live target is `check-env` (the Phase-4 # milestone: toolchain preflight). The split/build/check/expected/clean targets # have their NAMES fixed here per docs/SETUP.md §6.3, but are loud-failing stubs # until Phase 5 implements them. Run builds natively from the ext4 clone (H2/R2). # ============================================================================= SHELL := /bin/bash .ONESHELL: # FAIL-CLOSED BY DEFAULT (Phase-27 T2). Without `-e`, .ONESHELL sends the WHOLE recipe to one # `bash -c`, so a recipe's exit status is its LAST command's only — every earlier failure is # silently swallowed. That made `report`'s lint_symbol_refs / progress --audit / difficulty / # dup_report non-gates (dedup-check "worked" purely by being last), i.e. exactly the defect the # 26-A audit exists to kill: a loud failure nobody counts is as invisible as a silent one (R32). # `-e` makes every recipe line load-bearing. Deliberate opt-out: `check-env` (see its recipe). .SHELLFLAGS := -ec # DELETE A FAILED TARGET (Phase-30 S29). `as` reads a pipeline stream, so when an upstream stage # dies mid-stream (e.g. jtbl_rodata_pads' fail-loud table-count guard) `as` has already written a # TRUNCATED .o. make reports the error correctly — and then leaves the corpse on disk, newer than # its .c. The NEXT build considers it up to date and LINKS it, turning a loud, attributable compile # error into `undefined reference to $L105` / `func_8013C938` one build later. That is precisely how # the `JR-PAIR-IN-ONE-O0-OBJECT` "wall" was manufactured (§132). Same family as the §42b/§130 # stale-object traps: an artifact that outlives the command that failed to produce it. .DELETE_ON_ERROR: .DEFAULT_GOAL := help # --- paths & tooling --------------------------------------------------------- PYTHON := python3 VENV := .venv VENV_PY := $(VENV)/bin/python MIPS_PREFIX := mipsel-linux-gnu- AS := $(MIPS_PREFIX)as LD := $(MIPS_PREFIX)ld OBJCOPY := $(MIPS_PREFIX)objcopy CC1_PSX := tools/bin/gcc-2.7.2-psx/cc1 CC1_CDK := tools/bin/gcc-2.7.2-cdk/cc1 MASPSX := tools/maspsx/maspsx.py # ============================================================================= # Binaries — data-driven (Phase 9). Each binary is an alias key in BINARIES with a # namespaced _* variable set. `main` is the retail EXE SLUS_007.26 (the FIRST # instance); its artifact paths are PRESERVED VERBATIM (build/us/, *.us.* config) so # its rebuild stays a byte-exact no-op. The clean path convention (config/ # splat..yaml, build//, config/check..sha, config/symbols..txt, # .run/sig..jsonl) is documented now but first INSTANTIATED by Phase 10's second # binary. Select with `make build BINARY=`; defaults to the EXE. # ----------------------------------------------------------------------------- # Overlay binaries (Phase 13): each location overlay is registered as an alias in the # GENERATED config/overlays.mk (it defines OVERLAY_BINARIES + the per- var blocks), # kept out of this hand-maintained file so tools/new_overlay.sh never edits the Makefile # body. The `-include` is silent when absent (fresh clone / no overlays onboarded yet) -> # OVERLAY_BINARIES expands empty -> BINARIES stays `main resident` and every byte-locked # build is unchanged. Must precede the `:=` BINARIES line (simply-expanded -> read now). -include config/overlays.mk # P30 S44: module-class binaries (own load address, resident-shaped — the md_* small actor modules # + the SC07 endgame pair + the raw-stored overlays' registry). Same contract as overlays.mk: # generated by tools/new_binary.sh, silent when absent, must precede the `:=` line. -include config/modules.mk BINARIES := main resident $(OVERLAY_BINARIES) $(MODULE_BINARIES) BINARY ?= main $(if $(filter $(BINARY),$(BINARIES)),,$(error BINARY='$(BINARY)' not in BINARIES='$(BINARIES)')) # --- main (retail EXE SLUS_007.26) — values preserved from Phases 4-8 --------- main_EXE := extracted/retail/SLUS_007.26 main_NAME := SLUS_007.26 main_OUT_DIR := build/us main_OUT := $(main_OUT_DIR)/$(main_NAME) main_ELF := $(main_OUT).elf main_MAPFILE := $(main_OUT).map main_LD_SCRIPT := $(main_OUT).ld main_SPLAT_YAML := config/splat.us.exe.yaml main_CHECK_SHA := config/check.us.sha main_SYMBOLS := config/symbols.us.txt main_SIG := .run/sig.SLUS_007.26.jsonl main_GHIDRA_PROG := SLUS_007.26 # The fileoff->vram relation: text loads at file 0x800 / vram 0x80010000, so # base = 0x80010000 - 0x800 = 0x8000F800. NOT a universal PS1 constant — overlays # differ. Threaded into the tools as a REQUIRED param starting T5; defined here now. main_VRAM_BASE := 0x8000F800 main_TEXT_LO := 0x80010000 main_TEXT_HI := 0x800629DC # Source roots + undefined-sym outputs: main lives at the repo root (verbatim). main_ASM_DIR := asm main_SRC_DIR := src main_UNDEF_SYMS := undefined_syms_auto.txt main_UNDEF_FUNCS := undefined_funcs_auto.txt # --- resident (engine blob MAIN.CD/FILE_010/1.1, vram 0x800CEDF8 — Phase 10) ---- # The always-resident engine blob: extracted type-1 (uncompressed) payload, load # address RAM-proven in Phase 3 (T6b). Flat image (no PS-X EXE header); fileoff 0 -> # vram 0x800CEDF8 so VRAM_BASE = 0x800CEDF8 (NOT main's 0x8000F800). 365,404 B (0x5935C) # -> end vram 0x80128154. Its splat output NESTS under asm/resident + src/resident + # build/resident, kept disjoint from main's asm/ + src/ by the per-binary OBJS glob below. resident_EXE := extracted/retail/MAIN.CD.dir/FILE_010.dir/1.1 resident_NAME := resident resident_OUT_DIR := build/resident resident_OUT := $(resident_OUT_DIR)/$(resident_NAME) resident_ELF := $(resident_OUT).elf resident_MAPFILE := $(resident_OUT).map resident_LD_SCRIPT := $(resident_OUT).ld resident_SPLAT_YAML := config/splat.resident.yaml resident_CHECK_SHA := config/check.resident.sha resident_SYMBOLS := config/symbols.resident.txt resident_SIG := .run/sig.resident.jsonl resident_GHIDRA_PROG := resident resident_VRAM_BASE := 0x800CEDF8 resident_TEXT_LO := 0x800CEDF8 resident_TEXT_HI := 0x80128154 resident_ASM_DIR := asm/resident resident_SRC_DIR := src/resident resident_UNDEF_SYMS := build/resident/undefined_syms_auto.txt resident_UNDEF_FUNCS := build/resident/undefined_funcs_auto.txt # --- selected-binary aliases (resolve $(BINARY) -> the active instance) ------- EXE := $($(BINARY)_EXE) NAME := $($(BINARY)_NAME) OUT_DIR := $($(BINARY)_OUT_DIR) OUT := $($(BINARY)_OUT) ELF := $($(BINARY)_ELF) MAPFILE := $($(BINARY)_MAPFILE) LD_SCRIPT := $($(BINARY)_LD_SCRIPT) # Phase-26 §8: overlay jtbl-rodata carve args (empty = no carve). $(strip) so a per-binary var # that is unset stays EMPTY (a trailing comment on the := line would leave whitespace -> non-empty # -> the extract branch would misfire on every binary; caught on resident). JTBL_INTERLEAVE := $(strip $($(BINARY)_JTBL_INTERLEAVE)) SPLAT_YAML := $($(BINARY)_SPLAT_YAML) CHECK_SHA := $($(BINARY)_CHECK_SHA) SYMBOLS := $($(BINARY)_SYMBOLS) VRAM_BASE := $($(BINARY)_VRAM_BASE) TEXT_LO := $($(BINARY)_TEXT_LO) TEXT_HI := $($(BINARY)_TEXT_HI) ASM_DIR := $($(BINARY)_ASM_DIR) SRC_DIR := $($(BINARY)_SRC_DIR) UNDEF_SYMS := $($(BINARY)_UNDEF_SYMS) UNDEF_FUNCS := $($(BINARY)_UNDEF_FUNCS) GHIDRA_PROG := $($(BINARY)_GHIDRA_PROG) # cc1 smoke flags — the §5.4 first-candidate set; the real triple is pinned only # after Phase-6 fingerprinting. Used here purely to prove cc1 executes. CC1_SMOKE_FLAGS := -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker # binutils regression line (docs/SETUP.md §4.5): >= 2.38 is a WARN (PS1-matching # regression suspect; 2.35 known-good). The verdict is revisited in Phase 5. BINUTILS_WARN_MAJOR := 2 BINUTILS_WARN_MINOR := 38 .PHONY: help bootstrap check-env disc-extract extract build check expected clean report sig-refresh sig-overlays sig-resident sig-main sdk-dual build-all check-all audit-corpus audit-cdecl audit-binaries audit-text-sources audit-digest audit-frontier tools-health kit-corpus # ----------------------------------------------------------------------------- help: @echo "BFM-decomp — make targets (218 binaries: main + resident + 141 overlays + 75 modules; BINARY= scopes a target)" echo " make bootstrap fresh-clone setup: apt check (printed), venv, submodules, the two cc1 tarballs, then check-env" echo " make check-env toolchain preflight (python/venv/cc1/maspsx/binutils/headers; the extracted EXE + payload census)" echo " make disc-extract regenerate extracted/ from YOUR redump dump in disks/ and verify it against the committed manifest (H1: no ROM in the repo)" echo " make extract splat split one binary -> asm/, the linker script (runs disc-extract if its payload is absent)" echo " make extract-all disc-extract + splat split every binary (main first, then parallel)" echo " make check build one binary and compare its SHA1 to config/check..sha (build is an alias)" echo " make check-all build + SHA1-check every binary in parallel (build-all is an alias)" echo " make sdk-dual main byte-identical WITH and WITHOUT the (optional, user-supplied) PsyQ objects" echo " make report regenerate docs/ progress + difficulty + duplicate digests (BINARY=main also the fleet roll-up)" echo " make tools-health the pre-work ritual: fresh sigs, both boundary oracles, every audit, report, digest assertion" echo " make kit-corpus regenerate the tool index, the kit manifest and the verbatim corpora from config/tool_dictionary.tsv" echo " make sig-main | sig-main-oracle | sig-overlays | sig-resident | sig-modules the byte-derived function sigs (.run/sig.*.jsonl)" echo " make audit-corpus | audit-binaries | audit-text-sources | audit-digest | audit-disc | audit-frontier the oracles" echo " make clean remove build/, expected/, asm/, assets/ (fleet-wide; then: make extract-all && make check-all)" echo "The R22 contract proof: make clean && make extract-all && make check-all (expects: check-all: 218 passed, 0 failed of 218)" # ----------------------------------------------------------------------------- # Phase 7 reports: deterministic, committable docs/ digests. progress/difficulty/dup_report # are Ghidra-free; sig-refresh regenerates dup_report's input from the saved Ghidra DB. GHIDRA := $(or $(GHIDRA_INSTALL_DIR),$(HOME)/ghidra_12.1_PUBLIC) GHIDRA_PROJ := $(or $(BFM_GHIDRA_PROJ),$(CURDIR)/ghidra) # P33 B5: repo-relative (override: BFM_GHIDRA_PROJ) # The corpus oracle (Phase 26-A, R32/R33). A SECOND, INDEPENDENT oracle: it cross-checks splat's # function boundaries against sig_image's, which are derived from the ORIGINAL bytes without splat. # The byte-gate is structurally BLIND to a bad boundary (the .s halves are pasted back verbatim, so # the image stays byte-identical) — only an oracle that can DISAGREE can see it. GREEN since A4 # (0 phantom + 0 truncated; was 193 unmatchable slices from one bad symbol line). audit-corpus: $(VENV_PY) tools/corpus.py --all --audit # The C-declaration oracle (Phase 26-A; R33 BEFORE R32). ONE parser, replacing fifteen regex models # of what a C declaration is — models that disagree with each other and are blind, all fifteen, to # fn-ptr / sized-array / multi-declarator decls. # # It is a GATE, not just a capability, because this phase paid to learn that a loud failure NOBODY # COUNTS is exactly as invisible as a silent one (build_engine_types failed loudly for four phases # while hard-exiting on 81% of its own corpus). So: run it, and count it. # # Coverage is asserted from the C GRAMMAR itself — at file scope C admits nothing but declarations, # so the candidate set is every depth-0 statement, and there is no hand-maintained candidate regex # to rot. The real cross-gcc then adjudicates BOTH the parse and the residue (R34): it compiles each # declaration beside this parser's reconstruction of it, and a statement gcc also rejects is not C. # SAMPLED BY DEFAULT (P31 S70). This is a HEALTH check, not a regression suite. The full pass # re-parses every declaration in all 4,168 TUs and hands each to real gcc: ~787s of pure-Python # collection before the first cc1 call, and it made `make tools-health` unrunnable (>15 min, killed # twice). The exhaustive form still exists as `audit-cdecl-full` — run it when cdecl.py itself # changes, not on every health check. CDECL_AUDIT_TUS ?= 60 audit-cdecl: $(VENV_PY) tools/cdecl.py --audit --gcc --limit $(CDECL_AUDIT_TUS) audit-cdecl-full: $(VENV_PY) tools/cdecl.py --audit --gcc # Binary-citizenship gate (Phase-28 T7, R36 via R32). Asserts every onboarded binary # (main + resident + every config/splat.ov_*.yaml) is a full citizen of every consumer that # enumerates binaries: present in dup_report.BINARIES, has a sig, and (overlays) includes the shared # engine-core header so shared bodies can reach it. The 4 SC07 overlays were byte-clean yet invisible # to four consumers for a month; this is the loud assertion that makes the NEXT onboarding wire the # binary in or fail here, before matching is built on a binary half the tools cannot see. Cheap # (config + text scans; no build), so unlike audit-cdecl it CAN sit in the fast lane. audit-binaries: $(VENV_PY) tools/audit_binaries.py # P30 S43 (Drew's directive): the DENOMINATOR's completeness gate. Walks the DISC IMAGE, not our # configs, and asserts every byte lands in exactly one bucket — residue is a DEFECT (R32). This is # what makes "there was more code all along" a finding the tools report rather than a surprise we # trip over: three such surprises (the 0.4.dec glob, disc_code_sweep's raw-only decode, the 4,096-word # window) were each a tool correct about its subset and silent about the rest. # NOT in tools-health: it needs disks/, which a fresh clone does not have (H1 — the dump is ignored). audit-disc: $(VENV_PY) tools/disc_audit.py # P30 S1e: the committed fleet digest must still describe the CURRENT tree. A digest generated from # a working tree that later changed (work reverted before the commit landed) is BYTE-INVISIBLE — # check-all stays 140/140 over it — and the next honest regeneration then reads as a REGRESSION that # never happened. That cost a session-opening false alarm and gated the best-performing lever on a # phantom. R34: the byte-gate is a null oracle for documents, so this is a second one that disagrees. audit-digest: $(VENV_PY) tools/audit_digest.py # P33 D3: the published DATA must describe the current tree too — docs/progress.json, the README's # generated block and the badge files (the same numbers as the digest; R51: never typed) $(VENV_PY) tools/progress.py --json --readme --check # P33.5 task 7: the committed timeline must match what the digests generate (it used to be wired to nothing and sat stale) $(VENV_PY) tools/timeline.py --check # P30 S39 (Drew's MASTER_REMAINING proposal, derived form — docs/decision-log.md 2026-08-04): # "what's left" is answered by six artifacts, each individually derived and NONE ever checked # against the others. That gap cost T0 a hand-reconciliation (family_hseq 29,961 vs progress 28,296). # This is the R34 move: a SECOND view that can DISAGREE with the corpus oracle, loudly. # DELIBERATELY NOT in tools-health yet — additive until Drew has seen it; wiring is one line. audit-frontier: $(VENV_PY) tools/audit_frontier.py # P30 S28: every tracked C source must be TEXT. A raw NUL inside a char literal (`''` instead # of `'\0'`) COMPILES — the fleet stayed byte-identical — but grep treats the file as BINARY and # reports nothing, silently, so the file vanishes from every grep-based audit and hand-search. The # byte-gate is structurally blind to it (R34: correct bytes, nothing to say). Found when a # `grep -rn func_8013C08C src/` came back empty for a function defined right there; a templated body # had then carried the NUL into 137 overlays in this same session. Its own oracle, fail-closed. # P33.5 task 13.5: regenerate everything derived from config/tool_dictionary.tsv — docs/tool-index.md, the kit's # tools/MANIFEST.md and the verbatim corpora (decomp-architect/corpus/tools + corpus/cookbook + corpus/record, the third added # at P33.5 task 14.5). tools-health asserts them fresh. kit-corpus: $(VENV_PY) tools/tool_census.py --all audit-text-sources: $(VENV_PY) tools/audit_text_sources.py # The tool-health ritual (Phase-27 T2). Before the 26-A audit the two oracles above had NO dependent # — nothing invoked them, so "run the audits" was a manual habit, and a habit nobody automates is a # gate nobody counts (R32). This is that dependent: `make tools-health` runs both derived oracles and # the report gates (lint_symbol_refs + dedup-check) together, and under the global -e ANY one failing # aborts it. It is deliberately NOT a prerequisite of `report`/`build` — audit-cdecl cross-compiles # every C declaration through real gcc (minutes), so it belongs to a deliberate pre-matching ritual, # not the inner harvest loop. Matches the roadmap's standing invariant (audit-corpus · audit-cdecl · # report green before matching). tools-health: # Regenerate the byte-derived boundary oracles FIRST (they're gitignored/regenerable), so the # audit checks CURRENT sigs and never crashes on an absent one — the resident audit (T10) needs # the sig_image resident sig, and a fresh clone has neither it nor the overlay sigs. $(MAKE) --no-print-directory sig-overlays $(MAKE) --no-print-directory sig-resident $(MAKE) --no-print-directory sig-modules # main's independent oracle (P31 S77, contract §1.3). Regenerated here for the same reason as # the others: it is derived and gitignored, and an oracle wired into nothing runs for nobody — # neighbor_ref sat MANUAL from S68 to S77 while the playbook called it the biggest cost lever # in the wave. Without this line sig_is_independent("main") silently reverts to False on a # fresh clone and main's boundary blind spot comes back with the audit still green. $(MAKE) --no-print-directory sig-main-oracle # P33 A2: main's build-derived game-code sig — the one the fleet digest weighs main by. Regenerated # here for the R51 reason: progress.py prefers it, so a stale copy would be a stale denominator. $(MAKE) --no-print-directory sig-main # P33 A3: the with/without-SDK dual (contract §1.2). Skipped, loudly, on a machine without the SDK # objects — there every build already IS the without leg, and running it twice would prove nothing. if [ -d "$(LIBCD_ELF)" ] && [ -d "$(LIBPAD_ELF)" ]; then $(MAKE) --no-print-directory sdk-dual else echo "[skip] sdk-dual: no SDK objects on this machine — the default build IS the no-SDK leg" fi $(MAKE) --no-print-directory audit-corpus $(MAKE) --no-print-directory audit-cdecl $(MAKE) --no-print-directory audit-binaries $(MAKE) --no-print-directory audit-text-sources # Phase 35 T7: the S1 invariant — one source per unique function — as a gate (R36): the census self-test (7 verdicts), then the # strict check by exit code (R97): no same-address class unshared unless ledgered in config/dedup_exceptions.tsv, no DEFINE_func_ # site under src/ (--strict-macros), and the sig-blind second oracle (--strict-text). Runs BEFORE report so progress.py reads a # fresh .run/P35/census/share_census.json for its duplicate-copy fields. $(VENV_PY) tools/share_census.py --selftest $(VENV_PY) tools/share_census.py --check --strict-macros --strict-text --quiet # P36 S103: the hand-asm manifest has no drift. It ran ONLY in CI, so a DECOMPILE-NOW row converted by T4 batch tus10 # (cb2fb5e6d, 2026-09-09) turned CI red for a day while every local chain stayed green (R54: a guard that is not # running locally is not a guard). Fix a GONE row with `tools/verbatim_check.py --update` (a one-row diff since S103). $(VENV_PY) tools/verbatim_check.py --strict # P36 T8: the lever census's self-test and its gate — every surviving register pin / asm statement outside the GTE header # carries a `// !FAKE:` marker (0 UNMARKED) and no marker is an orphan; a marker on a kept ordinary-C fake (do-while, # dead-init) is counted apart. `--strict` (0 pins, 0 asm) is the STRUCTS phase's finish line (Drew, S104), not this rung's. $(VENV_PY) tools/lever_census.py --selftest $(VENV_PY) tools/lever_census.py --check -j 16 --quiet $(MAKE) --no-print-directory report BINARY=main # AFTER report (which regenerates the digest), so this asserts the freshly-written digest agrees # with the tree — and, on a tree whose digest was committed stale, says so instead of staying green. $(MAKE) --no-print-directory audit-digest # The cookbook index is DERIVED (R33) and self-asserts its coverage (R32). Stale = agents can't # find documented idioms and re-derive them at full token cost (measured, P30 wave 1). $(VENV_PY) tools/cookbook_index.py --check $(VENV_PY) tools/gccmap_cites.py --check # P33 B5: the Ghidra roster is DERIVED from config/ghidra/*.jsonl (R33); a stale roster misreports # which programs' RE work is tracked as text. Pure text check, no Ghidra needed. $(VENV_PY) tools/ghidra_roster.py --check # P33 D5 (+ P33.5 task 7): every relative link in the public-facing docs resolves (pending pages are listed, and must be # gone by gate 2); nothing links into docs/sunset/; a wiki page links into docs/ only at a Reference-index/README target; # every docs/ file is covered by one; a wiki page cites only TRACKED paths. The render selftest also asserts every page # is reachable from the sidebar. $(VENV_PY) tools/doc_links.py $(VENV_PY) tools/wiki_render.py --selftest # P33.5 task 7: the ROM-firewall page's ```gitignore fence IS the kit's template (one source, two copies). Skips loudly # until the kit's template exists (task 11); exit 2 from the tool = "nothing to compare", never a pass (R43). if [ -f decomp-architect/templates/gitignore.decomp ]; then $(VENV_PY) tools/gitignore_template_check.py else echo "[skip] gitignore-template: decomp-architect/templates/gitignore.decomp does not exist yet (Phase 33.5 task 11)" fi # P33.5 task 11: the day-one decomp kit stays free of this project's names/paths/addresses/rule numbers (fence-aware), # honours its placeholder contract, and its scripts parse; the selftest is the R39 control (a planted leak MUST fail). $(VENV_PY) tools/kit_lint.py --selftest $(VENV_PY) tools/kit_lint.py # P33.5 task 13.5: the tool census — the two enumerations agree (find == git ls-files), every tool has a dictionary row and # every row a file, the need-keyed docs/tool-index.md and the kit's MANIFEST are fresh, the three verbatim corpora under # decomp-architect/corpus/ are byte-equal to their sources (regenerate with `make kit-corpus`). $(VENV_PY) tools/tool_census.py --check # P33.5 task 14.5 (Drew: "the whole of our experience?"): the kit's distillation cites or dispositions EVERY rule (R1..RN from # the digest) and EVERY accelerator entry; an uncovered one fails here unless config/kit_coverage_map.tsv says where it went. $(VENV_PY) tools/kit_coverage.py $(VENV_PY) tools/xsig/tests/test_xsig.py 2>&1 | tail -1 | grep -q '^OK' && echo 'xsig tests: OK (8)' || { echo 'xsig tests: FAIL'; exit 1; } # Behavioural guards (P31 S70): tools-health audits DATA integrity; these assert that a tool # ACTUALLY DID the work it reports. A guard that is not running is not a guard (R54). $(VENV_PY) tools/work_evidence.py --selftest # P31 S72: a code subseg owning raw jump tables in >1 non-adjacent span makes every switch # function outside the one carveable span UNBANKABLE — `main` sat in that state from Phase 7 to # Phase 31 and eleven functions were written off as "PROVEN gate-rejects" because of it. The # evidence is derivable from the raw image on day one; nothing was comparing it. 3.7s fleet-wide. $(VENV_PY) tools/split_indicator.py --self-test # P31 S74: A HARD GATE NOW, exactly as the informational form said it would become. The four # violations it was waiting on (ov_SC01_084, ov_SC02_005, ov_SC02_011, ov_SC03_105 — 16 open # fns / 3,613 ins) are split, so the fleet is 213/213 OK and any NEW subseg owning raw tables # in >1 non-adjacent span is a regression that must fail here rather than be echoed past. $(VENV_PY) tools/split_indicator.py --quiet echo "tools-health: OK — sigs fresh; corpus(+resident) + cdecl + binaries + report(lint+dedup) + cookbook-index all green." report: $(VENV_PY) tools/progress.py --binary $(BINARY) --audit $(VENV_PY) tools/difficulty.py --binary $(BINARY) $(VENV_PY) tools/dup_report.py --binary $(BINARY) # Cross-binary dedup report (Phase 11): binary-spanning, run ONCE (not per-binary), so it # only fires for the default binary — avoids `make report BINARY=resident` rewriting the # identical file. --cross ignores --binary and scans every sig in BINARIES. ifeq ($(BINARY),main) # P33 A2: refresh main's build-derived sig before anything weighs main by it (a no-op message when # main is not built; progress.py then falls back to the legacy Ghidra sig or fails loudly — R32). $(MAKE) --no-print-directory sig-main $(VENV_PY) tools/dup_report.py --cross # Fleet roll-up (Phase 15): deterministic per-binary table + fleet totals -> docs/progress.fleet.md. $(VENV_PY) tools/progress.py --fleet # P33 D1/D3: the same numbers as DATA — docs/progress.json + the README block (never typed by hand) $(VENV_PY) tools/progress.py --json --readme # P33.5 task 7: the dated timeline is generated from the committed digests (R75) — regenerated here, after # progress.json, so its self-check against the last row sees the fresh data; audit-digest asserts it is fresh $(VENV_PY) tools/timeline.py # Backlog compaction (Phase 29): the near-miss log is append-only, so it fills with already-banked # noise (measured 6,867 rows, 98% banked). prune rewrites .run/backlog.jsonl to the open near-misses # (drop-now-matched P9 + best-per-addr) so the ledger tracks reality instead of drifting stale. $(VENV_PY) tools/backlog.py prune # Rename-drift gate (Phase 26-A): fail-closed if a symbols.us.txt rename left a func_ # ref dangling in committed src/ or src/shared/*.h — the R22 failure mode an incremental build # masks (stale .o) but a genuinely-clean rebuild fails on. The ONLY detector for it. $(VENV_PY) tools/lint_symbol_refs.py # Byte-honesty gate (Phase 11): fail-closed if any registered code-share drifted from its # recorded signature hash. Last in the recipe, so a stale share fails `make report` (P9). $(VENV_PY) tools/dedup_integrate.py --check endif sig-refresh: @if ss -tln 2>/dev/null | grep -qE ':8080([^0-9]|$$)'; then echo "sig-refresh: ERROR — Ghidra MCP serving on :8080; run tools/ghidra_mcp_stop.sh first."; exit 2 fi "$(GHIDRA)/support/analyzeHeadless" "$(GHIDRA_PROJ)" bfm -process $(GHIDRA_PROG) -noanalysis -readOnly \ -scriptPath tools/ghidra_scripts -postScript DumpFunctionSignatures.java # sig-overlays (Phase 11): Ghidra-FREE — sign every location-overlay payload (the 134 SCxx 0.4.dec) # at the shared overlay vram with tools/sig_image.py, so `make report` (--cross) can find cross-overlay # duplicates. Each -> .run/sig.ov__.jsonl (gitignored; regenerable). Re-run when overlays # change; not part of `make report` (it scans whatever ov_* sigs exist, like sig-refresh). OVERLAY_VRAM := 0x80128158 # Derived from config/overlays.mk's _EXE payloads (the SINGLE source of truth, R33) — NOT a # `find … 0.4.dec` glob, which silently dropped the 4 Phase-27 SC07 overlays whose code is at PAC # entry 1 (1.4.dec). `:` pairs built at Make level so every onboarded overlay signs. OVERLAY_SIG_JOBS := $(foreach a,$(OVERLAY_BINARIES),$(a):$($(a)_EXE)) sig-overlays: # PARALLEL (P31 S70). 211 independent per-overlay invocations that each write ONLY their own # .run/sig..jsonl (sig_image has exactly one write path, verified) — embarrassingly # parallel, and it was a serial `for` loop on a 32-core box while extract-all/check-all in this # same file already fan out. MEASURED, correcting my first claim: this was only ~52s of # tools-health, NOT the bulk — audit-cdecl's pure-Python collection pass (~787s) is the real cost. # Still worth it (52s -> 3.9s, 141/141 outputs byte-identical) and it is Drew's standing bar: # a slow gate is a BUG, nothing serial. @mkdir -p .run; : > .run/sig-overlays.txt echo "$(OVERLAY_SIG_JOBS)" | tr ' ' '\n' | sed '/^$$/d' | xargs -P$(JOBS) -I{} sh -c '\ job="{}"; alias=$${job%%:*}; f=$${job#*:}; \ if [ ! -f "$$f" ]; then echo "[WARN no payload] $$alias ($$f)"; \ elif $(VENV_PY) tools/sig_image.py --image "$$f" --vram-base $(OVERLAY_VRAM) --bootstrap --name "$$alias" >/dev/null 2>&1; \ then echo "[ OK ] $$alias"; else echo "[SIG FAIL] $$alias"; fi' | tee .run/sig-overlays.txt n=$$(grep -c "^\[ OK \]" .run/sig-overlays.txt || true) bad=$$(grep -c "^\[SIG FAIL\]" .run/sig-overlays.txt || true) echo "sig-overlays: signed $$n overlays -> .run/sig.ov_*.jsonl (of $(words $(OVERLAY_BINARIES)) onboarded)" # The serial form had NO failure detection at all — a sig_image crash just vanished (R32). if [ "$$bad" -ne 0 ]; then echo "[FAIL] sig-overlays: $$bad overlay(s) failed to sign"; exit 1; fi # sig-resident (Phase-27 T10; P31 T0 seed fix): sign the resident flat blob with sig_image — the # Ghidra-FREE, byte-DERIVED signer — so `make audit-corpus` gains a second boundary oracle for the # resident (R34; corpus.sig_is_independent trusts it, same standing as the S45 ELF-seeded modules). # P31 T0: --bootstrap's linear partition produced a WRONG denominator (144 rows vs the true 145) via # two boundary artifacts — it fused the +0 data word with the first function (row 0x800CEDF8 nins=18 # instead of func_800CEDFC) and glued/truncated the tail pair (func_800D33E0 missing). Fixed by the # S45 pattern: SEED from the built ELF's T symbols (unique 4-aligned addrs inside the # resident_TEXT_START/END markers — reproduces exactly 145, matching progress + the source defs). # Fresh-clone fallback (no build yet) stays --bootstrap and self-heals on the next run after a build. # h_exact is raw-byte SHA1 so it is format-independent — weighted_metrics is unaffected. R23-free. sig-resident: elf="build/resident/resident.elf" if [ -f "$$elf" ]; then mipsel-linux-gnu-nm "$$elf" | awk ' $$2=="T" { a=strtonum("0x" $$1); sym[$$3]=a; if (a%4==0) addr[a]=1 } END { lo=sym["resident_TEXT_START"]; hi=sym["resident_TEXT_END"]; for (a in addr) if (a>=lo && a .run/seeds.resident.txt $(VENV_PY) tools/sig_image.py --image $(resident_EXE) --vram-base $(resident_VRAM_BASE) \ --seeds .run/seeds.resident.txt --name resident echo "sig-resident: signed the resident (ELF-seeded, $$(wc -l < .run/seeds.resident.txt) fns) -> .run/sig.resident.jsonl" else $(VENV_PY) tools/sig_image.py --image $(resident_EXE) --vram-base $(resident_VRAM_BASE) --bootstrap --name resident echo "sig-resident: signed the resident (bootstrap fallback — re-run after a build for seeded boundaries)" fi # atlas (P31 T5): the full Frontier Atlas regen chain — family maps -> cards -> features -> the # partition-asserted atlas (every open stub in exactly one lever-labeled crack group). Run at # session T0 and after crack batches; ~10-15 min, zero tokens. Standalone atlas.py runs tolerate # maps that are stale only in the banked-since direction; this chain makes the normal path fresh. atlas: $(VENV_PY) tools/family_hseq.py >/dev/null $(VENV_PY) tools/family_cousins.py >/dev/null $(VENV_PY) tools/family_cousins.py --adapt-cards >/dev/null $(VENV_PY) tools/family_cousins.py --aprop-cards >/dev/null $(VENV_PY) tools/atlas_features.py $(VENV_PY) tools/atlas.py echo "atlas: chain complete -> .run/atlas.json + docs/frontier-atlas.md" # sig-main (P31 T3): sign main's game-code STUBS with sig_image at SPLAT-TRUE lengths. main has a # 0x800 EXE header (file0-vram = $(main_VRAM_BASE)), interleaved data islands, and LINKED PsyQ # regions, so --bootstrap/func_end both mis-slice (measured 3/40 nins drift vs the .s truth); # instead `corpus.py main --seed-ends` emits `0xADDR NINS` per stub (the .s count a C match must # reproduce) and each slice is exactly [addr, addr+4*nins) — no heuristic, no contiguity assumption # beyond each function's own slice. This sig is deliberately splat-SEEDED (the atlas needs the # boundaries a match must hit); it is NOT main's independent second oracle # (docs/second-oracle.md — sig_is_independent stays False for main). # P33 A2 — REWRITTEN: main's GAME-CODE sig at BUILD-TRUE lengths, Ghidra-free and splat-free. The stub # seeding above became a sig of nothing at 100% (0 stubs), and the whole-EXE alternative was the # 2026-08-05 Ghidra sig (.run/sig.SLUS_007.26.jsonl), which a public clone cannot regenerate and whose # flow-derived boundary on FUN_80023bf0 was 22 ins short (P31 S79). Seeds now come from the linked # objects themselves — tools/main_seed_ends.py reads each game-code object's .text section from the # link map and slices it at its own nm symbols (the data islands and the LINKED blocks sit BETWEEN # objects, so every slice is exact); sig_image then hashes the ORIGINAL EXE bytes at those boundaries. # This is the sig progress.py weighs main by. Needs a built main (map + objects): without one it # leaves any existing .run/sig.main.jsonl alone and says so — never a heuristic sig under the same # name (R51). sig-main: if [ -f "$(main_MAPFILE)" ]; then $(VENV_PY) tools/main_seed_ends.py --map $(main_MAPFILE) --out .run/seeds.main.txt $(VENV_PY) tools/sig_image.py --image $(main_EXE) --vram-base $(main_VRAM_BASE) --seeds .run/seeds.main.txt --name main echo "sig-main: signed $$(wc -l < .run/seeds.main.txt) main game-code fns (build-true lengths, from the link map + objects) -> .run/sig.main.jsonl" else echo "sig-main: no main build ($(main_MAPFILE) absent) — run 'make check BINARY=main' first; .run/sig.main.jsonl left as is" fi # sdk-dual (P33 A3) — main byte-identical WITH the real PsyQ objects AND WITHOUT them (roadmap contract # §1.2, the fresh-clone fallback invariant), both legs against config/check.us.sha, as ONE target: # leg 1 extract + check -> the SDK objects linked (map must list build/psyq/libcd/) # leg 2 extract + rm build/psyq + check NO_SDK=1 -> the INCLUDE_ASM stub tiles (map must list # build/src/libcd1.o and no build/psyq/) # leg 3 extract + check -> the tree back in its default (WITH) state # `extract BINARY=main` sits between the legs because psyq_integrate rewrites build/us/SLUS_007.26.ld # IN PLACE and extract regenerates it (the incremental trap tools/gate_main.py documents). Refuses to # run when any SDK object dir is absent — running the same leg twice and calling it a dual is exactly # the false green R32 forbids; a public clone's every `make check BINARY=main` already IS leg 2. # Run as `make -j$(nproc) sdk-dual` (the sub-makes inherit the jobserver). Maps kept for the record. # (recursively expanded — the *_ELF variables are defined further down the file) SDK_ELF_DIRS = $(LIBCD_ELF) $(LIBGS_ELF) $(LIBETC_ELF) $(LIBGPU_ELF) $(LIBMCRD_ELF) $(LIBC2_ELF) $(LIBGTE_ELF) $(SND_ELF) $(APICARD_ELF) $(LIBAPI42_ELF) $(LIBPAD_ELF) SDK_DUAL_DIR := .run/P33/verify sdk-dual: @set -e for d in $(SDK_ELF_DIRS); do if [ ! -d "$$d" ]; then echo "[FAIL] sdk-dual: the WITH leg cannot run — $$d is absent (tools/fetch_psyq.sh); refusing to run one leg twice and call it a dual (R32)" exit 1 fi done mkdir -p $(SDK_DUAL_DIR) echo "sdk-dual: leg 1 — WITH the PsyQ objects" $(MAKE) --no-print-directory extract BINARY=main $(MAKE) --no-print-directory check BINARY=main cp $(main_MAPFILE) $(SDK_DUAL_DIR)/main_with_sdk.map grep -q 'build/psyq/libcd/' $(SDK_DUAL_DIR)/main_with_sdk.map || { echo "[FAIL] sdk-dual: leg 1 did not link build/psyq/libcd/ — not a WITH build"; exit 1; } echo "sdk-dual: leg 2 — WITHOUT (NO_SDK=1, build/psyq removed)" $(MAKE) --no-print-directory extract BINARY=main rm -rf build/psyq $(MAKE) --no-print-directory check BINARY=main NO_SDK=1 cp $(main_MAPFILE) $(SDK_DUAL_DIR)/main_no_sdk.map if grep -q 'build/psyq/' $(SDK_DUAL_DIR)/main_no_sdk.map; then echo "[FAIL] sdk-dual: leg 2 linked build/psyq/ — not a WITHOUT build"; exit 1; fi grep -q 'build/src/libcd1.o' $(SDK_DUAL_DIR)/main_no_sdk.map || { echo "[FAIL] sdk-dual: leg 2 did not link the libcd1 stub tile"; exit 1; } echo "sdk-dual: leg 3 — restoring the default (WITH) state" $(MAKE) --no-print-directory extract BINARY=main $(MAKE) --no-print-directory check BINARY=main echo "sdk-dual: OK — main $$(cut -d' ' -f1 $(main_CHECK_SHA)) byte-identical WITH and WITHOUT the PsyQ objects (maps: $(SDK_DUAL_DIR)/main_with_sdk.map, main_no_sdk.map)" # sig-main-oracle (P31 S77) — MAIN'S INDEPENDENT SECOND ORACLE (roadmap contract §1.3). # Distinct from `sig-main` above, which is splat-SEEDED on purpose. This one signs the ORIGINAL EXE # bytes with NO splat symbols: `--vram-base 0x8000F800` puts file offset 0 at vram (so the 0x800 # PS-X EXE header simply falls below the first range), and `--segments` derives the game-code ranges # from the splat yaml's SEGMENT TYPES — coarse structure, never splat's FUNCTION boundaries, which # is the thing the oracle must stay free to disagree with. Entries inside each range are found by # byte-derived jal-closure, because seeding from splat's symbols would make every phantom look real # (docs/second-oracle.md names that trap). LINKED PsyQ blocks are excluded: real library objects, # outside the game-code denominator, and auditing them here would report ~960 phantoms that are # artefacts of comparing two oracles that never measured the same thing. sig-main-oracle: @$(VENV_PY) -c "import sys;sys.path.insert(0,'tools');import progress;progress.set_binary('main');print(','.join(sorted(progress.LINKED_SEGS)))" > .run/main_linked_segs.txt $(VENV_PY) tools/sig_image.py --image $(main_EXE) --vram-base 0x8000F800 \ --segments config/splat.us.exe.yaml --exclude-subsegs "$$(cat .run/main_linked_segs.txt)" \ --bootstrap --out .run/sig.main.oracle.jsonl @$(VENV_PY) -c "import sys;sys.path.insert(0,'tools');import corpus;r=corpus.audit('main');print('sig-main-oracle: main is now INDEPENDENT — %d in-domain stubs, %d PHANTOM, %d TRUNCATED, %d PAD-TAIL'%(r['stubs'],len(r['phantom']),len(r['truncated']),len(r['pad_tail'])))" # sig-modules (P30 S44): sign every module-class binary at ITS OWN vram (from modules.mk) with its # own TEXT_LO (the §154 module-id-word law: code starts past the header; bootstrap from offset 0 # yields 0 functions). Same derived-jobs shape as sig-overlays (R33). Empty MODULE_BINARIES = no-op. # S45: SEED from the built ELF's text symbols when the build exists (R33 — splat's post-link # boundaries are the finer oracle; --bootstrap's linear partition GLUES adjacent functions around # jtbl-dispatch code, which read as 24 TRUNCATED slices in audit-corpus). Fresh-clone fallback # (no build yet) stays --bootstrap; the next sig-modules after a build self-heals. MODULE_SIG_JOBS := $(foreach a,$(MODULE_BINARIES),$(a):$($(a)_EXE):$($(a)_VRAM_BASE):$($(a)_TEXT_LO)) sig-modules: # PARALLEL (P31 S70) — same rationale as sig-overlays. Each job writes only its own # .run/seeds..txt and .run/sig..jsonl, so the fan-out is safe. @mkdir -p .run; : > .run/sig-modules.txt echo "$(MODULE_SIG_JOBS)" | tr ' ' '\n' | sed '/^$$/d' | xargs -P$(JOBS) -I{} sh -c '\ job="{}"; alias=$${job%%:*}; rest=$${job#*:}; f=$${rest%%:*}; rest=$${rest#*:}; \ vram=$${rest%%:*}; tlo=$${rest#*:}; \ if [ ! -f "$$f" ]; then echo "[WARN no payload] $$alias ($$f)"; exit 0; fi; \ elf="build/$$alias/$$alias.elf"; \ if [ -f "$$elf" ]; then \ mipsel-linux-gnu-nm "$$elf" | awk -v lo=$$(($$tlo)) '"'"'$$2=="T" && $$3~"^func_" { a=strtonum("0x" $$1); if (a>=lo && a%4==0) printf "0x%X\n", a }'"'"' | sort -u > ".run/seeds.$$alias.txt"; \ $(VENV_PY) tools/sig_image.py --image "$$f" --vram-base "$$vram" --seeds ".run/seeds.$$alias.txt" --name "$$alias" $${tlo:+--text-lo "$$tlo"} >/dev/null 2>&1 \ && echo "[ OK ] $$alias" || echo "[SIG FAIL] $$alias"; \ else \ $(VENV_PY) tools/sig_image.py --image "$$f" --vram-base "$$vram" --bootstrap --name "$$alias" $${tlo:+--text-lo "$$tlo"} >/dev/null 2>&1 \ && echo "[ OK ] $$alias" || echo "[SIG FAIL] $$alias"; \ fi' | tee .run/sig-modules.txt n=$$(grep -c "^\[ OK \]" .run/sig-modules.txt || true) bad=$$(grep -c "^\[SIG FAIL\]" .run/sig-modules.txt || true) echo "sig-modules: signed $$n modules (of $(words $(MODULE_BINARIES)) onboarded)" if [ "$$bad" -ne 0 ]; then echo "[FAIL] sig-modules: $$bad module(s) failed to sign"; exit 1; fi # print- (P33 B5): echo one Makefile variable so tools derive per-binary facts from the ONE registry # (R33) instead of re-parsing overlays.mk/modules.mk — e.g. `make -s print-EXE BINARY=ov_SC01_077`, # `make -s print-VRAM_BASE BINARY=resident`, `make -s print-BINARIES`. print-%: @echo '$($*)' # bootstrap (P33 B3): the fresh-clone setup — apt presence (printed, never run), the venv from # requirements-python.txt, the submodules, the two cc1 tarballs verified + extracted, then check-env. bootstrap: @tools/bootstrap.sh # ----------------------------------------------------------------------------- # check-env: assert every Phase-4 toolchain component. Runs ALL checks (does not # stop at the first failure) so the report is complete, then exits nonzero if any # hard check failed. binutils >= 2.38 is a WARN, never a FAIL (§4.5). check-env: # DELIBERATE opt-out from the global `-e` (.SHELLFLAGS, Phase-27 T2). This recipe's contract is # "run EVERY preflight check, print EVERY [FAIL], exit with the accumulated status" — it manages # its own `fail` and exits 1 at the end. Under `-e` a probe assignment (e.g. `pyver=$$(python3 # ...)` on a box without python3) would abort at the FIRST problem and hide the rest, turning a # diagnostic into a stop-on-first-error. Accumulate-and-report is correct here; nowhere else. @set +e fail=0 echo "== BFM-decomp environment preflight (Phase 4 check-env) ==" echo # 1) Python >= 3.12 (system python3 drives tooling + the EXE-hash import) pyver=$$($(PYTHON) -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null) if $(PYTHON) -c 'import sys; raise SystemExit(0 if sys.version_info[:2] >= (3,12) else 1)' 2>/dev/null; then echo "[PASS] python3 $$pyver (>= 3.12)" else echo "[FAIL] python3 $${pyver:-not-found} (need >= 3.12)"; fail=1 fi # 2) venv present + splat importable if [ -x "$(VENV_PY)" ]; then if $(VENV_PY) -c 'import splat' 2>/dev/null; then sv=$$($(VENV_PY) -c 'import importlib.metadata as m; print(m.version("splat64"))' 2>/dev/null) echo "[PASS] venv 'import splat' OK (splat64 $${sv:-?})" else echo "[FAIL] venv present but 'import splat' failed (run: $(VENV_PY) -m pip install 'splat64[mips]>=0.41.0,<1.0.0')"; fail=1 fi else echo "[FAIL] $(VENV_PY) missing (run: $(PYTHON) -m venv $(VENV) && $(VENV)/bin/pip install 'splat64[mips]>=0.41.0,<1.0.0')"; fail=1 fi # 3) cc1 candidates executable — R12-clean smoke (stdin -> /dev/null, no temp file) for cc1 in "$(CC1_PSX)" "$(CC1_CDK)"; do if [ -x "$$cc1" ] && echo 'int _ce(){return 0;}' | "$$cc1" $(CC1_SMOKE_FLAGS) -o /dev/null 2>/dev/null; then echo "[PASS] cc1 runs: $$cc1" else echo "[FAIL] cc1 not runnable: $$cc1 (see docs/SETUP.md §4.7)"; fail=1 fi done # 4) maspsx submodule populated if [ -f "$(MASPSX)" ]; then echo "[PASS] maspsx present: $(MASPSX)" else echo "[FAIL] $(MASPSX) missing (run: git submodule update --init)"; fail=1 fi # 4b) the other three submodules (P33 B3) — matching tooling, not build inputs: WARN, not FAIL for sub in tools/asm-differ tools/m2c tools/decomp-permuter; do if [ -n "$$(ls -A "$$sub" 2>/dev/null)" ]; then echo "[PASS] submodule populated: $$sub" else echo "[WARN] submodule empty: $$sub (matching tooling only; run: git submodule update --init)" fi done # 4c) the four TRACKED splat preset headers (P33 B1) — assembled into every object for h in include/include_asm.h include/macro.inc include/labels.inc include/gte_macros.inc; do if [ -f "$$h" ]; then echo "[PASS] preset header present: $$h" else echo "[FAIL] $$h missing (tracked since P33 B1; 'make extract' regenerates it)"; fail=1 fi done # 5) mipsel binutils on PATH (as / ld / objcopy) for t in $(AS) $(LD) $(OBJCOPY); do if command -v $$t >/dev/null 2>&1; then echo "[PASS] $$t: $$($$t --version | head -1)" else echo "[FAIL] $$t not on PATH (apt install binutils-mipsel-linux-gnu)"; fail=1 fi done # 5b) binutils regression line: >= 2.38 -> WARN (not FAIL); §4.5 asver=$$($(AS) --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+' | head -1) if [ -n "$$asver" ]; then amaj=$${asver%%.*}; amin=$${asver##*.} if [ "$$amaj" -gt $(BINUTILS_WARN_MAJOR) ] || { [ "$$amaj" -eq $(BINUTILS_WARN_MAJOR) ] && [ "$$amin" -ge $(BINUTILS_WARN_MINOR) ]; }; then echo "[WARN] mipsel binutils $$asver >= 2.38 — PS1-matching regression suspect (2.35 known-good); revisit in Phase 5 (docs/SETUP.md §4.5)" else echo "[PASS] mipsel binutils $$asver (< 2.38)" fi fi # 6) the extracted EXE hash == EXPECTED_EXE_SHA1 (reused constant). P33 B1: the EXE is no longer # committed — it is regenerated from the user's disc by `make disc-extract`, so its absence is a # WARN with the instruction, not a FAIL; a PRESENT but wrong EXE is still a FAIL. if [ -f "$(EXE)" ]; then want=$$($(PYTHON) -c 'from tools.bfm_extract.extract_exe import EXPECTED_EXE_SHA1 as h; print(h)' 2>/dev/null) got=$$(sha1sum "$(EXE)" | cut -d' ' -f1) if [ -n "$$want" ] && [ "$$got" = "$$want" ]; then echo "[PASS] $(EXE) sha1 $$got == EXPECTED_EXE_SHA1" else echo "[FAIL] $(EXE) sha1 $${got:-none} != expected $${want:-unknown}"; fail=1 fi else echo "[WARN] $(EXE) absent — run 'make disc-extract' with your redump dump in $(DISC_DIR)/ (docs/SETUP.md §4.4)" fi # 7) the committed manifest oracle is self-consistent (manifest.sha1 == sha1(manifest.jsonl)); ms. if [ -f "$(EXTRACT_ROOT)/manifest.jsonl" ] && [ -f "$(EXTRACT_ROOT)/manifest.sha1" ]; then mgot=$$(sha1sum "$(EXTRACT_ROOT)/manifest.jsonl" | cut -d' ' -f1) mwant=$$(cat "$(EXTRACT_ROOT)/manifest.sha1") if [ "$$mgot" = "$$mwant" ]; then echo "[PASS] $(EXTRACT_ROOT)/manifest.sha1 == sha1(manifest.jsonl) ($$mgot)" else echo "[FAIL] $(EXTRACT_ROOT)/manifest.sha1 ($$mwant) != sha1(manifest.jsonl) ($$mgot) — the committed oracle is inconsistent"; fail=1 fi else echo "[FAIL] $(EXTRACT_ROOT)/manifest.jsonl + manifest.sha1 missing (the committed extraction oracle)"; fail=1 fi if [ -f "$(DISC_TRACK1)" ]; then echo "[INFO] disc dump present: '$(DISC_TRACK1)'"; else echo "[INFO] no disc dump under $(DISC_DIR)/ (needed only to (re)generate extracted/)"; fi # 8) the extracted payload census (P33 B3): how many of the fleet's inputs exist (INFO — disc-extract makes them) present=0; for p in $(foreach b,$(BINARIES),$($(b)_EXE)); do [ -f "$$p" ] && present=$$((present+1)); done echo "[INFO] extracted payloads present: $$present / $(words $(BINARIES)) binaries$$( [ "$$present" -eq $(words $(BINARIES)) ] || echo ' — run: make disc-extract')" echo if [ "$$fail" -ne 0 ]; then echo "check-env: FAIL — see the [FAIL] lines above." exit 1 fi echo "check-env: OK — Phase-4 toolchain ready." # ----------------------------------------------------------------------------- # Phase-5 build: splat split -> assemble -> link -> objcopy -> SHA1 check. # The code is 100% assembly (the "all-asm byte-match" milestone). The cpp->cc1-> # maspsx->as path is documented below but dormant until Phase 6 adds `c` segments. SPLAT := $(VENV_PY) -m splat CPP := $(MIPS_PREFIX)cpp # (SPLAT_YAML / OUT_DIR / OUT / ELF / MAPFILE / LD_SCRIPT / CHECK_SHA are per-binary # aliases in the "Binaries" data block near the top of this file — Phase 9. UNDEF_SYMS / # UNDEF_FUNCS / ASM_DIR / SRC_DIR joined them per-binary in Phase 10: a second binary # writes its undefined_*_auto under build// and nests its sources under /.) # P33 A3 — NO_SDK=1 builds main from splat's INCLUDE_ASM stub tiles even when the SDK object dirs # exist: the WITHOUT leg of the with/without-SDK dual (roadmap contract §1.2 — the fresh-clone # fallback invariant). Skips every psyq_integrate rewrite below AND the -T externals fragments, so # the link is exactly what a public clone without Sony's objects performs. `make sdk-dual` runs both # legs and asserts both SHA1s; until now the WITHOUT leg was only ever exercised by hand # (`mv .run/obj40 .run/obj40.off`) and it regressed once unnoticed (config/symbols.us.txt:248). NO_SDK ?= # Phase 7 (Task 2'): link the real PsyQ libcd SDK objects in place of the libcd-region asm stubs. # tools/psyq_integrate.py rewrites the splat .ld (swap stub objects -> build/psyq/libcd/*.o + NOLOAD # data placement, no carving) and emits the externals defsym fragment. Conditional on the SDK ELF # objects being present (gitignored, SDK-derived, via tools/psyq_build_libs.sh LIBCD); a fresh clone # without them builds byte-identically via the stubs. LIBCD_ELF := .run/obj40/libcd LIBCD_OBJDIR := build/psyq/libcd LIBCD_SYMS := build/psyq/libcd_externals.ld # libgs (Phase 7 Task #9, FULL integration; S78 #3/#4): 34 libgs objects in 8 blocks linked in place # of the libgs1..libgs8 block stubs (libgs7 = 2D_BG0/2D_BG1, S78 #3; libgs8 = GS_001 via the link-prepare # .bss split, S78 #4; the 4 remaining gaps are libgte objects, libgte27-30). Same conditional/ # idempotent model as libcd. The curated object dir is SDK-derived (gitignored), regenerated by # tools/make_libgs.sh (needs the LIBGS ELF from psyq_build_libs.sh LIBGS). GS_106 (block 4) anchors # uniquely only within the libgs window, so the integrate call passes 0x8005080C 0x80057928. LIBGS_ELF := .run/obj40/libgs_used LIBGS_OBJDIR := build/psyq/libgs LIBGS_SYMS := build/psyq/libgs_externals.ld # libetc (Phase 8): 5 objects (VSYNC/INTR/INTR_VB/INTR_DMA/VMODE) in ONE contiguous block at the tail # of the old 800 subseg (ends at libcd1). Single stub "libetc"; no placement window needed (all 5 # anchor uniquely over the full text window). Same conditional/idempotent model as libcd/libgs. LIBETC_ELF := .run/obj40/libetc LIBETC_OBJDIR := build/psyq/libetc LIBETC_SYMS := build/psyq/libetc_externals.ld # libgpu (Phase 8; S78 #4): EXT+PRIM (block `libgpu`) + SYS.o (block `libgpu2`, 3109 ins). SYS.o was # EXCLUDED from Phase 8 to P31 S78 as scattered-.bss (cookbook §9.1, the GS_001 class) and lived in 800c # as hand-matched SDK C + verbatim frags; psyq_integrate now splits such a .bss into per-base NOLOAD # pieces at link-prepare (tools/psyq_bss_split.py, cookbook §489), so the raw psyq_build_libs.sh LIBGPU # output links directly — no curated dir (psyq_identify drops the 8 objects the EXE does not link). LIBGPU_ELF := .run/obj40/libgpu LIBGPU_OBJDIR := build/psyq/libgpu LIBGPU_SYMS := build/psyq/libgpu_externals.ld # libmcrd (Phase 8): 2 objects (LIBMCRD.o = the 55 LIBMCRD_OBJ_* + _card_* memcard I/O; USERFUNC.o), 2 # non-adjacent blocks. Clean (.bss commons all recovered). NB: these are the libmcrd SDK objects; the # GAME's SaveLoadRoutine/Q#5 save logic is a separate Phase-12 item. LIBMCRD_ELF := .run/obj40/libmcrd LIBMCRD_OBJDIR := build/psyq/libmcrd LIBMCRD_SYMS := build/psyq/libmcrd_externals.ld # libc2 (Phase 8): C stdlib, 17 objects, 2 blocks (16-obj main run libc2_1 + STRCAT.o libc2_2). Clean # (PRNT.o's printf-format jtbl resolves via NOLOAD .rodata). LIBC2_ELF := .run/obj40/libc2 LIBC2_OBJDIR := build/psyq/libc2 LIBC2_SYMS := build/psyq/libc2_externals.ld # libgte (Phase 8): GTE math, 53 objects in 22 blocks across the 800b region; S78 +4 blocks (libgte23..26: the # MSC/SMP_00/FGO/PATCHGTE objects the §485 psyq_identify fix located in the former 800b* 'game code' gaps) + libgte9 re-derived as SMP_05 (subseg lines generated by # tools/gen_lib_subsegs.py). The integrate window 0x4787C..0x51804 restricts placement to 800b so it # sees 22 blocks (excludes the 5 deferred libgs-gap objects MTX_05/07/11/REG03/REG11; gsgap1/2/4/5 stay # stubs). Clean (no scattered .bss). stub list = libgte1..libgte22. LIBGTE_ELF := .run/obj40/libgte LIBGTE_OBJDIR := build/psyq/libgte LIBGTE_SYMS := build/psyq/libgte_externals.ld LIBGTE_STUBS := libgte1,libgte2,libgte3,libgte4,libgte5,libgte6,libgte7,libgte8,libgte9,libgte10,libgte11,libgte12,libgte13,libgte14,libgte15,libgte16,libgte17,libgte18,libgte19,libgte20,libgte21,libgte22,libgte23,libgte24,libgte25,libgte26,libgte27,libgte28,libgte29,libgte30 # Combined libspu+libsnd sound region (Phase 8; S78 #3/#4): the two SDK sound libs interleave in # 0x3A444..0x4239C so they link as one 63-object region (snd1..snd12). Curated dir .run/obj40/snd_used # built by tools/make_snd_used.py (3 addresses excluded as cross-object-common/false-positive stubs; # VM_F rejoined in S78 #4 via the link-prepare .bss split, cookbook §489). Window arg below. SND_ELF := .run/obj40/snd_used SND_OBJDIR := build/psyq/snd SND_SYMS := build/psyq/snd_externals.ld SND_STUBS := snd1,snd2,snd3,snd4,snd5,snd6,snd7,snd8,snd9,snd10,snd11,snd12 # Combined libapi+libcard 800c2 region (Phase 8; S79 #5): 26 objects in 7 blocks (apicard1..7) tiling # 0x80061F38..0x80062888 with no game code left between them. Curated dir .run/obj42/apicard_used # (tools/make_apicard_used.py: libapi from tools/psyq/lib421 = 4.2, the EXE's real libapi; libcard 4.0). # Window 0x61F38..0x62888. The former 800c2/800c2_2/800c2_3 "game code" rows were FIRST.o / PAD.o / # PATCH.o+CHCLRPAD.o (libapi 4.2 C objects) — apicard5/6/7. APICARD_ELF := .run/obj42/apicard_used APICARD_OBJDIR := build/psyq/apicard APICARD_SYMS := build/psyq/apicard_externals.ld APICARD_STUBS := apicard1,apicard2,apicard3,apicard4,apicard5,apicard6,apicard7 # The libapi 4.2 + libpad 4.2.1 band (S78 #12 named it, S79 #13 found the archive, S79 #5 wired it): # 0x8005CE18..0x8005FC68 = 33 interleaved Sony objects, all byte-identical from tools/psyq/lib421 # (SCE's 1998-02-26 "libpad.lib 4.2.1 for the DUAL SHOCK" patch + libapi.lib 4.2; ELF regenerated into # .run/obj42/{libapi42,libpad421} by psyq_lib_split.py + psyq-obj-parser — see docs/SETUP.md). Two raw # dirs, two calls, each windowed to its own objects and tiling its own stubs (libapi1/libapi2, # libpad1/libpad2). This was the src/800c3.c "REORDER_TUS island" — 129 hand-matched "C", 62 verbatim # bodies, 19 stubs incl. the four §332 "%lo-in-a-delay-slot walls": Sony code assembled in reorder mode. LIBAPI42_ELF := .run/obj42/libapi42 LIBAPI42_OBJDIR := build/psyq/libapi42 LIBAPI42_SYMS := build/psyq/libapi42_externals.ld LIBAPI42_STUBS := libapi1,libapi2 LIBPAD_ELF := .run/obj42/libpad421 LIBPAD_OBJDIR := build/psyq/libpad LIBPAD_SYMS := build/psyq/libpad_externals.ld LIBPAD_STUBS := libpad1,libpad2 # Assembler flags (docs/SETUP.md §6.2). -G0 is confirmed by the disassembly # (ledger #8: zero $gp-relative addressing). -no-pad-sections keeps section ends # un-padded so the link reproduces the original layout. ASFLAGS := -Iinclude -march=r3000 -mtune=r3000 -no-pad-sections -O1 -G0 # maspsx ASPSX version — ALWAYS explicit (G8). Inert for the all-asm build; the # real pin is Phase 6. (Only used on the future cpp->cc1->maspsx `c` path.) ASPSX_VERSION := 2.56 # Extra maspsx flags. --expand-div is PINNED (Phase-6 fingerprint): the original # emits the full aspsx div sequence (divu + bnez + break 0x7 zero-check); without it # maspsx leaves a bare divu and div/rem functions never match. Only affects div/rem, # so the all-INCLUDE_ASM build and div-free functions are unchanged. MASPSX_FLAGS := --expand-div # Phase-29 §8e: per-object jump-table pad spec (tools/jtbl_rodata_pads.py). Set ONLY as a # target-specific var by tools/jtbl_carve.py in config/overlays.mk for multi-table .rodata # carve spans; the file-scope empty default shields the recipe from an inherited environment # variable accidentally arming the filter fleet-wide (a plain `JTBL_PADS=... make` would # otherwise become a global make var). Unset => the compile pipeline is byte-unchanged. JTBL_PADS := # Object set must match the splat linker script's references. After the Phase-6 asm->c # flip the text subseg is src/800.c -> build/src/800.o; the per-function # asm/nonmatchings//*.s are TEXTUALLY .include'd by the .c (via INCLUDE_ASM) at # assembly time, so they are NOT separate objects and must be excluded from the glob. # header.s and the data subseg stay asm. Globbed at parse time -> run the canonical # `make extract && make build`. # Per-binary object scoping (Phase 10): main's sources live at the repo-level asm/ + src/; # a second binary (resident) nests at asm// + src//. The active binary's roots are # $(ASM_DIR)/$(SRC_DIR). main's roots CONTAIN the nested siblings, so they must be pruned from # main's glob (else resident's .s/.c contaminate main's OBJS and the link). The prune list is # DERIVED FROM $(BINARIES) — the $(filter $(ASM_DIR)/%,...) guard prunes only a sibling whose # root is genuinely nested under the active root, so it self-balances as binaries are added. OTHER_BINS := $(filter-out $(BINARY),$(BINARIES)) ASM_PRUNE := $(foreach b,$(OTHER_BINS),$(if $(filter $(ASM_DIR)/%,$($(b)_ASM_DIR)),-not -path '$($(b)_ASM_DIR)/*')) SRC_PRUNE := $(foreach b,$(OTHER_BINS),$(if $(filter $(SRC_DIR)/%,$($(b)_SRC_DIR)),-not -path '$($(b)_SRC_DIR)/*')) ASM_SRCS := $(shell find $(ASM_DIR) -name '*.s' -not -path '$(ASM_DIR)/nonmatchings/*' $(ASM_PRUNE) 2>/dev/null) # -not -name '.*': a tool's LIVE dotfile probe (masked_diff's src/.masked_diff_probe..c, written and deleted # within one process) must never enter the object list — it was present at parse time and gone at compile time, # and gate_main's clean rebuild died on "No rule to make target build/src/.masked_diff_probe.N.o" (P32 S83). C_SRCS := $(shell find $(SRC_DIR) -name '*.c' -not -name '.*' $(SRC_PRUNE) 2>/dev/null) OBJS := $(ASM_SRCS:%.s=build/%.o) $(C_SRCS:%.c=build/%.o) # Header-dependency tracking (Phase 15): now that shared headers (src/shared/*.h, common.h) # are build inputs, the cpp stage emits a .d per C object (-MMD, below) so editing a #included # header triggers a recompile — incremental `make check` stays trustworthy (R22). .d files live # under build/ (gitignored); -include ignores them on the first build. No effect on output bytes. C_DEPS := $(C_SRCS:%.c=build/%.d) -include $(C_DEPS) # ---- Phase 35 T3: a TWIN binary builds its PRIMARY's sources into its OWN objects ------------------------------------- # Five overlay pairs are byte-identical payloads (equal config/check.*.sha). "One source per unique function" makes a twin # a binary with NO source directory of its own: config/overlays.mk declares `_TWIN_OF := ` and points # `_SRC_DIR` at the primary's directory (so every tool that asks corpus.src_dir sees the one source). The twin keeps # its OWN splat yaml (create_c_files: False — splat must never recreate stub files for the deleted directory), asm/, linker # script and object names (build/src//_.o), so a parallel `check-all` never races the primary's objects; # the static pattern rules below compile them from src//_.c with the very same recipe as # build/src/%.o. The -O0 objects (the whale's _o0b/_o0c, the cluster's _o0) keep their flags under the twin's names. TWIN_OF := $($(BINARY)_TWIN_OF) ifneq ($(TWIN_OF),) TWIN_SRCDIR := $($(TWIN_OF)_SRC_DIR) TWIN_C_SRCS := $(shell find $(TWIN_SRCDIR) -name '$(TWIN_OF)*.c' -not -name '.*' 2>/dev/null) TWIN_OBJS := $(patsubst $(TWIN_SRCDIR)/$(TWIN_OF)%.c,build/src/$(BINARY)/$(BINARY)%.o,$(TWIN_C_SRCS)) OBJS := $(ASM_SRCS:%.s=build/%.o) $(TWIN_OBJS) C_DEPS := $(TWIN_OBJS:%.o=%.d) -include $(C_DEPS) $(filter-out build/src/$(BINARY)/$(BINARY).o,$(TWIN_OBJS)): build/src/$(BINARY)/$(BINARY)%.o: $(TWIN_SRCDIR)/$(TWIN_OF)%.c @mkdir -p $(dir $@) @echo " CC $@ (twin of $(TWIN_OF): $<)" @set -o pipefail; $(CPP) $(CPPFLAGS) -MMD -MP -MT $@ -MF $(@:.o=.d) $< | $(CC1_PSX) $(CC1FLAGS) | $(if $(filter $*,$(REORDER_TUS)),$(VENV_PY) tools/reorder_passthrough.py | $(AS) $(ASFLAGS_REORDER) -o $@,$(VENV_PY) $(MASPSX) --aspsx-version=$(ASPSX_VERSION) $(MASPSX_FLAGS) $(if $(JTBL_PADS),| $(VENV_PY) tools/jtbl_rodata_pads.py --pads $(JTBL_PADS),$(if $(filter md_% main,$(BINARY)),| $(VENV_PY) tools/jtbl_rodata_pads.py --derive $(BINARY) --tu $(notdir $*))) | $(AS) $(ASFLAGS) -o $@) build/src/$(BINARY)/$(BINARY).o: $(TWIN_SRCDIR)/$(TWIN_OF).c @mkdir -p $(dir $@) @echo " CC $@ (twin of $(TWIN_OF): $<)" @set -o pipefail; $(CPP) $(CPPFLAGS) -MMD -MP -MT $@ -MF $(@:.o=.d) $< | $(CC1_PSX) $(CC1FLAGS) | $(VENV_PY) $(MASPSX) --aspsx-version=$(ASPSX_VERSION) $(MASPSX_FLAGS) $(if $(JTBL_PADS),| $(VENV_PY) tools/jtbl_rodata_pads.py --pads $(JTBL_PADS),) | $(AS) $(ASFLAGS) -o $@ TWIN_O0_OBJS := $(patsubst $(TWIN_SRCDIR)/$(TWIN_OF)%.c,build/src/$(BINARY)/$(BINARY)%.o,$(wildcard $(TWIN_SRCDIR)/$(TWIN_OF)_o0.c $(TWIN_SRCDIR)/$(TWIN_OF)_o0?.c)) $(TWIN_O0_OBJS): CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker endif # splat `bin` subsegs (raw byte regions — e.g. an overlay's trailing non-word-aligned bytes that # spimdisasm's data path drops, since it won't emit a <4-byte partial word). splat extracts them to # assets//*.bin and references build/assets//*.o in the .ld; wrap each raw .bin into a # linkable object (bytes verbatim in .data). Per-binary: asset_path is scoped to assets/ so # overlays' same-named `trailing.bin` never collide; main/resident have no assets -> empty. ASSET_BINS := $(shell find assets/$(BINARY) -name '*.bin' 2>/dev/null) ASSET_OBJS := $(ASSET_BINS:assets/%.bin=build/assets/%.o) # ----------------------------------------------------------------------------- # disc-extract (P33 B1): the rom->decoder step. The repository ships NO ROM bytes (H1): every # payload under extracted/ — the EXE, the .CD archives, the PAC entries, the LZSS-decoded 0.4.dec # overlays and module blobs — is regenerated from the USER'S OWN redump dump by # tools/bfm_extract/extract.py, and the result is compared against the COMMITTED oracle # extracted/retail/manifest.jsonl (1,801 rows; manifest.sha1 is its hash). The oracle is never # written by a build step (--expect-manifest compares; a plain `extract.py` run is how the oracle # was made). Idempotent: a tree that already matches the oracle is a ~5 s hash probe and a no-op. # The oracle lists the 3 .DA audio files from Tracks 2-4, so the canonical input is the 4-track # BIN/CUE; a Track-1-only dump is accepted with an explicit PARTIAL verdict (never a silent pass). DISC_DIR ?= disks DISC_TRACK1 := $(DISC_DIR)/Brave Fencer Musashi (USA) (Track 1).bin DISC_TRACK2 := $(DISC_DIR)/Brave Fencer Musashi (USA) (Track 2).bin EXTRACT_ROOT := extracted/retail disc-extract: @set -e audio="" if [ ! -f "$(DISC_TRACK2)" ]; then audio="--allow-missing-audio"; fi if $(PYTHON) tools/bfm_extract/extract.py --verify --out "$(EXTRACT_ROOT)" $$audio >/dev/null 2>&1; then echo "disc-extract: up to date — $(EXTRACT_ROOT)/ matches the committed manifest (sha1 $$(cat $(EXTRACT_ROOT)/manifest.sha1))" exit 0 fi if [ ! -f "$(DISC_TRACK1)" ]; then echo "[FAIL] disc-extract: no disc dump at '$(DISC_TRACK1)'" echo " Stage your own redump dump of Brave Fencer Musashi (USA) — the 4-track BIN/CUE (Track 1 = data;" echo " Tracks 2-4 = the .DA audio) — under $(DISC_DIR)/ (docs/SETUP.md §4.4). The repository ships no ROM bytes (H1)." exit 2 fi # 1) the dump is the canonical one: full-track SHA1 + CRC32 vs redump (refuses a non-canonical dump) $(PYTHON) tools/bfm_extract/extract_exe.py --bin "$(DISC_TRACK1)" --verify-disc if [ -n "$$audio" ]; then echo "disc-extract: NOTE — '$(DISC_TRACK2)' absent: the 3 .DA audio files are not extracted; the result is PARTIAL"; fi # 2) extract everything and COMPARE against the committed oracle (writes nothing on a match) $(PYTHON) tools/bfm_extract/extract.py --bin "$(DISC_TRACK1)" --out "$(EXTRACT_ROOT)" --expect-manifest "$(EXTRACT_ROOT)/manifest.jsonl" $$audio # 3) the idempotency probe of the tree that was just written $(PYTHON) tools/bfm_extract/extract.py --verify --out "$(EXTRACT_ROOT)" $$audio echo "disc-extract: OK — $(EXTRACT_ROOT)/ regenerated from '$(DISC_TRACK1)' and verified against the committed manifest (sha1 $$(cat $(EXTRACT_ROOT)/manifest.sha1))" # extract: splat split -> asm/, the linker script, include/ macros, undefined_*_auto.txt. extract: @mkdir -p $(OUT_DIR) # P33 B1: the payload is the user's — regenerate extracted/ from the disc when it is absent (H1). if [ ! -f "$(EXE)" ]; then $(MAKE) --no-print-directory disc-extract; fi # A re-extract REWRITES every .s — and an object's assembly arrives through INCLUDE_ASM, which # expands to a `.include` consumed by maspsx/as AFTER cpp. So `.o <- .s` is NOT a dependency make # can see (-MMD tracks headers only), and an incremental build after an extract silently links # STALE OBJECTS. That is not merely slow: INCLUDE_ASM pastes the ORIGINAL assembly, so a stale # object still contributes the original bytes — the image stays byte-identical and SHA1 goes GREEN # while the split that was just changed is never exercised. A broken config/ change can therefore # be "verified" by an incremental build. (Found live in Phase 26-A: 8 of 136 binaries linked # against stale objects; they failed loudly only by luck, because the dead symbol happened to be # an undefined reference. A merely-different-but-valid split would have gone green on all 136.) # R22/H3 already legislate this ("clean rebuild"; "make clean after any config/ change") — but a # rule that depends on a human remembering is not a gate. Make it structural: invalidate here. ifeq ($(BINARY),main) # main's objects are TOP-LEVEL (build/src/*.o, build/asm/*.o); every other binary lives in its own # subdir. -maxdepth 1 so `make extract` for main cannot delete an overlay's objects. find build/src build/asm -maxdepth 1 -type f \( -name '*.o' -o -name '*.d' \) -delete 2>/dev/null || true else rm -rf build/src/$(BINARY) build/asm/$(BINARY) endif $(SPLAT) split $(SPLAT_YAML) ifeq ($(BINARY),main) # Phase 7 (LZSS): reorder splat's section-major .main into the real # .data(front) -> .rodata -> .data(tail) sandwich, so the migrated LZSS # jtbl_80072A38 (800.o .rodata) lands at 0x80072A38 between 531DC.data and # 6324C.data. Idempotent; keyed off splat's exact output (re-run = no-op). # EXE-only (overlays have no rodata island) — gated to BINARY=main; --front/--tail # name the sandwich .data objects (cookbook §8). # P31 S72: main's island is now a 7-PIECE sandwich (three .rodata carves, one per # jtbl-span-owning code object), so --front/--tail can no longer express it. --order # takes the address-ordered leaf list: a *.data.o leaf contributes its (.data), a code # object leaf contributes its (.rodata) carve. $(PYTHON) tools/ld_interleave.py --order 53198.data.o,800.o,63470.data.o,800_b.o,800_b_2.o,63940.data.o,800_c.o,63C4C.data.o $(LD_SCRIPT) endif # Phase-26 §8: overlays that carve a jr-function's jtbl into a dotted .rodata subseg run # ld_interleave to place the migrated .rodata between the pre/post data-tail chunks (the # data->rodata->data sandwich; cookbook §8). _JTBL_INTERLEAVE holds the --front/--tail # object basenames (set per overlay in config/overlays.mk). Empty for overlays with no carve. # NOTE (P31 S72): this --front/--tail form is the OVERLAY path only. main is driven by the # --order call in the BINARY=main branch above — its island is 7 pieces, which --front/--tail # cannot express (cookbook §426/§431). ifneq ($(JTBL_INTERLEAVE),) $(PYTHON) tools/ld_interleave.py --section .$(BINARY) $(JTBL_INTERLEAVE) $(LD_SCRIPT) endif # The linker script is an `extract` output, not produced by `build` — guard with a # friendly message instead of make's raw "No rule to make target". $(LD_SCRIPT): @echo "make: $(LD_SCRIPT) missing — run 'make extract' first."; exit 1 # Assemble one splat .s (all-asm path). build/asm/%.o: asm/%.s @mkdir -p $(dir $@) @echo " AS $@" @$(AS) $(ASFLAGS) -o $@ $< # Wrap a splat `bin` asset (raw bytes) into a linkable object: assemble a one-line stub that # .incbin's the raw file into .data (format-safe — same mipsel-as as everything else, no objcopy # -I binary arch guessing). The .ld pulls it by path. The --set-section-alignment forces .data to # 1-byte align (as defaults it to 16) so ld places the 1-3 trailing bytes at the exact word-floor # offset and does NOT pad the image up to a 16/8-byte boundary (that added a stray byte otherwise). build/assets/%.o: assets/%.bin @mkdir -p $(dir $@) @echo " INCBIN $@" @printf '.section .data\n.incbin "%s"\n' "$<" | $(AS) $(ASFLAGS) -o $@ @$(OBJCOPY) --set-section-alignment .data=1 $@ # C path (Phase 6): modern cpp -> vintage cc1 -> maspsx -> modern as. Each src/*.c is # splat-generated INCLUDE_ASM stubs (file-scope __asm__ .include of the per-function # asm/nonmatchings//.s); as we match, stubs are replaced by real C. The flags # below are the docs/SETUP.md §5.4 FIRST-CANDIDATE set — provisional until the Phase-6 # fingerprint ladder PINS the triple (then this block + ASPSX_VERSION are updated, G8). CPPFLAGS := -lang-c -Iinclude -undef -Wall -fno-builtin -Dmips -D__GNUC__=2 -D__OPTIMIZE__ -Dpsx -D_PSYQ -D_MIPSEL -D_LANGUAGE_C CC1FLAGS := -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker # The optional jtbl_rodata_pads stage (Phase-29 §8e) is inserted only when the object has a # JTBL_PADS target-specific var (written by tools/jtbl_carve.py for multi-table .rodata carve # spans): it replaces cc1's per-table `.align 3` with the ORIGINAL's exact pad bytes, so a merged # span reproduces the original packing regardless of section-start parity. Unset => stage absent, # pipeline byte-identical to pre-§8e. # MODULES (P31 S62 T3a): every md_* object runs the filter in --derive mode instead — the pads are # derived at build time from the retail island + the maspsx stream (cookbook §303), so no spec is # stored and nothing can drift. A stored JTBL_PADS still wins if one is set. # §332b — THE REORDER ISLAND. `800c2` / `800c3` were originally assembled in REORDER mode (the # assembler filled the delay slots). maspsx force-emits `.set noreorder`, which makes that # unreachable and made a whole class there look like a permanent compiler wall (§332). For these # TUs only, swap maspsx for tools/reorder_passthrough.py + `as -O2` — the pipeline # tools/oracle_reorder.py proved byte-exact (0 diffs on func_80061FA8 vs 57 on the pinned path). # The whole-binary SHA1 gate is the arbiter: if this were wrong the build simply fails. # The §332b -O2 reorder island. THE STEMS MUST BE LISTED INDIVIDUALLY: `$(filter $*,...)` is an # exact match, so `800c2` does NOT cover `800c2_2`/`800c2_3` — those TUs were assembled through # maspsx while their siblings went through reorder_passthrough, which is why func_80062388's # `lui at / jr ra / sw a0,lo(at)` read as COMPILER-INEXPRESSIBLE (P31 S75): a probe showed cc1 + # reorder_passthrough + `as -O2` emits exactly that sequence. It was a build-config gap, not a # gcc limit (cookbook §452 corrected). # S79 #5: the island is EMPTY — all four TUs (800c2, 800c2_2, 800c2_3, 800c3) were libapi 4.2 / libpad 4.2.1 # objects and are LINKED now (apicard5-7, libapi1/2, libpad1/2). The mechanism stays for any future # reorder-assembled TU (cookbook §332b); match_one/rtu_match read this list and tolerate it empty. REORDER_TUS := ASFLAGS_REORDER := -Iinclude -march=r3000 -mtune=r3000 -no-pad-sections -O2 -G0 build/src/%.o: src/%.c @mkdir -p $(dir $@) @echo " CC $@" @set -o pipefail; $(CPP) $(CPPFLAGS) -MMD -MP -MT $@ -MF $(@:.o=.d) $< | $(CC1_PSX) $(CC1FLAGS) | $(if $(filter $*,$(REORDER_TUS)),$(VENV_PY) tools/reorder_passthrough.py | $(AS) $(ASFLAGS_REORDER) -o $@,$(VENV_PY) $(MASPSX) --aspsx-version=$(ASPSX_VERSION) $(MASPSX_FLAGS) $(if $(JTBL_PADS),| $(VENV_PY) tools/jtbl_rodata_pads.py --pads $(JTBL_PADS),$(if $(filter md_% main,$(BINARY)),| $(VENV_PY) tools/jtbl_rodata_pads.py --derive $(BINARY) --tu $(notdir $*))) | $(AS) $(ASFLAGS) -o $@) # Per-module optimization override (SETUP §5.5 — per-module compiler mixing). The boot/ # main/game-mode-dispatch module (src/boot.c, vram 0x80010000-0x800123F0) was compiled at # -O0, NOT the -O2 game-code default: frame-pointer setup + unfolded large-offset loads # are the evidence (GameModeDispatch byte-matches only at -O0). gcc 2.7.2 has no # per-function optimize pragma, so opt level is per-file. Target-specific CC1FLAGS (the # pattern recipe reads $(CC1FLAGS), so this overrides it for just build/src/boot.o): build/src/boot.o: CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker # Phase-19 T1: same per-file -O0 mechanism for the ov_SC01_077 -O0 cluster (16 contiguous fns # vram 0x8013B568..0x8013C98C, prologue sig 21F0A003). Split into its own .c by the splat config # (config/splat.ov_SC01_077.yaml) so this override reaches just that .o. build/src/ov_SC01_077/ov_SC01_077_o0.o: CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker # Phase-24: the whale func_80144B9C is a 2nd -O0 region (0x80144B9C..0x801457A4) present in EVERY # overlay (reach-134), carved into its own object _o0b by each overlay's splat config; the # struct-assign memcpy matches only at -O0. One wildcard rule -O0-compiles all overlays' _o0b.o # (the ×134 rollout; tools/rollout_whale_o0.py — one-shot, retired S45). All share src/shared/func_80144B9C.h. # # P30 T2: the glob is `_o0?` (was `_o0b`) so ANY lettered -O0 sub-split is covered by this one rule. # A 4th -O0 region was found inside an -O2 jr split (0x80183CF0..0x80184920, 15 contiguous fns in # ov_SC03_014 + ov_SC03_015) — the "carve within a carve": the containing object is sub-split into # pre/-O0/post and the middle region named _o0c. Without the widened glob each new region would # need its own hand-added rule, and a MISSED rule is silent: the region compiles at -O2 and every # residual it produces is a pure artifact (§116 — opt level is a property of the FILE). # corpus.o0_sources() parses this rule and resolves `?` via glob, so the -O0 oracle stays correct. # P31 S68: glob widened to src/md_*/ too — module binaries now get -O0 sub-splits # (md_MAIN_003_o0c, the 9-stub o0 carve, is the first). Without this a `src/md_*` region # file silently compiled -O2: byte-neutral while stub-only (INCLUDE_ASM is verbatim asm), # but every -O0 draft banked into it would mystery-fail the gate (§362's trap class). # corpus.o0_sources() splits multi-glob $(wildcard ...) specs, so the -O0 oracle follows. # P31 S77: the glob now covers TOP-LEVEL src/*_o0?.c too — main's TUs are top-level files, not # src/main/*, so every -O0 island in the EXE was outside this rule. Measured: func_8002C410 # MATCHES 299/299 at -O0 and DIFFs 228-vs-299 at -O2, and could not bank for want of this one # glob. Same main-blindness family as draw_waves drawing zero main functions (S76). WHALE_O0B_OBJS := $(patsubst src/%.c,build/src/%.o,$(wildcard src/ov_*/ov_*_o0?.c src/md_*/md_*_o0?.c src/*_o0?.c)) $(WHALE_O0B_OBJS): CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker # Phase-29 T2 Arm A: the -O0 cluster (0x8013B568..0x8013C98C) carved per single-file overlay into # _o0.o (tools/rollout_o0_cluster.py — one-shot, retired S45; the generic driver is tools/rollout_o0.py) — same per-file -O0 mechanism so its h_seq family members # bank whole-binary (the Task-1 swing verdict: they masked-MATCH only at -O0). One wildcard rule # -O0-compiles every overlay's _o0.o; ov_SC01_077_o0.o already has its explicit rule above (filtered # out to avoid a duplicate target-specific assignment). `*_o0.c` never matches the whale's `*_o0b.c`. O0_CLUSTER_OBJS := $(patsubst src/%.c,build/src/%.o,$(filter-out src/ov_SC01_077/ov_SC01_077_o0.c,$(wildcard src/ov_*/ov_*_o0.c))) $(O0_CLUSTER_OBJS): CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker # md_MAIN_011 IS AN ENTIRELY -O0 MODULE (P31 S59, census in .run/s59_o0/). All 21 functions in its # single code subseg carry the -O0 frame-pointer prologue (`sw $fp` + `addu $fp,$sp,$zero`), the # .c is stub-only, and no -O0 glob matches `src/md_*/` at all — so its functions were unbankable no # matter how good a draft was, and the wave draw now refuses to draw them (build_wave_atlas's # `o0-in-an-O2-object` skip). Whole-object override, the `boot` precedent (§6): no splat change, no # carve, and therefore none of the §18-P29 re-disassembly risk. Byte-neutral while the file is # stub-only — proven by gating md_MAIN_011 when this landed. `corpus.o0_sources()` parses this rule, # so every -O0-aware tool picks the object up without a name convention. build/src/md_MAIN_011/md_MAIN_011.o: CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker # link (the .ld pulls in the .o by path) + objcopy to the raw PS-X EXE image. $(OUT): $(OBJS) $(ASSET_OBJS) $(LD_SCRIPT) @set -e mkdir -p $(dir $@) # PsyQ SDK library integrations are EXE-only (libgs/libgte/sound/apicard are # SLUS_007.26's layout). A second binary (BINARY != main) skips this block and links # its own stubs. NB: ifeq/endif are make directives (column 0, no tab), resolved at # parse time; with .ONESHELL the included recipe lines still run as one shell. ifeq ($(BINARY),main) ifeq ($(NO_SDK),) # Wire in the real libcd objects (after the build objects exist — the externals discovery # trial-links the whole image). Idempotent: re-running re-derives the externals only. if [ -d "$(LIBCD_ELF)" ]; then $(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) --yaml $(main_SPLAT_YAML) $(LIBCD_ELF) $(LD_SCRIPT) $(LIBCD_OBJDIR) $(LIBCD_SYMS) libcd1,libcd2 else echo " (no $(LIBCD_ELF) — libcd region stays asm stubs; run tools/psyq_build_libs.sh LIBCD)" fi if [ -d "$(LIBGS_ELF)" ]; then $(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) --yaml $(main_SPLAT_YAML) $(LIBGS_ELF) $(LD_SCRIPT) $(LIBGS_OBJDIR) $(LIBGS_SYMS) libgs1,libgs2,libgs3,libgs4,libgs5,libgs6,libgs7,libgs8 0x8005080C 0x80057928 else echo " (no $(LIBGS_ELF) — libgs region stays asm stubs; run tools/make_libgs.sh)" fi if [ -d "$(LIBETC_ELF)" ]; then $(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) --yaml $(main_SPLAT_YAML) $(LIBETC_ELF) $(LD_SCRIPT) $(LIBETC_OBJDIR) $(LIBETC_SYMS) libetc else echo " (no $(LIBETC_ELF) — libetc region stays asm stubs; run tools/psyq_build_libs.sh LIBETC)" fi if [ -d "$(LIBGPU_ELF)" ]; then $(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) --yaml $(main_SPLAT_YAML) $(LIBGPU_ELF) $(LD_SCRIPT) $(LIBGPU_OBJDIR) $(LIBGPU_SYMS) libgpu,libgpu2 else echo " (no $(LIBGPU_ELF) — libgpu region stays asm stubs; run tools/psyq_build_libs.sh LIBGPU)" fi if [ -d "$(LIBMCRD_ELF)" ]; then $(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) --yaml $(main_SPLAT_YAML) $(LIBMCRD_ELF) $(LD_SCRIPT) $(LIBMCRD_OBJDIR) $(LIBMCRD_SYMS) libmcrd1,libmcrd2 else echo " (no $(LIBMCRD_ELF) — libmcrd region stays asm stubs; run tools/psyq_build_libs.sh LIBMCRD)" fi if [ -d "$(LIBC2_ELF)" ]; then $(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) --yaml $(main_SPLAT_YAML) $(LIBC2_ELF) $(LD_SCRIPT) $(LIBC2_OBJDIR) $(LIBC2_SYMS) libc2_1,libc2_2 else echo " (no $(LIBC2_ELF) — libc2 region stays asm stubs; run tools/psyq_build_libs.sh LIBC2)" fi if [ -d "$(LIBGTE_ELF)" ]; then $(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) --yaml $(main_SPLAT_YAML) $(LIBGTE_ELF) $(LD_SCRIPT) $(LIBGTE_OBJDIR) $(LIBGTE_SYMS) $(LIBGTE_STUBS) 0x8004787C 0x80053AF8 else echo " (no $(LIBGTE_ELF) — libgte region stays asm stubs; run tools/psyq_build_libs.sh LIBGTE)" fi if [ -d "$(SND_ELF)" ]; then $(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) --yaml $(main_SPLAT_YAML) $(SND_ELF) $(LD_SCRIPT) $(SND_OBJDIR) $(SND_SYMS) $(SND_STUBS) 0x8003A444 0x8004239C else echo " (no $(SND_ELF) — sound region stays asm stubs; run tools/psyq_build_libs.sh LIBSPU LIBSND + tools/make_snd_used.py)" fi if [ -d "$(APICARD_ELF)" ]; then $(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) --yaml $(main_SPLAT_YAML) $(APICARD_ELF) $(LD_SCRIPT) $(APICARD_OBJDIR) $(APICARD_SYMS) $(APICARD_STUBS) 0x80061F38 0x80062888 else echo " (no $(APICARD_ELF) — apicard region stays asm stubs; run tools/psyq_build_libs.sh LIBCARD + the lib421 ELF step in docs/SETUP.md + tools/make_apicard_used.py)" fi if [ -d "$(LIBAPI42_ELF)" ]; then $(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) --yaml $(main_SPLAT_YAML) $(LIBAPI42_ELF) $(LD_SCRIPT) $(LIBAPI42_OBJDIR) $(LIBAPI42_SYMS) $(LIBAPI42_STUBS) 0x8005CE18 0x8005E188 else echo " (no $(LIBAPI42_ELF) — libapi band blocks stay asm stubs; convert tools/psyq/lib421/LIBAPI.LIB per docs/SETUP.md)" fi if [ -d "$(LIBPAD_ELF)" ]; then $(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) --yaml $(main_SPLAT_YAML) $(LIBPAD_ELF) $(LD_SCRIPT) $(LIBPAD_OBJDIR) $(LIBPAD_SYMS) $(LIBPAD_STUBS) 0x8005D0D8 0x8005FC68 else echo " (no $(LIBPAD_ELF) — libpad band blocks stay asm stubs; convert tools/psyq/lib421/LIBPAD.LIB per docs/SETUP.md)" fi else echo " NO_SDK=1: the PsyQ object integrations are SKIPPED — main links its INCLUDE_ASM stub tiles (the fresh-clone leg)" endif endif # The externals fragments belong to the SDK-object link only: under NO_SDK the stale files left by a # previous WITH build must not be picked up (they would defsym names the stub tiles already carry). SYMS="" if [ -z "$(NO_SDK)" ]; then [ -f "$(LIBCD_SYMS)" ] && SYMS="-T $(LIBCD_SYMS)"; [ -f "$(LIBGS_SYMS)" ] && SYMS="$$SYMS -T $(LIBGS_SYMS)"; [ -f "$(LIBETC_SYMS)" ] && SYMS="$$SYMS -T $(LIBETC_SYMS)"; [ -f "$(LIBGPU_SYMS)" ] && SYMS="$$SYMS -T $(LIBGPU_SYMS)"; [ -f "$(LIBMCRD_SYMS)" ] && SYMS="$$SYMS -T $(LIBMCRD_SYMS)"; [ -f "$(LIBC2_SYMS)" ] && SYMS="$$SYMS -T $(LIBC2_SYMS)"; [ -f "$(LIBGTE_SYMS)" ] && SYMS="$$SYMS -T $(LIBGTE_SYMS)"; [ -f "$(SND_SYMS)" ] && SYMS="$$SYMS -T $(SND_SYMS)"; [ -f "$(APICARD_SYMS)" ] && SYMS="$$SYMS -T $(APICARD_SYMS)"; [ -f "$(LIBAPI42_SYMS)" ] && SYMS="$$SYMS -T $(LIBAPI42_SYMS)"; [ -f "$(LIBPAD_SYMS)" ] && SYMS="$$SYMS -T $(LIBPAD_SYMS)" true fi echo " LD $(ELF)" $(LD) -T $(LD_SCRIPT) -T $(UNDEF_SYMS) -T $(UNDEF_FUNCS) $$SYMS --no-check-sections -Map $(MAPFILE) -o $(ELF) echo " OBJCOPY $@" $(OBJCOPY) -O binary $(ELF) $@ # Trim the linker's end-of-segment 4-align pad: splat's .ld does `. = ALIGN(., 4)` after the # data section, over-emitting up to 3 zero bytes when the payload size isn't 4-aligned (most # overlays; the EXE + resident are 4-aligned so this never fires for them). Shrink-ONLY and # capped at 3 bytes -> it can never hide a real shortfall (build < target fails the SHA) nor # extend the image. The target's true byte length is the matched payload $(EXE). tsz=$$(stat -c%s "$(EXE)"); osz=$$(stat -c%s "$@"); d=$$((osz - tsz)) if [ "$$d" -gt 0 ] && [ "$$d" -le 3 ]; then truncate -s "$$tsz" "$@"; echo " TRIM $@ (-$$d B linker end-align pad)"; fi # build = produce $(OUT) and verify its SHA1 (check pulls in $(OUT)). build: check # extract-all / build-all / check-all (Phase 13; PARALLELIZED Phase 26): build + SHA1-check EVERY # binary in $(BINARIES) -> a single fleet PASS/FAIL. Recursion ($(MAKE) BINARY={}) RE-PARSES the # Makefile per binary so each gets its correctly-pruned OBJS (a `foreach` can't — the OBJS glob is # parse-time, keyed on $(BINARY)). PARALLEL across binaries via `xargs -P$(JOBS)`: every binary's # outputs are per-binary-disjoint (asm/, build/, build/{src,asm}/) and include/ is # READ-ONLY during a build, so concurrent builds never race. The one shared WRITE is the 4 generated # include/*.inc macros at EXTRACT time (identical content per binary) -> `extract-all` seeds them once # via main (serial) before fanning out. Proven 136/136 byte-identical, ~10x faster (Phase 26: the # serial R22 ~9m -> the parallel R22 ~1m). CLEAN fleet proof (R22 — clean rebuild): # make clean && make extract-all && make check-all JOBS ?= 16 # parallel binary builds/extracts (override: `make check-all JOBS=32`) # extract-all: splat-split every binary. Seed `main` FIRST (serial) so the shared include/*.inc macros # (+ build/psyq) exist before the parallel fan-out; then extract the rest in parallel. extract-all: @mkdir -p .run; : > .run/extract-all.txt # P33 B1: the payloads come from the user's disc — regenerate/verify extracted/ ONCE, serially, # before anything reads it (a ~5 s no-op when the tree already matches the committed manifest). $(MAKE) --no-print-directory disc-extract # Under the global `-e` a failing main extract now aborts here. It previously did NOT: its status # was swallowed by .ONESHELL, and the closing `! grep -q` then passed regardless — a seed failure # could sail through as green. $(MAKE) --no-print-directory extract BINARY=main echo "$(filter-out main,$(BINARIES))" | tr ' ' '\n' | xargs -P$(JOBS) -I{} sh -c \ '$(MAKE) --no-print-directory extract BINARY={} >.run/extract.{}.log 2>&1 && echo "[ OK ] {}" || echo "[EXTRACT FAIL] {}"' \ | tee .run/extract-all.txt pass=$$(grep -c "^\[ OK \]" .run/extract-all.txt || true) fail=$$(grep -c "^\[EXTRACT FAIL\]" .run/extract-all.txt || true) want=$$(( $(words $(BINARIES)) - 1 )) echo "extract-all: $$pass extracted, $$fail failed of $$want (+ main, serial)" # Assert COVERAGE (pass == N-1), not the absence of a marker (R32) — `! grep -q "EXTRACT FAIL"` # was a vacuous pass on an empty pipeline. if [ "$$pass" -ne "$$want" ]; then echo "[FAIL] extract-all: expected $$want extracted, got $$pass (failed=$$fail)"; exit 1 fi check-all: @mkdir -p .run; : > .run/check-all.txt echo "$(BINARIES)" | tr ' ' '\n' | xargs -P$(JOBS) -I{} sh -c \ '$(MAKE) --no-print-directory check BINARY={} >.run/check.{}.log 2>&1 && echo "[ OK ] {}" || { echo "[FAIL] {}"; tail -3 .run/check.{}.log >&2; }' \ | tee .run/check-all.txt # `|| true`: grep -c EXITS 1 when the count is 0, and under `-e` a failing command substitution # aborts the assignment — so the bare form would make check-all FAIL exactly when nothing failed. pass=$$(grep -c "^\[ OK \]" .run/check-all.txt || true) fail=$$(grep -c "^\[FAIL\]" .run/check-all.txt || true) want=$(words $(BINARIES)) echo "check-all: $$pass passed, $$fail failed of $$want" # Assert COVERAGE (pass == N), not merely the absence of a failure marker (R32). `fail -eq 0` # was a VACUOUS PASS: if the xargs pipeline emitted nothing at all, pass=0 fail=0 -> [ 0 -eq 0 ] # -> green while checking NOTHING. The byte-gate is a correctness oracle with a null coverage # dimension; this line is the coverage half. if [ "$$pass" -ne "$$want" ]; then echo "[FAIL] check-all: expected $$want passing, got $$pass (failed=$$fail)"; exit 1 fi build-all: check-all # check: SHA1 of the build vs the committed original hash. The definition of "build OK". check: $(OUT) @got=$$(sha1sum $(OUT) | cut -d' ' -f1) want=$$(cut -d' ' -f1 $(CHECK_SHA) 2>/dev/null) if [ -z "$$want" ]; then echo "[FAIL] $(CHECK_SHA) missing or empty"; exit 1; fi if [ "$$got" = "$$want" ]; then echo "[ OK ] $(OUT)" echo " sha1 $$got == $(CHECK_SHA) (BYTE-IDENTICAL)" else echo "[FAIL] $(OUT)" echo " got $$got" echo " want $$want" exit 1 fi # expected: snapshot a SHA1-verified build into expected/build/ as the asm-differ # baseline for Phase 6 (asm-differ diffs build/ vs expected/build/). expected: build @set -e mkdir -p expected/build # Per-binary-safe (Phase 10): refresh ONLY the active binary's image dir, then merge-copy # build/ into the shared expected/ mirror. cp MERGES (never deletes), so `make expected # BINARY=X` preserves binary Y's baseline even when Y isn't currently in build/ (the old # `rm -rf expected/build` wiped every sibling). asm-differ reads expected/$(OUT) (image mode) # + expected/build/ (object mode); both resolve under the merged mirror. rm -rf expected/$(OUT_DIR) cp -r build/. expected/build/ echo "expected: baseline refreshed for binary=$(BINARY) -> expected/build/ (siblings preserved)" # clean: remove ALL regenerable outputs (build/ + the splat tree) so a config change # is followed by a stale-free `make clean && make extract && make build` (H3). clean: @# `clean` is FLEET-WIDE and takes no BINARY scope (cookbook §445). extract/build/check all @# honour BINARY=, so `make clean BINARY=` reads as scoped and is not: it deletes asm/ for @# all 213 binaries, and the failure then surfaces somewhere else entirely — `corpus refused: @# N stub(s) have NO .s on disk`, or `.ld missing`, or a concurrent agent's gate refusing @# for a reason that is a fact about your shell. Say so rather than silently ignoring the @# variable (R43). Recovery is `make extract-all`, NOT `make extract BINARY=`. @if [ -n "$(filter-out main,$(BINARY))" ] || [ "$(origin BINARY)" = "command line" ]; then \ echo "clean: NOTE — BINARY=$(BINARY) is IGNORED here; clean is fleet-wide (cookbook §445)."; \ echo "clean: this removes asm/ for ALL binaries. Recover with 'make extract-all'."; \ fi @rm -rf build expected asm assets undefined_syms_auto.txt undefined_funcs_auto.txt @# P33 B1: the four splat preset headers (include/include_asm.h, macro.inc, labels.inc, gte_macros.inc) @# are TRACKED now — generic splat presets, identical for every binary and ROM-free, so a compile-only @# CI can assemble without a disc. `make extract` rewrites them only if splat's presets change (a @# visible diff), so clean no longer deletes them. @echo "clean: removed build/, expected/, and the regenerated splat tree (asm/, assets/, undefined_*_auto.txt)."