bios fingerprint: compute it with cmake -P, not a bash found on PATH

The windows-2022 release job accepted the committed backends in the step
shell (Git Bash) and refused them at configure: runtime.cmake ran the bash
fingerprint through whichever bash find_program() located, and that
environment hashed a byte-identical tree differently. The fingerprint is now
tools/bios_emitter_fingerprint.cmake, run with the configuring cmake itself
on every platform; the shell script is a thin wrapper around it. Same digest
shape as before, so existing stamps stay valid (verified on an LF tree and
an autocrlf clone).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Alex Vanderveen
2026-09-30 20:49:23 -04:00
parent 52c7b7447b
commit efcb48fbcb
3 changed files with 106 additions and 80 deletions
+7 -4
View File
@@ -767,9 +767,11 @@ list(APPEND PSXRECOMP_BIOS_GENERATED "${_psxrt_registry_c}")
option(PSXRECOMP_BIOS_STALE_FATAL
"Fail configure when a linked BIOS stem's generated/ stamp predates the emitter"
OFF)
# The fingerprint is a CMake script run with THIS cmake, never a bash found on
# PATH: on the windows-2022 runner the bash CMake located hashed differently
# from the step shell's, and a byte-identical tree read as STALE there only.
if(NOT PSXRECOMP_SKIP_BIOS_STALE_CHECK AND _psxrt_bios_linked)
find_program(_psxrt_bash NAMES bash)
if(_psxrt_bash AND EXISTS "${PSXRECOMP_ROOT}/tools/bios_emitter_fingerprint.sh")
if(EXISTS "${PSXRECOMP_ROOT}/tools/bios_emitter_fingerprint.cmake")
foreach(_psxrt_chk_stem IN LISTS _psxrt_bios_linked)
set(_psxrt_stamp "${PSXRECOMP_ROOT}/generated/${_psxrt_chk_stem}.emitter.sha")
set(_psxrt_chk_profile "${PSXRECOMP_ROOT}/bios/${_psxrt_chk_stem}.toml")
@@ -777,8 +779,9 @@ if(NOT PSXRECOMP_SKIP_BIOS_STALE_CHECK AND _psxrt_bios_linked)
continue()
endif()
execute_process(
COMMAND "${_psxrt_bash}" "${PSXRECOMP_ROOT}/tools/bios_emitter_fingerprint.sh"
"${_psxrt_chk_profile}"
COMMAND "${CMAKE_COMMAND}" "-DROOT=${PSXRECOMP_ROOT}"
"-DPROFILE=${_psxrt_chk_profile}"
-P "${PSXRECOMP_ROOT}/tools/bios_emitter_fingerprint.cmake"
WORKING_DIRECTORY "${PSXRECOMP_ROOT}"
OUTPUT_VARIABLE _psxrt_cur_fp OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE _psxrt_fp_rc ERROR_QUIET)
+86
View File
@@ -0,0 +1,86 @@
# bios_emitter_fingerprint.cmake — THE emitter fingerprint, as a CMake script.
#
# cmake -DPROFILE=bios/<stem>.toml [-DROOT=<framework>] [-DFP_DEBUG=1] \
# -P tools/bios_emitter_fingerprint.cmake
#
# Prints one SHA-256 over every input that shapes the generated BIOS C: the
# psxrecomp-bios emitter sources, the cycle headers they bake in, the profile
# and its seeds. Not the ROM image: the profile pins its SHA-256 and the
# emitter refuses any other image, and CI has no dump.
#
# WHY CMAKE AND NOT BASH. runtime.cmake compares this against the committed
# generated/<stem>.emitter.sha at configure (fatal in release CI). A bash
# implementation hashed differently depending on which bash/coreutils CMake
# found on a Windows runner (Git Bash in the step shell agreed with Linux; the
# bash CMake located did not), so a byte-identical tree read as STALE there
# only. cmake is the one tool every configure already has, and
# file(READ)+string(SHA256) behave the same everywhere. Carriage returns are
# stripped before hashing so a CRLF checkout agrees with an LF one.
#
# Digest shape (kept identical to the previous bash form so existing stamps
# stay valid): per file, "<sha256 of CR-stripped bytes> -\n"; the fingerprint
# is the SHA-256 of that concatenated listing.
cmake_minimum_required(VERSION 3.20)
if(NOT ROOT)
get_filename_component(ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
endif()
if(NOT PROFILE)
set(PROFILE "bios/SCPH1001.toml")
endif()
if(NOT IS_ABSOLUTE "${PROFILE}")
set(PROFILE "${ROOT}/${PROFILE}")
endif()
set(_files
recompiler/src/full_function_emitter.cpp
recompiler/src/full_function_emitter.h
recompiler/src/strict_translator.cpp
recompiler/src/main_bios.cpp
recompiler/src/function_discovery.cpp
recompiler/src/bios_address_model.cpp
recompiler/src/bios_address_model.h
recompiler/src/config_loader.cpp
recompiler/src/control_flow.cpp
recompiler/src/function_analysis.cpp
recompiler/src/mips_decoder.cpp
recompiler/src/bios_slice_walker.cpp
recompiler/src/basic_block.cpp
runtime/include/psx_cyc.h
runtime/include/psx_instr_cost.h)
set(_paths)
foreach(_f IN LISTS _files)
list(APPEND _paths "${ROOT}/${_f}")
endforeach()
# Per-profile inputs: the profile itself and the seeds file it names.
if(EXISTS "${PROFILE}")
list(APPEND _paths "${PROFILE}")
file(STRINGS "${PROFILE}" _seed_lines REGEX "^[ \t]*seeds[ \t]*=[ \t]*\"[^\"]*\"")
if(_seed_lines)
list(GET _seed_lines 0 _seed_line)
string(REGEX REPLACE "^[ \t]*seeds[ \t]*=[ \t]*\"([^\"]*)\".*$" "\\1" _seeds "${_seed_line}")
if(_seeds)
list(APPEND _paths "${ROOT}/${_seeds}")
endif()
endif()
else()
list(APPEND _paths "${ROOT}/recompiler/seeds/phase2_ghidra_seeds.json")
endif()
set(_listing "")
foreach(_p IN LISTS _paths)
if(NOT EXISTS "${_p}")
continue()
endif()
file(READ "${_p}" _content)
string(REPLACE "\r" "" _content "${_content}")
string(SHA256 _d "${_content}")
if(FP_DEBUG)
file(RELATIVE_PATH _rel "${ROOT}" "${_p}")
message("${_d} ${_rel}")
endif()
string(APPEND _listing "${_d} -\n")
endforeach()
string(SHA256 _fp "${_listing}")
# message() writes to stderr; the digest is the script's stdout contract.
execute_process(COMMAND "${CMAKE_COMMAND}" -E echo "${_fp}")
+13 -76
View File
@@ -1,80 +1,17 @@
#!/usr/bin/env bash
# bios_emitter_fingerprint.sh — print a single content hash of every input that
# affects the BIOS generated C: the psxrecomp-bios emitter sources, the cycle
# model they bake in, and — per BIOS profile — the profile TOML itself, its
# seeds file, and the ROM image. regen_bios.sh writes this into
# generated/<stem>.emitter.sha after a regen; runtime/runtime.cmake recomputes
# it at configure time and warns if it differs — i.e. "you changed an emitter
# input but didn't regenerate the BIOS". Hashing the ROM and profile means
# swapping the BIOS image or editing its profile trips the staleness warning
# too, not just emitter-source edits.
# bios_emitter_fingerprint.sh — thin wrapper: the fingerprint is computed by
# tools/bios_emitter_fingerprint.cmake (one implementation, run by cmake -P on
# every platform; see its header for why a bash implementation could not be
# trusted across runners). regen_bios.sh writes the result into
# generated/<stem>.emitter.sha; runtime.cmake recomputes it at configure.
#
# Usage: bios_emitter_fingerprint.sh [profile.toml]
# default profile: bios/SCPH1001.toml
#
# The rom/seeds extraction expects simple `key = "value"` lines, which is how
# the tracked profiles are written. Keep the FILES list in sync with what
# actually feeds psxrecomp-bios --emit-full.
set -euo pipefail
# Usage: bios_emitter_fingerprint.sh [profile.toml] (default: bios/SCPH1001.toml)
# PSXRECOMP_FP_DEBUG=1 also prints "<digest> <path>" per input to stderr.
set -eu
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
PROFILE="${1:-bios/SCPH1001.toml}"
FILES=(
recompiler/src/full_function_emitter.cpp
recompiler/src/full_function_emitter.h
recompiler/src/strict_translator.cpp
recompiler/src/main_bios.cpp
recompiler/src/function_discovery.cpp
recompiler/src/bios_address_model.cpp
recompiler/src/bios_address_model.h
recompiler/src/config_loader.cpp
recompiler/src/control_flow.cpp
recompiler/src/function_analysis.cpp
recompiler/src/mips_decoder.cpp
recompiler/src/bios_slice_walker.cpp
recompiler/src/basic_block.cpp
runtime/include/psx_cyc.h
runtime/include/psx_instr_cost.h
)
# Per-profile inputs: the profile itself and its seeds file. The ROM image is
# deliberately NOT hashed: the profile pins its SHA-256 ([program.image]) and
# psxrecomp-bios refuses any other image, so the profile already stands for
# the ROM -- and the generated C for a retail stem is committed and built by
# CI, which has no dump. Hashing the image here made every CI configure read
# "stale" for a tree that was byte-identical to the developer's.
if [ -f "$PROFILE" ]; then
FILES+=("$PROFILE")
toml_value() {
sed -n "s/^[[:space:]]*$1[[:space:]]*=[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$PROFILE" | head -1
}
SEEDS="$(toml_value seeds)"
[ -n "$SEEDS" ] && FILES+=("$SEEDS")
else
# Legacy fallback (pre-profile invocations): the historical seeds path.
FILES+=(recompiler/seeds/phase2_ghidra_seeds.json)
fi
# Hash only files that exist (list may evolve). sha256sum is in Git Bash/coreutils.
present=()
for f in "${FILES[@]}"; do [ -f "$f" ] && present+=("$f"); done
# Single combined digest over file CONTENTS in the FILES-array order above.
# Content-only (stdin mode) on purpose: `sha256sum "$f"` emits "<hash> <path>"
# lines, so the digest depended on the profile's PATH SPELLING — regen_bios.sh
# records with a relative path while runtime.cmake recomputes with an absolute
# one, and every configure read as STALE with a byte-identical tree. The array
# order is defined by this script alone, so it is stable across spellings.
# CR-stripped: Git for Windows checks text files out with CRLF (core.autocrlf
# defaults to true on the windows-* runners), and the same source hashed
# differently there than on the developer's LF checkout -- every Windows CI
# configure read a byte-identical tree as STALE. Stripping CR is a no-op for an
# LF file, so stamps written before this line stay valid.
# PSXRECOMP_FP_DEBUG=1 prints "<per-file digest> <path>" lines to stderr, so a
# runner that disagrees with a developer's stamp can be diffed input by input.
for f in "${present[@]}"; do
d="$(tr -d '\r' < "$f" | sha256sum | awk '{print $1}')"
if [ -n "${PSXRECOMP_FP_DEBUG:-}" ]; then echo "$d $f" >&2; fi
echo "$d -"
done | sha256sum | awk '{print $1}'
CMAKE_BIN="${CMAKE:-cmake}"
command -v "$CMAKE_BIN" >/dev/null 2>&1 || { echo "bios_emitter_fingerprint: cmake not found on PATH" >&2; exit 1; }
args=(-DROOT="$ROOT" -DPROFILE="$PROFILE")
[ -n "${PSXRECOMP_FP_DEBUG:-}" ] && args+=(-DFP_DEBUG=1)
exec "$CMAKE_BIN" "${args[@]}" -P "$ROOT/tools/bios_emitter_fingerprint.cmake"