Files
BFM-decomp/Makefile
T
Drew T 02f060f607 feat(phase-31): S79 #5 — the libpad 4.2.1 + libapi 4.2 band and the apicard region LINKED from real objects: 13 stubs + 4 TUs + the reorder island gone; main 16 stubs, fleet 38
800c3 (0x8005CE18-0x8005FC68, one contiguous run of 33 interleaved Sony objects) is now four
stub rows — libapi1 (21 BIOS trampolines + COUNTER), libpad1 (PADENTRY + PADMAIN 760), libapi2
(L02/L03), libpad2 (PADCMD PADIF PADPORTD PADSEQD WAITRC2) — fed by two WINDOWED psyq_integrate
calls from the raw .run/obj42/{libapi42,libpad421} dirs (integrate tiles each stub with one
library; every boundary checked against .text SECTION sizes). The apicard region's three
"game code" rows were libapi 4.2's C objects to the byte: 800c2 = FIRST.o (firstfile + the
"no jump table wall" stub func_80062144), 800c2_2 = PAD.o, 800c2_3 = PATCH.o + CHCLRPAD.o ->
apicard5/6/7; make_apicard_used.py sources libapi from 4.2 (the EXE's real libapi; libcard
stays 4.0) into .run/obj42/apicard_used, 26 objects / 7 blocks, no game code left in
0x80061F38-0x80062888. src/800c3.c (129 hand-matched "C", 62 verbatim bodies, 19 stubs incl.
the four §332 %lo-in-a-delay-slot "walls"), src/800c2.c, src/800c2_2.c, src/800c2_3.c removed;
REORDER_TUS is empty (mechanism kept). Cookbook §490.

Two stale instruments fixed: exclude_audit let a pinned WALL outrank LINKED (PopMatrix/
PushMatrix had sat as walls since S68 while living in libgte3, linked since Phase 8) — LINKED
dominates now, config/wave_exclude.txt 13 -> 3; frontier_classify carried a hard-coded 49-name
LINKED set (R51) and reported 337 "stubs" — derived from the Makefile now.

Verified: main 143dbb89f34491258bbc27810d0a12ec8b43a8dd WITH all SDK dirs and WITHOUT them from
a fresh extract; make tools-health OK; R22 fleet extract-all 212/212 + check-all 213/213.
Metrics: main REAL 839->773, LINKED 1,150->1,256, VERBATIM 29->3, stubs 29->16, byte-identical
2,075/2,091 = 99.2%; game-code weighted 93.3% (38,748/41,534), remainder 2,786 = the open-stub
sum; fleet stubs 51->38 (frontier_classify: 39 rows incl. the data word). Verbatim manifest
33 -> 6. Docs: worklist rows + "S79 task #5", SETUP (fresh-clone obj42 commands, Makefile
blocks, exclude_audit), decision-log "S79 addendum 2", accelerators "S79 (2)", CURRENT_PHASE
S79 FINAL refreshed (census, metrics, the task #6 brief).
2026-09-04 17:56:31 -06:00

1033 lines
68 KiB
Makefile
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <alias>_* 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 <bin> path convention (config/
# splat.<bin>.yaml, build/<bin>/, config/check.<bin>.sha, config/symbols.<bin>.txt,
# .run/sig.<bin>.jsonl) is documented now but first INSTANTIATED by Phase 10's second
# binary. Select with `make build BINARY=<alias>`; 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-<ov> 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 check-env extract build check expected clean report sig-refresh sig-overlays sig-resident build-all check-all audit-corpus audit-cdecl audit-binaries audit-text-sources audit-digest audit-frontier tools-health
# -----------------------------------------------------------------------------
help:
@echo "BFM-decomp — make targets:"
echo " make check-env Phase-4 toolchain preflight (the only live target)"
echo " make extract [Phase 5] splat split -> asm/ + linker scripts"
echo " make build [Phase 5] full pipeline -> build/us/SLUS_007.26 (+ SHA1 check)"
echo " make check [Phase 5] standalone SHA1 verification"
echo " make expected [Phase 5] snapshot build/us -> expected/ (asm-differ baseline)"
echo " make clean [Phase 5] remove build output"
echo " make report [Phase 7] regenerate docs/ progress+difficulty+duplicate digests"
echo " make sig-refresh [Phase 7] regenerate .run/sig.*.jsonl from Ghidra (MCP must be stopped)"
# -----------------------------------------------------------------------------
# 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 := $(HOME)/bfm-decomp/ghidra
# 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
# 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 (`'<NUL>'` 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.
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
$(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
$(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
# 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)
$(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
# 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_<ADDR>
# 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_<SCxx>_<nnn>.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 <ov>_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). `<alias>:<payload>` 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.<alias>.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<hi) printf "0x%08X\n", a }' \
| sort -u > .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).
sig-main:
$(VENV_PY) tools/corpus.py main --seed-ends > .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 stubs (splat-true lengths) -> .run/sig.main.jsonl"
# 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.<alias>.txt and .run/sig.<alias>.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
# -----------------------------------------------------------------------------
# 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
# 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) committed EXE hash == EXPECTED_EXE_SHA1 (reused constant; fresh-clone-safe)
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 "[FAIL] $(EXE) missing (committed retail EXE)"; fail=1
fi
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/<bin>/ and nests its sources under <bin>/.)
# 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/<seg>/*.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/<bin>/ + src/<bin>/. 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)
C_SRCS := $(shell find $(SRC_DIR) -name '*.c' $(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)
# 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/<alias>/*.bin and references build/assets/<alias>/*.o in the .ld; wrap each raw .bin into a
# linkable object (bytes verbatim in .data). Per-binary: asset_path is scoped to assets/<alias> 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)
# extract: splat split -> asm/, the linker script, include/ macros, undefined_*_auto.txt.
extract:
@mkdir -p $(OUT_DIR)
# 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). <bin>_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/<seg>/<fn>.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 <ov>_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 <ov>_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
# <ov>_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)
# 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
endif
SYMS=""; [ -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)"
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/<bin>, build/<bin>, build/{src,asm}/<bin>) 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
# 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/<obj> vs expected/build/<obj>).
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/<obj> (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=<x>` 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 `<bin>.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=<x>`.
@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
@rm -f include/include_asm.h include/macro.inc include/labels.inc include/gte_macros.inc
@echo "clean: removed build/, expected/, and the regenerated splat tree (asm/, assets/, include macros, undefined_*_auto.txt)."