Files
psxrecomp/cmake/psx_execution_profile.cmake
T
Shokunin 0cfc201e17 Execution identity: hash source text with CRLF read as LF
PSX_EXECUTION_ID hashed raw bytes of the contract files and generated
bodies. A Windows checkout with core.autocrlf=true (the Git for Windows
default) has CRLF in every text file, so a Mac and a PC build of the same
commits (R4 #35, psxrecomp 2a4824fc) got different identities and the
netplay content gate refused the session with "content mismatch". LF
files hash exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 01:09:01 -07:00

209 lines
10 KiB
CMake

# Implementation selection is a developer build choice, never a runtime mod.
include_guard(GLOBAL)
set(PSX_EXECUTION_PROFILE "ENHANCED" CACHE STRING "Execution implementation: ENHANCED or REFERENCE")
set_property(CACHE PSX_EXECUTION_PROFILE PROPERTY STRINGS ENHANCED REFERENCE)
if(NOT PSX_EXECUTION_PROFILE MATCHES "^(ENHANCED|REFERENCE)$")
message(FATAL_ERROR "PSX_EXECUTION_PROFILE must be ENHANCED or REFERENCE")
endif()
# Identity hashes are over the TEXT of sources and contracts, with CRLF read
# as LF. A Windows checkout with core.autocrlf=true has CRLF in every text
# file (including generated/), so hashing raw bytes gave a Mac and a PC build
# of the same commits different PSX_EXECUTION_IDs, and the netplay content
# gate refused the session ("content mismatch").
function(_psx_execution_text_sha256 path out)
file(READ "${path}" _text)
string(REPLACE "\r\n" "\n" _text "${_text}")
string(SHA256 _hash "${_text}")
set(${out} "${_hash}" PARENT_SCOPE)
endfunction()
function(_psx_execution_stamp target)
get_target_property(_profile ${target} PSX_EXECUTION_PROFILE)
get_target_property(_families ${target} PSX_IMPLEMENTATION_FAMILIES)
if(NOT _families)
set(_families "")
endif()
list(SORT _families)
set(_identity "psx-execution-v1\n${_profile}\n")
get_target_property(_shared_files ${target} PSX_EXECUTION_CONTRACT_FILES)
set(_shared_identity "")
if(_shared_files)
foreach(_source IN LISTS _shared_files)
_psx_execution_text_sha256("${_source}" _hash)
string(APPEND _shared_identity "${_hash}\n")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_source}")
endforeach()
endif()
string(SHA256 _shared_hash "${_shared_identity}")
string(APPEND _identity "shared:${_shared_hash}\n")
set(_json "")
foreach(_name IN LISTS _families)
get_target_property(_contract ${target} "PSX_IMPL_${_name}_CONTRACT")
get_target_property(_sources ${target} "PSX_IMPL_${_name}_SOURCES")
set(_source_identity "")
foreach(_source IN LISTS _sources)
# Generated private bodies must exist before configure. Their bytes,
# not workstation paths, identify the implementation.
_psx_execution_text_sha256("${_source}" _hash)
string(APPEND _source_identity "${_hash}\n")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_source}")
endforeach()
string(SHA256 _body_hash "${_source_identity}")
string(APPEND _identity "${_name}:${_contract}:${_body_hash}\n")
if(_json)
string(APPEND _json ",\n")
endif()
string(APPEND _json " {\"name\":\"${_name}\",\"contract\":\"${_contract}\",\"source_sha256\":\"${_body_hash}\"}")
endforeach()
string(SHA256 _digest "${_identity}")
string(SUBSTRING "${_digest}" 0 8 _cookie)
if(_profile STREQUAL "ENHANCED")
set(_enhanced 1)
else()
set(_enhanced 0)
endif()
set(_dir "${CMAKE_CURRENT_BINARY_DIR}/psx_execution/${target}")
file(MAKE_DIRECTORY "${_dir}")
file(CONFIGURE OUTPUT "${_dir}/profile.h" CONTENT
"#pragma once
#define PSX_EXECUTION_ENHANCED @_enhanced@
#define PSX_EXECUTION_NAME \"@_profile@\"
#define PSX_EXECUTION_ID \"@_digest@\"
#define PSX_EXECUTION_COOKIE 0x@_cookie@u
" @ONLY)
file(CONFIGURE OUTPUT "${_dir}/manifest.json" CONTENT
"{\n \"schema\":1,\n \"profile\":\"@_profile@\",\n \"identity\":\"@_digest@\",\n \"shared_contract_sha256\":\"@_shared_hash@\",\n \"implementations\":[\n@_json@\n ]\n}\n" @ONLY)
set_property(TARGET ${target} PROPERTY PSX_EXECUTION_MANIFEST "${_dir}/manifest.json")
endfunction()
function(psxrecomp_execution_profile target)
cmake_parse_arguments(P "" "PROFILE" "CONTRACT_FILES" ${ARGN})
if(P_PROFILE)
set(_profile "${P_PROFILE}")
else()
set(_profile "${PSX_EXECUTION_PROFILE}")
endif()
if(NOT _profile MATCHES "^(ENHANCED|REFERENCE)$")
message(FATAL_ERROR "Invalid execution profile for ${target}: ${_profile}")
endif()
get_target_property(_existing ${target} PSX_EXECUTION_PROFILE)
if(_existing)
if(NOT _existing STREQUAL _profile)
message(FATAL_ERROR "Execution profile already selected for ${target}")
endif()
return()
endif()
set_property(TARGET ${target} PROPERTY PSX_EXECUTION_PROFILE "${_profile}")
set(_inputs "")
foreach(_source IN LISTS P_CONTRACT_FILES)
get_filename_component(_absolute "${_source}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
if(NOT EXISTS "${_absolute}")
message(FATAL_ERROR "Shared execution input does not exist: ${_absolute}")
endif()
list(APPEND _inputs "${_absolute}")
endforeach()
set_property(TARGET ${target} PROPERTY PSX_EXECUTION_CONTRACT_FILES "${_inputs}")
target_compile_definitions(${target} PRIVATE
"PSX_EXECUTION_PROFILE_CONFIG=\"${CMAKE_CURRENT_BINARY_DIR}/psx_execution/${target}/profile.h\"")
_psx_execution_stamp(${target})
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_BINARY_DIR}/psx_execution/${target}/manifest.json"
"$<TARGET_FILE_DIR:${target}>/$<TARGET_FILE_BASE_NAME:${target}>.execution.json"
VERBATIM)
endfunction()
# A materialized game shard can also be a configure/hash input. Compile an
# include wrapper so Ninja does not give the shard both a generation rule and
# CMake's input-file phony rule. Keep hashing the original body.
function(_psx_execution_compile_source target source output)
get_filename_component(_absolute "${source}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
get_source_file_property(_generated "${_absolute}" GENERATED)
if(NOT _generated)
set(${output} "${source}" PARENT_SCOPE)
return()
endif()
string(SHA256 _path_hash "${_absolute}")
get_filename_component(_ext "${_absolute}" LAST_EXT)
set(_wrapper "${CMAKE_CURRENT_BINARY_DIR}/psx_execution/${target}/sources/${_path_hash}${_ext}")
file(CONFIGURE OUTPUT "${_wrapper}" CONTENT "#include \"@_absolute@\"\n" @ONLY)
foreach(_property LANGUAGE COMPILE_DEFINITIONS COMPILE_OPTIONS COMPILE_FLAGS
INCLUDE_DIRECTORIES OBJECT_DEPENDS SKIP_PRECOMPILE_HEADERS
SKIP_UNITY_BUILD_INCLUSION)
get_source_file_property(_value "${_absolute}" "${_property}")
if(NOT _value STREQUAL "NOTFOUND")
set_source_files_properties("${_wrapper}" PROPERTIES "${_property}" "${_value}")
endif()
endforeach()
set(${output} "${_wrapper}" PARENT_SCOPE)
endfunction()
# Both source lists implement the same caller contract. Exclude these files
# from common target sources; exactly one list is linked. CONTRACT_FILES names
# adapter headers/recipes whose changes also affect the implementation identity.
function(psxrecomp_add_implementation target)
cmake_parse_arguments(I "" "NAME;CONTRACT" "HLE_SOURCES;LLE_SOURCES;CONTRACT_FILES" ${ARGN})
if(I_UNPARSED_ARGUMENTS OR NOT I_NAME MATCHES "^[a-z0-9][a-z0-9_.-]*$" OR
NOT I_CONTRACT MATCHES "^[A-Za-z0-9][A-Za-z0-9_.-]*$" OR
NOT I_HLE_SOURCES OR NOT I_LLE_SOURCES)
message(FATAL_ERROR "Implementation needs NAME, CONTRACT, HLE_SOURCES and LLE_SOURCES")
endif()
get_target_property(_profile ${target} PSX_EXECUTION_PROFILE)
if(NOT _profile)
psxrecomp_execution_profile(${target})
get_target_property(_profile ${target} PSX_EXECUTION_PROFILE)
endif()
get_target_property(_families ${target} PSX_IMPLEMENTATION_FAMILIES)
if(I_NAME IN_LIST _families)
message(FATAL_ERROR "Duplicate implementation family ${I_NAME}")
endif()
if(_profile STREQUAL "ENHANCED")
set(_selected ${I_HLE_SOURCES})
else()
set(_selected ${I_LLE_SOURCES})
endif()
set(_inputs "")
foreach(_source IN LISTS _selected I_CONTRACT_FILES)
get_filename_component(_absolute "${_source}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
if(NOT EXISTS "${_absolute}")
message(FATAL_ERROR "Implementation input does not exist: ${_absolute}")
endif()
list(APPEND _inputs "${_absolute}")
endforeach()
# CONTRACT_FILES can include common generated game bodies already added by
# runtime.cmake, not only selected replacement bodies. Normalize those
# existing source entries before declaring their configure dependency.
get_target_property(_common_sources ${target} SOURCES)
set(_common_compile_sources "")
foreach(_source IN LISTS _common_sources)
if(_source MATCHES "^\\$<")
list(APPEND _common_compile_sources "${_source}")
continue()
endif()
get_filename_component(_absolute "${_source}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
if(_absolute IN_LIST _inputs)
_psx_execution_compile_source(${target} "${_absolute}" _compiled)
list(APPEND _common_compile_sources "${_compiled}")
else()
list(APPEND _common_compile_sources "${_source}")
endif()
endforeach()
set_property(TARGET ${target} PROPERTY SOURCES "${_common_compile_sources}")
set(_compile_sources "")
foreach(_source IN LISTS _selected)
_psx_execution_compile_source(${target} "${_source}" _compiled)
list(APPEND _compile_sources "${_compiled}")
endforeach()
target_sources(${target} PRIVATE ${_compile_sources})
set_property(TARGET ${target} APPEND PROPERTY PSX_IMPLEMENTATION_FAMILIES "${I_NAME}")
set_property(TARGET ${target} PROPERTY "PSX_IMPL_${I_NAME}_CONTRACT" "${I_CONTRACT}")
set_property(TARGET ${target} PROPERTY "PSX_IMPL_${I_NAME}_SOURCES" "${_inputs}")
_psx_execution_stamp(${target})
# The runtime helper creates this optional sibling before title code adds
# implementation families. Both must receive the selected replacement TU.
if(TARGET ${target}-pgxp)
psxrecomp_add_implementation(${target}-pgxp ${ARGN})
endif()
endfunction()