From 60b4ff4165ea8dbfd9b4565fe83336d711be2a5d Mon Sep 17 00:00:00 2001 From: Adam Anthony Date: Thu, 2 Apr 2026 13:19:18 -0400 Subject: [PATCH] Only regenerate ROOT dictionaries when needed Previously, any change to any header would cause all ROOT dictionaries to be regenerated which can slow down compile time significantly. In addition any time cmake was reconfigured, even if not files were changed all the dictionaries would be regenerated. This commit adds a stamp file to each dictionary which is updated with the last modification time of the headers used to generate the dictionary. It uses hashs to determine the last update time. --- cmake/modules/ROOTTargetMacros.cmake | 44 +++++++++++++++++++++--- cmake/modules/update_dict_stamp.cmake | 48 +++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 cmake/modules/update_dict_stamp.cmake 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()