Files
BFM-decomp/Makefile
T
Drew T 4570a5854b feat(phase-30): cookbook-index — a SYMPTOM-keyed index (226 sections, derived + coverage-asserted, in tools-health)
Wave-1 measured the tax: three agents each reported a 'NEW idiom' that was ALREADY documented —
the asm-label alias (line ~2516, same 'address-of perturbs regalloc' mechanism) and the void->s32
non-neutrality (§41d, Phase 26; the agents cited the very entry §41d corrects). They consulted the
cookbook as instructed and could not FIND them. 716 KB / 226 sections with no index = a
discoverability failure, and every wave re-paying for prior waves' findings is the inverse of R16.

docs/cookbook-index.md maps SYMPTOM (what you see in the diff) -> sections, 14 buckets, a section
listed under every symptom it addresses. Derived by tools/cookbook_index.py (R33 — cannot drift),
--check wired into tools-health.

R32 on my own tool: the first regex required an em-dash separator and silently dropped 50 sections
— including §1 (idiom catalog), §2, §5a (cross-jump, cited by an agent today). An index missing its
most-cited entries turns 'I could not find it' into 'it is not there'. Now asserts extracted ==
candidate '§' headers and hard-exits on a gap.
2026-07-30 20:12:07 -06:00

754 lines
46 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
.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
BINARIES := main resident $(OVERLAY_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 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.
audit-cdecl:
$(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
# 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 audit-corpus
$(MAKE) --no-print-directory audit-cdecl
$(MAKE) --no-print-directory audit-binaries
$(MAKE) --no-print-directory report BINARY=main
# 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
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:
@n=0
for job in $(OVERLAY_SIG_JOBS); do
alias=$${job%%:*}; f=$${job#*:}
[ -f "$$f" ] || { echo "sig-overlays: WARN no payload for $$alias ($$f)"; continue; }
$(VENV_PY) tools/sig_image.py --image "$$f" --vram-base $(OVERLAY_VRAM) --bootstrap --name "$$alias" >/dev/null
n=$$((n+1))
done
echo "sig-overlays: signed $$n overlays -> .run/sig.ov_*.jsonl (of $(words $(OVERLAY_BINARIES)) onboarded)"
# sig-resident (Phase-27 T10): sign the resident flat blob with sig_image — the Ghidra-FREE,
# byte-DERIVED signer — so `make audit-corpus` gains a SECOND, INDEPENDENT boundary oracle for the
# resident (R34; corpus.sig_is_independent now trusts it). sig_image already supports the resident
# case; it was simply never invoked. Overwrites .run/sig.resident.jsonl (was a Ghidra sig); h_exact
# is raw-byte SHA1 so it is format-independent — weighted_metrics is unaffected. R23-free.
sig-resident:
$(VENV_PY) tools/sig_image.py --image $(resident_EXE) --vram-base $(resident_VRAM_BASE) --bootstrap --name resident
echo "sig-resident: signed the resident -> .run/sig.resident.jsonl (byte-derived, second-oracle-ready)"
# -----------------------------------------------------------------------------
# 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): 31 libgs objects in 6 contiguous blocks linked in place
# of the libgs1..libgs6 block stubs (the 5 non-libgs gaps stay gsgapN asm stubs). 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 0x80051804 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): EXT+PRIM only — SYS.o EXCLUDED (scattered-.bss, cookbook §9.1, GS_001 class; stays a
# stub in 800c). Curated dir libgpu_used = {EXT,PRIM}; regenerate: tools/psyq_build_libs.sh LIBGPU then
# `mkdir -p .run/obj40/libgpu_used && cp .run/obj40/libgpu/{EXT,PRIM}.o .run/obj40/libgpu_used/`.
LIBGPU_ELF := .run/obj40/libgpu_used
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 (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
# Combined libspu+libsnd sound region (Phase 8): the two SDK sound libs interleave in 0x3A444..0x4239C
# so they link as one 60-object region (snd1..snd9). Curated dir .run/obj40/snd_used built by
# tools/make_snd_used.py (4 addresses excluded as scattered-.bss/false-positive stubs). 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
# Combined libapi+libcard 800c2 region (Phase 8): 22 objects in 4 blocks (apicard1..4). Curated dir
# .run/obj40/apicard_used (tools/make_apicard_used.py). Window 0x61F38..0x62888. (libapi's ~22 objects
# in the 800c3 region are DEFERRED — lowest value.)
APICARD_ELF := .run/obj40/apicard_used
APICARD_OBJDIR := build/psyq/apicard
APICARD_SYMS := build/psyq/apicard_externals.ld
APICARD_STUBS := apicard1,apicard2,apicard3,apicard4
# 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).
$(PYTHON) tools/ld_interleave.py --front 53198.data.o --tail 6324C.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.
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.
build/src/%.o: src/%.c
@mkdir -p $(dir $@)
@echo " CC $@"
@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 $@
# 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). All share src/shared/func_80144B9C.h.
WHALE_O0B_OBJS := $(patsubst src/%.c,build/src/%.o,$(wildcard src/ov_*/ov_*_o0b.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) — 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
# 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) $(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) $(LIBGS_ELF) $(LD_SCRIPT) $(LIBGS_OBJDIR) $(LIBGS_SYMS) libgs1,libgs2,libgs3,libgs4,libgs5,libgs6 0x80051804 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) $(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) $(LIBGPU_ELF) $(LD_SCRIPT) $(LIBGPU_OBJDIR) $(LIBGPU_SYMS) libgpu
else
echo " (no $(LIBGPU_ELF) — libgpu region stays asm stubs; run tools/psyq_build_libs.sh LIBGPU + curate libgpu_used)"
fi
if [ -d "$(LIBMCRD_ELF)" ]; then
$(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) $(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) $(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) $(LIBGTE_ELF) $(LD_SCRIPT) $(LIBGTE_OBJDIR) $(LIBGTE_SYMS) $(LIBGTE_STUBS) 0x8004787C 0x80051804
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) $(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) $(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 LIBAPI LIBCARD + tools/make_apicard_used.py)"
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)"
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:
@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)."