diff --git a/cmake/modules/ROOTTargetMacros.cmake b/cmake/modules/ROOTTargetMacros.cmake index 124996c11..27a02c8e2 100644 --- a/cmake/modules/ROOTTargetMacros.cmake +++ b/cmake/modules/ROOTTargetMacros.cmake @@ -84,7 +84,9 @@ function(generate_target_and_root_library target) endif() list(APPEND headers ${habs}) get_filename_component(hName ${habs} NAME) - configure_file(${habs} "${CMAKE_BINARY_DIR}/include/${hName}") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${habs}" "${CMAKE_BINARY_DIR}/include/${hName}" + OUTPUT_QUIET ERROR_QUIET) endforeach() @@ -249,12 +251,44 @@ function(make_target_root_dictionary target) set(includeDirs $>) - # add a custom command to generate the dictionary using rootcling + # Write the list of input headers to a cmake file at configure time. + # The build-time stamp script reads this file to compute content hashes, + # avoiding semicolon/shell-escaping issues when passing long lists via -D. + set(_headersListFile ${CMAKE_CURRENT_BINARY_DIR}/${dictionary}_headers_list.cmake) + set(_headersListContent "set(DICT_HEADERS\n") + foreach(_h ${headers}) + string(APPEND _headersListContent " \"${_h}\"\n") + endforeach() + string(APPEND _headersListContent ")\n") + file(WRITE "${_headersListFile}.tmp" "${_headersListContent}") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${_headersListFile}.tmp" "${_headersListFile}" + OUTPUT_QUIET ERROR_QUIET) + file(REMOVE "${_headersListFile}.tmp") + + set(stampFile ${CMAKE_CURRENT_BINARY_DIR}/${dictionary}_headers.stamp) + + # Step 1: Update stamp only when header CONTENT changes (not just mtime). + # This prevents rootcling from rerunning after git pull or cmake reconfigure + # when header content is actually unchanged. + # cmake-format: off + add_custom_command( + OUTPUT ${stampFile} + COMMAND ${CMAKE_COMMAND} + "-DHEADERS_LIST_FILE=${_headersListFile}" + "-DSTAMP_FILE=${stampFile}" + -P "${CMAKE_SOURCE_DIR}/cmake/modules/update_dict_stamp.cmake" + DEPENDS ${headers} + VERBATIM + COMMENT "Checking header content for ${dictionary}") + # cmake-format: on + + # Step 2: Run rootcling only when stamp changes (i.e., header CONTENT changed). # cmake-format: off - set(space " ") - #message(STATUS " Adding dictionary ${dictionaryFile} to target ${target}") add_custom_command( OUTPUT ${dictionaryFile} ${pcmFile} ${rootmapFile} + BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/${pcmBase} VERBATIM COMMAND ${CMAKE_COMMAND} -E env "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:$ENV{LD_LIBRARY_PATH}" ${ROOT_rootcling_CMD} @@ -267,7 +301,7 @@ function(make_target_root_dictionary target) ${headers} COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_BINARY_DIR}/${pcmBase} ${pcmFile} COMMAND_EXPAND_LISTS - DEPENDS ${headers}) + DEPENDS ${stampFile}) # cmake-format: on # add dictionary source to the target sources and suppress warnings diff --git a/cmake/modules/update_dict_stamp.cmake b/cmake/modules/update_dict_stamp.cmake new file mode 100644 index 000000000..c26bf07b4 --- /dev/null +++ b/cmake/modules/update_dict_stamp.cmake @@ -0,0 +1,48 @@ +# Helper script invoked at build time to update a ROOT dictionary stamp file +# only when the content of the input headers has actually changed. +# +# This breaks the mtime-based dependency chain for rootcling: rather than +# rebuilding dictionaries whenever any header has a newer mtime (e.g., after +# git pull or cmake reconfigure), rootcling only reruns when header content +# actually differs. +# +# Arguments (passed via -D flags): +# HEADERS_LIST_FILE -- path to a cmake file that sets DICT_HEADERS to the +# list of input header paths +# STAMP_FILE -- path to the stamp file to (conditionally) update + +if(NOT DEFINED HEADERS_LIST_FILE OR NOT DEFINED STAMP_FILE) + message(FATAL_ERROR + "update_dict_stamp.cmake requires HEADERS_LIST_FILE and STAMP_FILE variables") +endif() + +if(NOT EXISTS "${HEADERS_LIST_FILE}") + message(FATAL_ERROR + "Headers list file missing: ${HEADERS_LIST_FILE}\n" + "Re-run cmake to regenerate it: cmake -S -B ") +endif() + +include("${HEADERS_LIST_FILE}") # sets DICT_HEADERS + +# Compute combined SHA256 hash of all input headers. +set(_combined "") +foreach(_h IN LISTS DICT_HEADERS) + if(NOT EXISTS "${_h}") + message(FATAL_ERROR "Header not found: ${_h}") + endif() + file(SHA256 "${_h}" _h_hash) + string(APPEND _combined "${_h}:${_h_hash}\n") +endforeach() +string(SHA256 _new_hash "${_combined}") +set(_new_content "${_new_hash}\n") + +# Only rewrite stamp file when the hash has changed. +# Preserving the mtime when content is unchanged prevents rootcling from +# rerunning (the dictionary custom command depends on this stamp's mtime). +set(_old_content "") +if(EXISTS "${STAMP_FILE}") + file(READ "${STAMP_FILE}" _old_content) +endif() +if(NOT _new_content STREQUAL _old_content) + file(WRITE "${STAMP_FILE}" "${_new_content}") +endif()