mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
7a969d1c61
The single-object module binaries could not be carved at all: o0_subsplit planned correctly and then jr_isolate_all refused with 'unaddressable content'. That blocked 9 of the 12 remaining -O0-in-an--O2-TU functions fleet-wide, including a byte-correct 345-instruction draft with nowhere to go. THREE ROOT CAUSES behind the refusal, all fixed here: * overlay_src_split.load_ov_syms: an interior YAML comment terminated the symbol-file list. md_MAIN_003's yaml annotates the list body, so only symbols.us.txt loaded and D_800D3200 resolved to None -> refusal. * jr_isolate_all._partition: a trailing content chunk (the verbatim-asm pair after the last addressable anchor) now attaches to the LAST region when every symbol it defines resolves at/after the last cut, instead of hard-refusing. * _file_scope_decls: bare tag forward decls (struct S_D2394;) exempted from the dedupe refusal; plus addr_of's D_<hex8> fallback. THEN A LINK FAILURE THE CARVE CAUSED, worth knowing: spimdisasm migrates rodata referenced by exactly one function into that function's .s ONLY within the same subseg. The carve moved func_800D30D0 into the jr subseg while the .rodata island stayed on md_MAIN_003, so three dlabel string blocks were SILENTLY DROPPED -> undefined reference to D_800CEE58/D_800CEE80. Adding INCLUDE_RODATA does not resurrect them (splat marks them migrated segment-wide and emits nothing). The fix is to rename the .rodata subseg to the jr object, where every island emitter lives. The regenerated func_800D30D0.s came back byte-identical to the pre-carve .s. Makefile: the -O0 glob widened to src/md_*/md_*_o0?.c. Without it the region file compiles -O2 -- byte-neutral while stub-only, but every -O0 draft banked into it would mystery-fail the gate (§362's trap class). This is why the Makefile and tool hunks MUST land with the carve: a fresh clone would otherwise lose the -O0 flag. VERIFIED INDEPENDENTLY of the agent that did it: sha1 dd1b32ecf1103c6f7cf1943d25546a3046e17b14 == config/check.md_MAIN_003.sha, from a rebuild I ran myself; md_MAIN_003 13 -> 12 stubs; func_800D0D6C absent from corpus.stubs. interleave_check's DRIFT on this binary is PRE-EXISTING (identical on a clean tree, verified before any change) -- md_MAIN_003 has no _JTBL_INTERLEAVE block and must not get one; forcing ALIGNED moves the leading rodata island after .text and shifts every address by 0xD8. config/overlays.mk untouched (R59/R60). 8 of the 9 md_MAIN_003 -O0 stubs remain: they need drafts and follow-on carves.
907 lines
57 KiB
Makefile
907 lines
57 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:
|
||
# FAIL-CLOSED BY DEFAULT (Phase-27 T2). Without `-e`, .ONESHELL sends the WHOLE recipe to one
|
||
# `bash -c`, so a recipe's exit status is its LAST command's only — every earlier failure is
|
||
# silently swallowed. That made `report`'s lint_symbol_refs / progress --audit / difficulty /
|
||
# dup_report non-gates (dedup-check "worked" purely by being last), i.e. exactly the defect the
|
||
# 26-A audit exists to kill: a loud failure nobody counts is as invisible as a silent one (R32).
|
||
# `-e` makes every recipe line load-bearing. Deliberate opt-out: `check-env` (see its recipe).
|
||
.SHELLFLAGS := -ec
|
||
# DELETE A FAILED TARGET (Phase-30 S29). `as` reads a pipeline stream, so when an upstream stage
|
||
# dies mid-stream (e.g. jtbl_rodata_pads' fail-loud table-count guard) `as` has already written a
|
||
# TRUNCATED .o. make reports the error correctly — and then leaves the corpse on disk, newer than
|
||
# its .c. The NEXT build considers it up to date and LINKS it, turning a loud, attributable compile
|
||
# error into `undefined reference to $L105` / `func_8013C938` one build later. That is precisely how
|
||
# the `JR-PAIR-IN-ONE-O0-OBJECT` "wall" was manufactured (§132). Same family as the §42b/§130
|
||
# stale-object traps: an artifact that outlives the command that failed to produce it.
|
||
.DELETE_ON_ERROR:
|
||
.DEFAULT_GOAL := help
|
||
|
||
# --- paths & tooling ---------------------------------------------------------
|
||
PYTHON := python3
|
||
VENV := .venv
|
||
VENV_PY := $(VENV)/bin/python
|
||
MIPS_PREFIX := mipsel-linux-gnu-
|
||
AS := $(MIPS_PREFIX)as
|
||
LD := $(MIPS_PREFIX)ld
|
||
OBJCOPY := $(MIPS_PREFIX)objcopy
|
||
CC1_PSX := tools/bin/gcc-2.7.2-psx/cc1
|
||
CC1_CDK := tools/bin/gcc-2.7.2-cdk/cc1
|
||
MASPSX := tools/maspsx/maspsx.py
|
||
# =============================================================================
|
||
# Binaries — data-driven (Phase 9). Each binary is an alias key in BINARIES with a
|
||
# namespaced <alias>_* variable set. `main` is the retail EXE SLUS_007.26 (the FIRST
|
||
# instance); its artifact paths are PRESERVED VERBATIM (build/us/, *.us.* config) so
|
||
# its rebuild stays a byte-exact no-op. The clean <bin> path convention (config/
|
||
# splat.<bin>.yaml, build/<bin>/, config/check.<bin>.sha, config/symbols.<bin>.txt,
|
||
# .run/sig.<bin>.jsonl) is documented now but first INSTANTIATED by Phase 10's second
|
||
# binary. Select with `make build BINARY=<alias>`; defaults to the EXE.
|
||
# -----------------------------------------------------------------------------
|
||
# Overlay binaries (Phase 13): each location overlay is registered as an alias in the
|
||
# GENERATED config/overlays.mk (it defines OVERLAY_BINARIES + the per-<ov> var blocks),
|
||
# kept out of this hand-maintained file so tools/new_overlay.sh never edits the Makefile
|
||
# body. The `-include` is silent when absent (fresh clone / no overlays onboarded yet) ->
|
||
# OVERLAY_BINARIES expands empty -> BINARIES stays `main resident` and every byte-locked
|
||
# build is unchanged. Must precede the `:=` BINARIES line (simply-expanded -> read now).
|
||
-include config/overlays.mk
|
||
# P30 S44: module-class binaries (own load address, resident-shaped — the md_* small actor modules
|
||
# + the SC07 endgame pair + the raw-stored overlays' registry). Same contract as overlays.mk:
|
||
# generated by tools/new_binary.sh, silent when absent, must precede the `:=` line.
|
||
-include config/modules.mk
|
||
BINARIES := main resident $(OVERLAY_BINARIES) $(MODULE_BINARIES)
|
||
BINARY ?= main
|
||
$(if $(filter $(BINARY),$(BINARIES)),,$(error BINARY='$(BINARY)' not in BINARIES='$(BINARIES)'))
|
||
|
||
# --- main (retail EXE SLUS_007.26) — values preserved from Phases 4-8 ---------
|
||
main_EXE := extracted/retail/SLUS_007.26
|
||
main_NAME := SLUS_007.26
|
||
main_OUT_DIR := build/us
|
||
main_OUT := $(main_OUT_DIR)/$(main_NAME)
|
||
main_ELF := $(main_OUT).elf
|
||
main_MAPFILE := $(main_OUT).map
|
||
main_LD_SCRIPT := $(main_OUT).ld
|
||
main_SPLAT_YAML := config/splat.us.exe.yaml
|
||
main_CHECK_SHA := config/check.us.sha
|
||
main_SYMBOLS := config/symbols.us.txt
|
||
main_SIG := .run/sig.SLUS_007.26.jsonl
|
||
main_GHIDRA_PROG := SLUS_007.26
|
||
# The fileoff->vram relation: text loads at file 0x800 / vram 0x80010000, so
|
||
# base = 0x80010000 - 0x800 = 0x8000F800. NOT a universal PS1 constant — overlays
|
||
# differ. Threaded into the tools as a REQUIRED param starting T5; defined here now.
|
||
main_VRAM_BASE := 0x8000F800
|
||
main_TEXT_LO := 0x80010000
|
||
main_TEXT_HI := 0x800629DC
|
||
# Source roots + undefined-sym outputs: main lives at the repo root (verbatim).
|
||
main_ASM_DIR := asm
|
||
main_SRC_DIR := src
|
||
main_UNDEF_SYMS := undefined_syms_auto.txt
|
||
main_UNDEF_FUNCS := undefined_funcs_auto.txt
|
||
|
||
# --- resident (engine blob MAIN.CD/FILE_010/1.1, vram 0x800CEDF8 — Phase 10) ----
|
||
# The always-resident engine blob: extracted type-1 (uncompressed) payload, load
|
||
# address RAM-proven in Phase 3 (T6b). Flat image (no PS-X EXE header); fileoff 0 ->
|
||
# vram 0x800CEDF8 so VRAM_BASE = 0x800CEDF8 (NOT main's 0x8000F800). 365,404 B (0x5935C)
|
||
# -> end vram 0x80128154. Its splat output NESTS under asm/resident + src/resident +
|
||
# build/resident, kept disjoint from main's asm/ + src/ by the per-binary OBJS glob below.
|
||
resident_EXE := extracted/retail/MAIN.CD.dir/FILE_010.dir/1.1
|
||
resident_NAME := resident
|
||
resident_OUT_DIR := build/resident
|
||
resident_OUT := $(resident_OUT_DIR)/$(resident_NAME)
|
||
resident_ELF := $(resident_OUT).elf
|
||
resident_MAPFILE := $(resident_OUT).map
|
||
resident_LD_SCRIPT := $(resident_OUT).ld
|
||
resident_SPLAT_YAML := config/splat.resident.yaml
|
||
resident_CHECK_SHA := config/check.resident.sha
|
||
resident_SYMBOLS := config/symbols.resident.txt
|
||
resident_SIG := .run/sig.resident.jsonl
|
||
resident_GHIDRA_PROG := resident
|
||
resident_VRAM_BASE := 0x800CEDF8
|
||
resident_TEXT_LO := 0x800CEDF8
|
||
resident_TEXT_HI := 0x80128154
|
||
resident_ASM_DIR := asm/resident
|
||
resident_SRC_DIR := src/resident
|
||
resident_UNDEF_SYMS := build/resident/undefined_syms_auto.txt
|
||
resident_UNDEF_FUNCS := build/resident/undefined_funcs_auto.txt
|
||
|
||
# --- selected-binary aliases (resolve $(BINARY) -> the active instance) -------
|
||
EXE := $($(BINARY)_EXE)
|
||
NAME := $($(BINARY)_NAME)
|
||
OUT_DIR := $($(BINARY)_OUT_DIR)
|
||
OUT := $($(BINARY)_OUT)
|
||
ELF := $($(BINARY)_ELF)
|
||
MAPFILE := $($(BINARY)_MAPFILE)
|
||
LD_SCRIPT := $($(BINARY)_LD_SCRIPT)
|
||
# Phase-26 §8: overlay jtbl-rodata carve args (empty = no carve). $(strip) so a per-binary var
|
||
# that is unset stays EMPTY (a trailing comment on the := line would leave whitespace -> non-empty
|
||
# -> the extract branch would misfire on every binary; caught on resident).
|
||
JTBL_INTERLEAVE := $(strip $($(BINARY)_JTBL_INTERLEAVE))
|
||
SPLAT_YAML := $($(BINARY)_SPLAT_YAML)
|
||
CHECK_SHA := $($(BINARY)_CHECK_SHA)
|
||
SYMBOLS := $($(BINARY)_SYMBOLS)
|
||
VRAM_BASE := $($(BINARY)_VRAM_BASE)
|
||
TEXT_LO := $($(BINARY)_TEXT_LO)
|
||
TEXT_HI := $($(BINARY)_TEXT_HI)
|
||
ASM_DIR := $($(BINARY)_ASM_DIR)
|
||
SRC_DIR := $($(BINARY)_SRC_DIR)
|
||
UNDEF_SYMS := $($(BINARY)_UNDEF_SYMS)
|
||
UNDEF_FUNCS := $($(BINARY)_UNDEF_FUNCS)
|
||
GHIDRA_PROG := $($(BINARY)_GHIDRA_PROG)
|
||
|
||
# cc1 smoke flags — the §5.4 first-candidate set; the real triple is pinned only
|
||
# after Phase-6 fingerprinting. Used here purely to prove cc1 executes.
|
||
CC1_SMOKE_FLAGS := -quiet -O2 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
|
||
|
||
# binutils regression line (docs/SETUP.md §4.5): >= 2.38 is a WARN (PS1-matching
|
||
# regression suspect; 2.35 known-good). The verdict is revisited in Phase 5.
|
||
BINUTILS_WARN_MAJOR := 2
|
||
BINUTILS_WARN_MINOR := 38
|
||
|
||
.PHONY: help check-env extract build check expected clean report sig-refresh sig-overlays sig-resident build-all check-all audit-corpus audit-cdecl audit-binaries audit-text-sources audit-digest audit-frontier tools-health
|
||
|
||
# -----------------------------------------------------------------------------
|
||
help:
|
||
@echo "BFM-decomp — make targets:"
|
||
echo " make check-env Phase-4 toolchain preflight (the only live target)"
|
||
echo " make extract [Phase 5] splat split -> asm/ + linker scripts"
|
||
echo " make build [Phase 5] full pipeline -> build/us/SLUS_007.26 (+ SHA1 check)"
|
||
echo " make check [Phase 5] standalone SHA1 verification"
|
||
echo " make expected [Phase 5] snapshot build/us -> expected/ (asm-differ baseline)"
|
||
echo " make clean [Phase 5] remove build output"
|
||
echo " make report [Phase 7] regenerate docs/ progress+difficulty+duplicate digests"
|
||
echo " make sig-refresh [Phase 7] regenerate .run/sig.*.jsonl from Ghidra (MCP must be stopped)"
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# Phase 7 reports: deterministic, committable docs/ digests. progress/difficulty/dup_report
|
||
# are Ghidra-free; sig-refresh regenerates dup_report's input from the saved Ghidra DB.
|
||
GHIDRA := $(or $(GHIDRA_INSTALL_DIR),$(HOME)/ghidra_12.1_PUBLIC)
|
||
GHIDRA_PROJ := $(HOME)/bfm-decomp/ghidra
|
||
|
||
# The corpus oracle (Phase 26-A, R32/R33). A SECOND, INDEPENDENT oracle: it cross-checks splat's
|
||
# function boundaries against sig_image's, which are derived from the ORIGINAL bytes without splat.
|
||
# The byte-gate is structurally BLIND to a bad boundary (the .s halves are pasted back verbatim, so
|
||
# the image stays byte-identical) — only an oracle that can DISAGREE can see it. GREEN since A4
|
||
# (0 phantom + 0 truncated; was 193 unmatchable slices from one bad symbol line).
|
||
audit-corpus:
|
||
$(VENV_PY) tools/corpus.py --all --audit
|
||
|
||
# The C-declaration oracle (Phase 26-A; R33 BEFORE R32). ONE parser, replacing fifteen regex models
|
||
# of what a C declaration is — models that disagree with each other and are blind, all fifteen, to
|
||
# fn-ptr / sized-array / multi-declarator decls.
|
||
#
|
||
# It is a GATE, not just a capability, because this phase paid to learn that a loud failure NOBODY
|
||
# COUNTS is exactly as invisible as a silent one (build_engine_types failed loudly for four phases
|
||
# while hard-exiting on 81% of its own corpus). So: run it, and count it.
|
||
#
|
||
# Coverage is asserted from the C GRAMMAR itself — at file scope C admits nothing but declarations,
|
||
# so the candidate set is every depth-0 statement, and there is no hand-maintained candidate regex
|
||
# to rot. The real cross-gcc then adjudicates BOTH the parse and the residue (R34): it compiles each
|
||
# declaration beside this parser's reconstruction of it, and a statement gcc also rejects is not C.
|
||
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
|
||
|
||
# P30 S43 (Drew's directive): the DENOMINATOR's completeness gate. Walks the DISC IMAGE, not our
|
||
# configs, and asserts every byte lands in exactly one bucket — residue is a DEFECT (R32). This is
|
||
# what makes "there was more code all along" a finding the tools report rather than a surprise we
|
||
# trip over: three such surprises (the 0.4.dec glob, disc_code_sweep's raw-only decode, the 4,096-word
|
||
# window) were each a tool correct about its subset and silent about the rest.
|
||
# NOT in tools-health: it needs disks/, which a fresh clone does not have (H1 — the dump is ignored).
|
||
audit-disc:
|
||
$(VENV_PY) tools/disc_audit.py
|
||
|
||
# P30 S1e: the committed fleet digest must still describe the CURRENT tree. A digest generated from
|
||
# a working tree that later changed (work reverted before the commit landed) is BYTE-INVISIBLE —
|
||
# check-all stays 140/140 over it — and the next honest regeneration then reads as a REGRESSION that
|
||
# never happened. That cost a session-opening false alarm and gated the best-performing lever on a
|
||
# phantom. R34: the byte-gate is a null oracle for documents, so this is a second one that disagrees.
|
||
audit-digest:
|
||
$(VENV_PY) tools/audit_digest.py
|
||
|
||
# P30 S39 (Drew's MASTER_REMAINING proposal, derived form — docs/decision-log.md 2026-08-04):
|
||
# "what's left" is answered by six artifacts, each individually derived and NONE ever checked
|
||
# against the others. That gap cost T0 a hand-reconciliation (family_hseq 29,961 vs progress 28,296).
|
||
# This is the R34 move: a SECOND view that can DISAGREE with the corpus oracle, loudly.
|
||
# DELIBERATELY NOT in tools-health yet — additive until Drew has seen it; wiring is one line.
|
||
audit-frontier:
|
||
$(VENV_PY) tools/audit_frontier.py
|
||
|
||
# P30 S28: every tracked C source must be TEXT. A raw NUL inside a char literal (`'<NUL>'` instead
|
||
# of `'\0'`) COMPILES — the fleet stayed byte-identical — but grep treats the file as BINARY and
|
||
# reports nothing, silently, so the file vanishes from every grep-based audit and hand-search. The
|
||
# byte-gate is structurally blind to it (R34: correct bytes, nothing to say). Found when a
|
||
# `grep -rn func_8013C08C src/` came back empty for a function defined right there; a templated body
|
||
# had then carried the NUL into 137 overlays in this same session. Its own oracle, fail-closed.
|
||
audit-text-sources:
|
||
$(VENV_PY) tools/audit_text_sources.py
|
||
|
||
# The tool-health ritual (Phase-27 T2). Before the 26-A audit the two oracles above had NO dependent
|
||
# — nothing invoked them, so "run the audits" was a manual habit, and a habit nobody automates is a
|
||
# gate nobody counts (R32). This is that dependent: `make tools-health` runs both derived oracles and
|
||
# the report gates (lint_symbol_refs + dedup-check) together, and under the global -e ANY one failing
|
||
# aborts it. It is deliberately NOT a prerequisite of `report`/`build` — audit-cdecl cross-compiles
|
||
# every C declaration through real gcc (minutes), so it belongs to a deliberate pre-matching ritual,
|
||
# not the inner harvest loop. Matches the roadmap's standing invariant (audit-corpus · audit-cdecl ·
|
||
# report green before matching).
|
||
tools-health:
|
||
# Regenerate the byte-derived boundary oracles FIRST (they're gitignored/regenerable), so the
|
||
# audit checks CURRENT sigs and never crashes on an absent one — the resident audit (T10) needs
|
||
# the sig_image resident sig, and a fresh clone has neither it nor the overlay sigs.
|
||
$(MAKE) --no-print-directory sig-overlays
|
||
$(MAKE) --no-print-directory sig-resident
|
||
$(MAKE) --no-print-directory sig-modules
|
||
$(MAKE) --no-print-directory audit-corpus
|
||
$(MAKE) --no-print-directory audit-cdecl
|
||
$(MAKE) --no-print-directory audit-binaries
|
||
$(MAKE) --no-print-directory audit-text-sources
|
||
$(MAKE) --no-print-directory report BINARY=main
|
||
# AFTER report (which regenerates the digest), so this asserts the freshly-written digest agrees
|
||
# with the tree — and, on a tree whose digest was committed stale, says so instead of staying green.
|
||
$(MAKE) --no-print-directory audit-digest
|
||
# The cookbook index is DERIVED (R33) and self-asserts its coverage (R32). Stale = agents can't
|
||
# find documented idioms and re-derive them at full token cost (measured, P30 wave 1).
|
||
$(VENV_PY) tools/cookbook_index.py --check
|
||
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; P31 T0 seed fix): sign the resident flat blob with sig_image — the
|
||
# Ghidra-FREE, byte-DERIVED signer — so `make audit-corpus` gains a second boundary oracle for the
|
||
# resident (R34; corpus.sig_is_independent trusts it, same standing as the S45 ELF-seeded modules).
|
||
# P31 T0: --bootstrap's linear partition produced a WRONG denominator (144 rows vs the true 145) via
|
||
# two boundary artifacts — it fused the +0 data word with the first function (row 0x800CEDF8 nins=18
|
||
# instead of func_800CEDFC) and glued/truncated the tail pair (func_800D33E0 missing). Fixed by the
|
||
# S45 pattern: SEED from the built ELF's T symbols (unique 4-aligned addrs inside the
|
||
# resident_TEXT_START/END markers — reproduces exactly 145, matching progress + the source defs).
|
||
# Fresh-clone fallback (no build yet) stays --bootstrap and self-heals on the next run after a build.
|
||
# h_exact is raw-byte SHA1 so it is format-independent — weighted_metrics is unaffected. R23-free.
|
||
sig-resident:
|
||
elf="build/resident/resident.elf"
|
||
if [ -f "$$elf" ]; then
|
||
mipsel-linux-gnu-nm "$$elf" | awk '
|
||
$$2=="T" { a=strtonum("0x" $$1); sym[$$3]=a; if (a%4==0) addr[a]=1 }
|
||
END { lo=sym["resident_TEXT_START"]; hi=sym["resident_TEXT_END"];
|
||
for (a in addr) if (a>=lo && a<hi) printf "0x%08X\n", a }' \
|
||
| sort -u > .run/seeds.resident.txt
|
||
$(VENV_PY) tools/sig_image.py --image $(resident_EXE) --vram-base $(resident_VRAM_BASE) \
|
||
--seeds .run/seeds.resident.txt --name resident
|
||
echo "sig-resident: signed the resident (ELF-seeded, $$(wc -l < .run/seeds.resident.txt) fns) -> .run/sig.resident.jsonl"
|
||
else
|
||
$(VENV_PY) tools/sig_image.py --image $(resident_EXE) --vram-base $(resident_VRAM_BASE) --bootstrap --name resident
|
||
echo "sig-resident: signed the resident (bootstrap fallback — re-run after a build for seeded boundaries)"
|
||
fi
|
||
|
||
# atlas (P31 T5): the full Frontier Atlas regen chain — family maps -> cards -> features -> the
|
||
# partition-asserted atlas (every open stub in exactly one lever-labeled crack group). Run at
|
||
# session T0 and after crack batches; ~10-15 min, zero tokens. Standalone atlas.py runs tolerate
|
||
# maps that are stale only in the banked-since direction; this chain makes the normal path fresh.
|
||
atlas:
|
||
$(VENV_PY) tools/family_hseq.py >/dev/null
|
||
$(VENV_PY) tools/family_cousins.py >/dev/null
|
||
$(VENV_PY) tools/family_cousins.py --adapt-cards >/dev/null
|
||
$(VENV_PY) tools/family_cousins.py --aprop-cards >/dev/null
|
||
$(VENV_PY) tools/atlas_features.py
|
||
$(VENV_PY) tools/atlas.py
|
||
echo "atlas: chain complete -> .run/atlas.json + docs/frontier-atlas.md"
|
||
|
||
# sig-main (P31 T3): sign main's game-code STUBS with sig_image at SPLAT-TRUE lengths. main has a
|
||
# 0x800 EXE header (file0-vram = $(main_VRAM_BASE)), interleaved data islands, and LINKED PsyQ
|
||
# regions, so --bootstrap/func_end both mis-slice (measured 3/40 nins drift vs the .s truth);
|
||
# instead `corpus.py main --seed-ends` emits `0xADDR NINS` per stub (the .s count a C match must
|
||
# reproduce) and each slice is exactly [addr, addr+4*nins) — no heuristic, no contiguity assumption
|
||
# beyond each function's own slice. This sig is deliberately splat-SEEDED (the atlas needs the
|
||
# boundaries a match must hit); it is NOT main's independent second oracle
|
||
# (docs/second-oracle.md — sig_is_independent stays False for main).
|
||
sig-main:
|
||
$(VENV_PY) tools/corpus.py main --seed-ends > .run/seeds.main.txt
|
||
$(VENV_PY) tools/sig_image.py --image $(main_EXE) --vram-base $(main_VRAM_BASE) --seeds .run/seeds.main.txt --name main
|
||
echo "sig-main: signed $$(wc -l < .run/seeds.main.txt) main stubs (splat-true lengths) -> .run/sig.main.jsonl"
|
||
|
||
# sig-modules (P30 S44): sign every module-class binary at ITS OWN vram (from modules.mk) with its
|
||
# own TEXT_LO (the §154 module-id-word law: code starts past the header; bootstrap from offset 0
|
||
# yields 0 functions). Same derived-jobs shape as sig-overlays (R33). Empty MODULE_BINARIES = no-op.
|
||
# S45: SEED from the built ELF's text symbols when the build exists (R33 — splat's post-link
|
||
# boundaries are the finer oracle; --bootstrap's linear partition GLUES adjacent functions around
|
||
# jtbl-dispatch code, which read as 24 TRUNCATED slices in audit-corpus). Fresh-clone fallback
|
||
# (no build yet) stays --bootstrap; the next sig-modules after a build self-heals.
|
||
MODULE_SIG_JOBS := $(foreach a,$(MODULE_BINARIES),$(a):$($(a)_EXE):$($(a)_VRAM_BASE):$($(a)_TEXT_LO))
|
||
sig-modules:
|
||
@n=0
|
||
for job in $(MODULE_SIG_JOBS); do
|
||
alias=$${job%%:*}; rest=$${job#*:}; f=$${rest%%:*}; rest=$${rest#*:}
|
||
vram=$${rest%%:*}; tlo=$${rest#*:}
|
||
[ -f "$$f" ] || { echo "sig-modules: WARN no payload for $$alias ($$f)"; continue; }
|
||
elf="build/$$alias/$$alias.elf"
|
||
if [ -f "$$elf" ]; then
|
||
# func_*-named, 4-aligned, at/after TEXT_LO only: the module links into ONE output
|
||
# section, so nm types EVERY symbol T — data labels (odd addresses) included.
|
||
mipsel-linux-gnu-nm "$$elf" | awk -v lo=$$(($$tlo)) \
|
||
'$$2=="T" && $$3~"^func_" { a=strtonum("0x" $$1); if (a>=lo && a%4==0) printf "0x%X\n", a }' \
|
||
| sort -u > ".run/seeds.$$alias.txt"
|
||
$(VENV_PY) tools/sig_image.py --image "$$f" --vram-base "$$vram" \
|
||
--seeds ".run/seeds.$$alias.txt" --name "$$alias" \
|
||
$${tlo:+--text-lo "$$tlo"} >/dev/null
|
||
else
|
||
$(VENV_PY) tools/sig_image.py --image "$$f" --vram-base "$$vram" --bootstrap --name "$$alias" \
|
||
$${tlo:+--text-lo "$$tlo"} >/dev/null
|
||
fi
|
||
n=$$((n+1))
|
||
done
|
||
echo "sig-modules: signed $$n modules (of $(words $(MODULE_BINARIES)) onboarded)"
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# 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.
|
||
# MODULES (P31 S62 T3a): every md_* object runs the filter in --derive mode instead — the pads are
|
||
# derived at build time from the retail island + the maspsx stream (cookbook §303), so no spec is
|
||
# stored and nothing can drift. A stored JTBL_PADS still wins if one is set.
|
||
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),$(if $(filter md_%,$(BINARY)),| $(VENV_PY) tools/jtbl_rodata_pads.py --derive $(BINARY) --tu $(notdir $*))) | $(AS) $(ASFLAGS) -o $@
|
||
|
||
# Per-module optimization override (SETUP §5.5 — per-module compiler mixing). The boot/
|
||
# main/game-mode-dispatch module (src/boot.c, vram 0x80010000-0x800123F0) was compiled at
|
||
# -O0, NOT the -O2 game-code default: frame-pointer setup + unfolded large-offset loads
|
||
# are the evidence (GameModeDispatch byte-matches only at -O0). gcc 2.7.2 has no
|
||
# per-function optimize pragma, so opt level is per-file. Target-specific CC1FLAGS (the
|
||
# pattern recipe reads $(CC1FLAGS), so this overrides it for just build/src/boot.o):
|
||
build/src/boot.o: CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
|
||
|
||
# Phase-19 T1: same per-file -O0 mechanism for the ov_SC01_077 -O0 cluster (16 contiguous fns
|
||
# vram 0x8013B568..0x8013C98C, prologue sig 21F0A003). Split into its own .c by the splat config
|
||
# (config/splat.ov_SC01_077.yaml) so this override reaches just that .o.
|
||
build/src/ov_SC01_077/ov_SC01_077_o0.o: CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
|
||
|
||
# Phase-24: the whale func_80144B9C is a 2nd -O0 region (0x80144B9C..0x801457A4) present in EVERY
|
||
# overlay (reach-134), carved into its own object <ov>_o0b by each overlay's splat config; the
|
||
# struct-assign memcpy matches only at -O0. One wildcard rule -O0-compiles all overlays' _o0b.o
|
||
# (the ×134 rollout; tools/rollout_whale_o0.py — one-shot, retired S45). All share src/shared/func_80144B9C.h.
|
||
#
|
||
# P30 T2: the glob is `_o0?` (was `_o0b`) so ANY lettered -O0 sub-split is covered by this one rule.
|
||
# A 4th -O0 region was found inside an -O2 jr split (0x80183CF0..0x80184920, 15 contiguous fns in
|
||
# ov_SC03_014 + ov_SC03_015) — the "carve within a carve": the containing object is sub-split into
|
||
# pre/-O0/post and the middle region named <ov>_o0c. Without the widened glob each new region would
|
||
# need its own hand-added rule, and a MISSED rule is silent: the region compiles at -O2 and every
|
||
# residual it produces is a pure artifact (§116 — opt level is a property of the FILE).
|
||
# corpus.o0_sources() parses this rule and resolves `?` via glob, so the -O0 oracle stays correct.
|
||
# P31 S68: glob widened to src/md_*/ too — module binaries now get -O0 sub-splits
|
||
# (md_MAIN_003_o0c, the 9-stub o0 carve, is the first). Without this a `src/md_*` region
|
||
# file silently compiled -O2: byte-neutral while stub-only (INCLUDE_ASM is verbatim asm),
|
||
# but every -O0 draft banked into it would mystery-fail the gate (§362's trap class).
|
||
# corpus.o0_sources() splits multi-glob $(wildcard ...) specs, so the -O0 oracle follows.
|
||
WHALE_O0B_OBJS := $(patsubst src/%.c,build/src/%.o,$(wildcard src/ov_*/ov_*_o0?.c src/md_*/md_*_o0?.c))
|
||
$(WHALE_O0B_OBJS): CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
|
||
|
||
# Phase-29 T2 Arm A: the -O0 cluster (0x8013B568..0x8013C98C) carved per single-file overlay into
|
||
# <ov>_o0.o (tools/rollout_o0_cluster.py — one-shot, retired S45; the generic driver is tools/rollout_o0.py) — same per-file -O0 mechanism so its h_seq family members
|
||
# bank whole-binary (the Task-1 swing verdict: they masked-MATCH only at -O0). One wildcard rule
|
||
# -O0-compiles every overlay's _o0.o; ov_SC01_077_o0.o already has its explicit rule above (filtered
|
||
# out to avoid a duplicate target-specific assignment). `*_o0.c` never matches the whale's `*_o0b.c`.
|
||
O0_CLUSTER_OBJS := $(patsubst src/%.c,build/src/%.o,$(filter-out src/ov_SC01_077/ov_SC01_077_o0.c,$(wildcard src/ov_*/ov_*_o0.c)))
|
||
$(O0_CLUSTER_OBJS): CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
|
||
|
||
# md_MAIN_011 IS AN ENTIRELY -O0 MODULE (P31 S59, census in .run/s59_o0/). All 21 functions in its
|
||
# single code subseg carry the -O0 frame-pointer prologue (`sw $fp` + `addu $fp,$sp,$zero`), the
|
||
# .c is stub-only, and no -O0 glob matches `src/md_*/` at all — so its functions were unbankable no
|
||
# matter how good a draft was, and the wave draw now refuses to draw them (build_wave_atlas's
|
||
# `o0-in-an-O2-object` skip). Whole-object override, the `boot` precedent (§6): no splat change, no
|
||
# carve, and therefore none of the §18-P29 re-disassembly risk. Byte-neutral while the file is
|
||
# stub-only — proven by gating md_MAIN_011 when this landed. `corpus.o0_sources()` parses this rule,
|
||
# so every -O0-aware tool picks the object up without a name convention.
|
||
build/src/md_MAIN_011/md_MAIN_011.o: CC1FLAGS := -quiet -O0 -G0 -mips1 -mcpu=3000 -mgas -msoft-float -fgnu-linker
|
||
|
||
# link (the .ld pulls in the .o by path) + objcopy to the raw PS-X EXE image.
|
||
$(OUT): $(OBJS) $(ASSET_OBJS) $(LD_SCRIPT)
|
||
@set -e
|
||
mkdir -p $(dir $@)
|
||
# PsyQ SDK library integrations are EXE-only (libgs/libgte/sound/apicard are
|
||
# SLUS_007.26's layout). A second binary (BINARY != main) skips this block and links
|
||
# its own stubs. NB: ifeq/endif are make directives (column 0, no tab), resolved at
|
||
# parse time; with .ONESHELL the included recipe lines still run as one shell.
|
||
ifeq ($(BINARY),main)
|
||
# Wire in the real libcd objects (after the build objects exist — the externals discovery
|
||
# trial-links the whole image). Idempotent: re-running re-derives the externals only.
|
||
if [ -d "$(LIBCD_ELF)" ]; then
|
||
$(PYTHON) tools/psyq_integrate.py --vram-base $(main_VRAM_BASE) --exe $(main_EXE) --symbols $(main_SYMBOLS) $(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)."
|
||
|