diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 6ad7ac9d..5463e147 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -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) diff --git a/tools/bios_emitter_fingerprint.cmake b/tools/bios_emitter_fingerprint.cmake new file mode 100644 index 00000000..c324bb74 --- /dev/null +++ b/tools/bios_emitter_fingerprint.cmake @@ -0,0 +1,86 @@ +# bios_emitter_fingerprint.cmake — THE emitter fingerprint, as a CMake script. +# +# cmake -DPROFILE=bios/.toml [-DROOT=] [-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/.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, " -\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}") diff --git a/tools/bios_emitter_fingerprint.sh b/tools/bios_emitter_fingerprint.sh index 8e84d731..8cce9238 100755 --- a/tools/bios_emitter_fingerprint.sh +++ b/tools/bios_emitter_fingerprint.sh @@ -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/.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/.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 " " 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 " " -# 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 " " 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"