Skip to content

Commit 60b4ff4

Browse files
committed
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.
1 parent 4d6ff4d commit 60b4ff4

2 files changed

Lines changed: 87 additions & 5 deletions

File tree

cmake/modules/ROOTTargetMacros.cmake

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ function(generate_target_and_root_library target)
8484
endif()
8585
list(APPEND headers ${habs})
8686
get_filename_component(hName ${habs} NAME)
87-
configure_file(${habs} "${CMAKE_BINARY_DIR}/include/${hName}")
87+
execute_process(
88+
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${habs}" "${CMAKE_BINARY_DIR}/include/${hName}"
89+
OUTPUT_QUIET ERROR_QUIET)
8890
endforeach()
8991

9092

@@ -249,12 +251,44 @@ function(make_target_root_dictionary target)
249251

250252
set(includeDirs $<REMOVE_DUPLICATES:$<TARGET_PROPERTY:${target},INCLUDE_DIRECTORIES>>)
251253

252-
# add a custom command to generate the dictionary using rootcling
254+
# Write the list of input headers to a cmake file at configure time.
255+
# The build-time stamp script reads this file to compute content hashes,
256+
# avoiding semicolon/shell-escaping issues when passing long lists via -D.
257+
set(_headersListFile ${CMAKE_CURRENT_BINARY_DIR}/${dictionary}_headers_list.cmake)
258+
set(_headersListContent "set(DICT_HEADERS\n")
259+
foreach(_h ${headers})
260+
string(APPEND _headersListContent " \"${_h}\"\n")
261+
endforeach()
262+
string(APPEND _headersListContent ")\n")
263+
file(WRITE "${_headersListFile}.tmp" "${_headersListContent}")
264+
execute_process(
265+
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
266+
"${_headersListFile}.tmp" "${_headersListFile}"
267+
OUTPUT_QUIET ERROR_QUIET)
268+
file(REMOVE "${_headersListFile}.tmp")
269+
270+
set(stampFile ${CMAKE_CURRENT_BINARY_DIR}/${dictionary}_headers.stamp)
271+
272+
# Step 1: Update stamp only when header CONTENT changes (not just mtime).
273+
# This prevents rootcling from rerunning after git pull or cmake reconfigure
274+
# when header content is actually unchanged.
275+
# cmake-format: off
276+
add_custom_command(
277+
OUTPUT ${stampFile}
278+
COMMAND ${CMAKE_COMMAND}
279+
"-DHEADERS_LIST_FILE=${_headersListFile}"
280+
"-DSTAMP_FILE=${stampFile}"
281+
-P "${CMAKE_SOURCE_DIR}/cmake/modules/update_dict_stamp.cmake"
282+
DEPENDS ${headers}
283+
VERBATIM
284+
COMMENT "Checking header content for ${dictionary}")
285+
# cmake-format: on
286+
287+
# Step 2: Run rootcling only when stamp changes (i.e., header CONTENT changed).
253288
# cmake-format: off
254-
set(space " ")
255-
#message(STATUS " Adding dictionary ${dictionaryFile} to target ${target}")
256289
add_custom_command(
257290
OUTPUT ${dictionaryFile} ${pcmFile} ${rootmapFile}
291+
BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/${pcmBase}
258292
VERBATIM
259293
COMMAND ${CMAKE_COMMAND} -E env "LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:$ENV{LD_LIBRARY_PATH}"
260294
${ROOT_rootcling_CMD}
@@ -267,7 +301,7 @@ function(make_target_root_dictionary target)
267301
${headers}
268302
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_BINARY_DIR}/${pcmBase} ${pcmFile}
269303
COMMAND_EXPAND_LISTS
270-
DEPENDS ${headers})
304+
DEPENDS ${stampFile})
271305
# cmake-format: on
272306

273307
# add dictionary source to the target sources and suppress warnings
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Helper script invoked at build time to update a ROOT dictionary stamp file
2+
# only when the content of the input headers has actually changed.
3+
#
4+
# This breaks the mtime-based dependency chain for rootcling: rather than
5+
# rebuilding dictionaries whenever any header has a newer mtime (e.g., after
6+
# git pull or cmake reconfigure), rootcling only reruns when header content
7+
# actually differs.
8+
#
9+
# Arguments (passed via -D flags):
10+
# HEADERS_LIST_FILE -- path to a cmake file that sets DICT_HEADERS to the
11+
# list of input header paths
12+
# STAMP_FILE -- path to the stamp file to (conditionally) update
13+
14+
if(NOT DEFINED HEADERS_LIST_FILE OR NOT DEFINED STAMP_FILE)
15+
message(FATAL_ERROR
16+
"update_dict_stamp.cmake requires HEADERS_LIST_FILE and STAMP_FILE variables")
17+
endif()
18+
19+
if(NOT EXISTS "${HEADERS_LIST_FILE}")
20+
message(FATAL_ERROR
21+
"Headers list file missing: ${HEADERS_LIST_FILE}\n"
22+
"Re-run cmake to regenerate it: cmake -S <src> -B <build>")
23+
endif()
24+
25+
include("${HEADERS_LIST_FILE}") # sets DICT_HEADERS
26+
27+
# Compute combined SHA256 hash of all input headers.
28+
set(_combined "")
29+
foreach(_h IN LISTS DICT_HEADERS)
30+
if(NOT EXISTS "${_h}")
31+
message(FATAL_ERROR "Header not found: ${_h}")
32+
endif()
33+
file(SHA256 "${_h}" _h_hash)
34+
string(APPEND _combined "${_h}:${_h_hash}\n")
35+
endforeach()
36+
string(SHA256 _new_hash "${_combined}")
37+
set(_new_content "${_new_hash}\n")
38+
39+
# Only rewrite stamp file when the hash has changed.
40+
# Preserving the mtime when content is unchanged prevents rootcling from
41+
# rerunning (the dictionary custom command depends on this stamp's mtime).
42+
set(_old_content "")
43+
if(EXISTS "${STAMP_FILE}")
44+
file(READ "${STAMP_FILE}" _old_content)
45+
endif()
46+
if(NOT _new_content STREQUAL _old_content)
47+
file(WRITE "${STAMP_FILE}" "${_new_content}")
48+
endif()

0 commit comments

Comments
 (0)