mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-27 14:02:04 -04:00
af2f40d153
R22 CLEAN-FLEET: make clean -> extract 136 -> build 136 -> check-all = 136 PASSED, 0 FAILED.
make audit-corpus: 0 PHANTOM + 0 TRUNCATED (was 193).
=== A4: a CORPUS defect the byte-gate could never have caught ===
config/symbols.us.txt:981 declared `listCdBuffer = 0x80180000` — a correct Phase-3 name for MAIN's
LIST.CD RAM buffer. But that address is OUTSIDE main's image and INSIDE the overlay slot, and every
overlay's splat config stacks symbols.us.txt. High RAM is REUSED: an address that is a buffer to main
is live CODE to an overlay. So splat saw a symbol boundary mid-code and, across 97 of 134 overlays:
* CUT 97 REAL FUNCTIONS IN HALF (a head ending on a `lui`, no return), and
* INVENTED 96 PHANTOM ONES (a tail beginning by reading the assembler temp $at).
193 slices NOBODY COULD EVER MATCH — not "hard", not "a compiler wall": unmatchable by construction.
They sat in the harvest queue as ordinary work, so agents would burn on them forever and the failures
would be filed as intrinsic compiler residuals.
The phantom listCdBuffer.s in ov_SC01_005 literally begins:
lw $ra, 0x10($sp) / addiu $sp, $sp, 0x18 / jr $ra
splat cut a function immediately before its EPILOGUE and called the epilogue a function.
AND IT HAD ALREADY CONTAMINATED REAL WORK: in ov_SC03_031 the cut landed where the epilogue was
exactly `jr $ra; nop`, so the Phase-26 x134 sweep innocently BANKED the phantom as
`void listCdBuffer(void) {}` — byte-correct, gate-green, entirely fictitious — while leaving
func_8017FFC4 permanently unmatchable. Removed.
WHY NO GATE CAUGHT IT, AND WHY THAT IS THE POINT: INCLUDE_ASM pastes the two .s halves back VERBATIM
in original order, so the image is byte-identical either way. The byte-gate was green the whole time
and always would have been. It is a perfect CORRECTNESS oracle and a NULL COVERAGE oracle. No
assertion added INSIDE it could ever have found this. What found it was a SECOND, INDEPENDENT oracle:
tools/sig_image.py derives boundaries from the ORIGINAL bytes without splat, and DISAGREED with the
corpus (58,524/58,621 agreement with spimdisasm; correct on all 97 disagreements).
=> When one oracle is structurally blind to a class of error, the answer is not a better assertion
inside it. It is a SECOND ORACLE THAT CAN DISAGREE WITH IT. (`make audit-corpus` is now that.)
THE RULE (the mirror of R13/R15, never written down): a symbol whose address falls inside ANOTHER
binary's vram window must never enter that binary's symbol stack.
FIX: config/symbols.us.ram.txt — main-scoped symbols outside main's image — stacked ONLY by
config/splat.us.exe.yaml. Main keeps the name it needs (10 %hi / 11 %lo refs; 143dbb89 byte-identical);
the overlays never see it. Exactly one symbol was in scope fleet-wide; the resident window was clean.
AND A REAL FUNCTION THE ACCIDENT WAS HIDING: in ov_SC01_084 / ov_SC02_041 / ov_SC03_094 / ov_SC06_008
there IS a genuine function at 0x80180000 (111 / 35 / 28 / 74 ins), reachable ONLY via a fn-pointer
table (.word func_80180000) and never by `jal` — so splat cannot find it and needs the boundary
DECLARED. listCdBuffer had been supplying it by luck. Now declared honestly, per-overlay, in
config/symbols.<ov>.txt — exactly where R13/R15 says an overlay-scoped symbol belongs.
=== A5: the closeness oracle every crack agent trusts was lying on 155 functions ===
masked_diff._reloc_kind() knew 26/HI16/LO16. An over-approximating sweep of every reloc objdump emits
across all 3,367 build objects found FOUR: R_MIPS_26, HI16, LO16 — and R_MIPS_PC16 (211). PC16 fell
through to a FULL-WORD compare, but the object holds an UNRESOLVED PLACEHOLDER in the branch
displacement, so that compare can NEVER succeed.
DECISIVE TEST (derived from the invariant, not from reading the regex): INCLUDE_ASM pastes the
ORIGINAL asm, so for every stub diff_object_s() MUST be 0. Measured, coverage-asserted:
2,741 functions scored — old mask: 150 LIES; PC16 masked: 4 LIES.
(The 4 survivors are the separate length-delta defect.) A phantom non-zero sends an agent to grind at
a wall that is not there, and the wasted attempt is then booked as a MATCHING failure, feeding
reserved_walls() and PERMANENTLY BLACKLISTING a function that was never broken.
=== NEW FINDING (found by cutting the R22 corner): a STALE OBJECT CAN PRODUCE A FALSE PASS ===
`.o <- .s` is not a dependency make can see: assembly arrives via INCLUDE_ASM, expanded to a `.include`
consumed by maspsx/as AFTER cpp, while -MMD tracks headers only. Re-extract, build incrementally, and
make links a STALE object. This is not merely slow — INCLUDE_ASM pastes the ORIGINAL bytes, so a stale
object still yields the original image: SHA1 GOES GREEN while the split just changed is never exercised.
A broken config change can be "verified" by an incremental build. Live proof: 8 of 136 binaries linked
stale objects here; they failed LOUDLY ONLY BY LUCK (the dead symbol was an undefined reference) — a
merely-different-but-valid split would have gone green on all 136.
R22/H3 already legislate this, and I broke them. But a rule that needs a human to remember it is not a
gate. FIX: `extract` now invalidates the objects that include what it just rewrote (main's are top-level,
so -maxdepth 1 — verified it cannot clobber the other 1,605 objects). Structural, not advisory.
R14 self-catch, recorded: my first A5 test passed `fn=` to diff_object_s(), which takes two args; the
TypeError was swallowed by my own `except Exception: continue` and it reported 0 scored / 0 lies. I
wrote the exact bug I was auditing, inside the test for it. Caught only because 0 looked wrong. The
test now asserts its own coverage.
631 lines
37 KiB
Makefile
631 lines
37 KiB
Makefile
# 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:
|
||
.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 build-all check-all
|
||
|
||
# -----------------------------------------------------------------------------
|
||
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. Currently RED:
|
||
# 193 unmatchable slices from one bad symbol line. Wire into `report` once A4 lands it green.
|
||
audit-corpus:
|
||
$(VENV_PY) tools/corpus.py --all --audit
|
||
|
||
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
|
||
# 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
|
||
sig-overlays:
|
||
@n=0
|
||
for f in $$(find extracted/retail -path '*SC*.CD.dir/FILE_*.dir/0.4.dec' | sort); do
|
||
nm=$$(echo "$$f" | sed -E 's|.*/(SC[0-9]+)\.CD\.dir/FILE_([0-9]+)\.dir.*|ov_\1_\2|')
|
||
$(VENV_PY) tools/sig_image.py --image "$$f" --vram-base $(OVERLAY_VRAM) --bootstrap --name "$$nm" >/dev/null
|
||
n=$$((n+1))
|
||
done
|
||
echo "sig-overlays: signed $$n overlays -> .run/sig.ov_*.jsonl"
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# 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:
|
||
@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
|
||
|
||
# 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
|
||
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) | $(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
|
||
|
||
# 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
|
||
$(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 "[EXTRACT FAIL] {}"' \
|
||
| tee .run/extract-all.txt
|
||
! grep -q "EXTRACT FAIL" .run/extract-all.txt
|
||
|
||
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
|
||
pass=$$(grep -c "^\[ OK \]" .run/check-all.txt); fail=$$(grep -c "^\[FAIL\]" .run/check-all.txt)
|
||
echo "check-all: $$pass passed, $$fail failed of $(words $(BINARIES))"
|
||
[ "$$fail" -eq 0 ]
|
||
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)."
|