From 014ceba199678151ca46b56dc29880726ec0d23b Mon Sep 17 00:00:00 2001 From: DhanashreePetare Date: Wed, 29 Jul 2026 06:32:43 +0000 Subject: [PATCH 1/2] [CMake] Use DEPFILE instead of IMPLICIT_DEPENDS for dictionaries Re-applying commit ae4d830f71e after revert in 94caecfbbf8. Closes #21203. --- cmake/modules/RootMacros.cmake | 11 ++-- core/dictgen/src/rootcling_impl.cxx | 96 +++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/cmake/modules/RootMacros.cmake b/cmake/modules/RootMacros.cmake index 864f5f6c81a2d..ca87a42186965 100644 --- a/cmake/modules/RootMacros.cmake +++ b/cmake/modules/RootMacros.cmake @@ -685,10 +685,10 @@ function(ROOT_GENERATE_DICTIONARY dictionary) endforeach() #---build the implicit dependencies arguments - # NOTE: only the Makefile generator respects this! - foreach(_dep ${_linkdef} ${_list_of_header_dependencies}) - list(APPEND _implicitdeps CXX ${_dep}) - endforeach() + # NOTE: DEPFILE is used instead of IMPLICIT_DEPENDS because IMPLICIT_DEPENDS + # only works with Unix Makefiles and has issues with cross-directory dependencies. + # DEPFILE works with all generators (Ninja, Unix Makefiles, etc.) + set(depfile_path ${CMAKE_CURRENT_BINARY_DIR}/${dictionary}.depfile) if(ARG_MODULE) set(MODULE_LIB_DEPENDENCY ${ARG_DEPENDENCIES}) @@ -741,7 +741,8 @@ function(ROOT_GENERATE_DICTIONARY dictionary) # make the dictionary generation command depend on the C++ standard, ensuring that the # dictionaries will be rebuilt if the C++ standard is changed in an incremental build. -DR__DUMMY_CXX_STANDARD_${CMAKE_CXX_STANDARD} - IMPLICIT_DEPENDS ${_implicitdeps} + -MF ${depfile_path} + DEPFILE ${depfile_path} DEPENDS ${_list_of_header_dependencies} ${_linkdef} ${ROOTCLINGDEP} ${pcm_dependencies} ${MODULE_LIB_DEPENDENCY} ${ARG_EXTRA_DEPENDENCIES} diff --git a/core/dictgen/src/rootcling_impl.cxx b/core/dictgen/src/rootcling_impl.cxx index caf295b27f703..d2ae3ec534233 100644 --- a/core/dictgen/src/rootcling_impl.cxx +++ b/core/dictgen/src/rootcling_impl.cxx @@ -3709,6 +3709,10 @@ static llvm::cl::list gOptWDiags("W", llvm::cl::Prefix, llvm::cl::ZeroOrMore, llvm::cl::desc("Specify compiler diagnostics options."), llvm::cl::cat(gRootclingOptions)); +static llvm::cl::opt +gOptDepFile("MF", + llvm::cl::desc("Write dependency output to the specified file."), + llvm::cl::cat(gRootclingOptions)); // Really OneOrMore, will be changed in RootClingMain below. static llvm::cl::list gOptDictionaryHeaderFiles(llvm::cl::Positional, llvm::cl::ZeroOrMore, @@ -4517,6 +4521,11 @@ int RootClingMain(int argc, // Check if code goes to stdout or rootcling file std::ofstream fileout; string main_dictname(gOptDictionaryFileName.getValue()); + // Keep the original dictionary output file name (with extension) for the + // dependency file target: `main_dictname` gets its extension stripped below + // and `gOptDictionaryFileName` is turned into a temporary name by the + // tmpCatalog a few lines down. + const std::string dictOutputFileName(gOptDictionaryFileName.getValue()); std::ostream *splitDictStream = nullptr; std::unique_ptr splitDeleter(nullptr); // Store the temp files @@ -4999,6 +5008,93 @@ int RootClingMain(int argc, // make sure the file is closed before committing fileout.close(); + // Write the dependency file if requested (-MF ). It uses the + // Makefile format understood by CMake's DEPFILE and Ninja's "deps = gcc", + // listing every real header that was opened while generating the dictionary + // so that incremental builds pick up changes to transitively included files. + if (!gOptDepFile.empty() && rootclingRetCode == 0 && !dictOutputFileName.empty()) { + std::ofstream depFile(gOptDepFile.c_str()); + if (!depFile) { + ROOT::TMetaUtils::Error(nullptr, + "rootcling: failed to open dependency file %s\n", + gOptDepFile.c_str()); + rootclingRetCode = 1; + } else { + // Escape a path for the Makefile-format dependency file: forward + // slashes (needed on Windows) and backslash-escape the characters that + // are special to make (space, tab, '#', ':'). + auto escapeForDepFile = [](std::string path) { + std::replace(path.begin(), path.end(), '\\', '/'); + std::string escaped; + escaped.reserve(path.size()); + for (char c : path) { + if (c == ' ' || c == '\t' || c == '#' || c == ':') + escaped += '\\'; + escaped += c; + } + return escaped; + }; + + // The target is the final dictionary source file. Note that + // gOptDictionaryFileName has been turned into a temporary name by the + // tmpCatalog, so we use the original name captured earlier. + depFile << escapeForDepFile(dictOutputFileName) << ":"; + + // Collect all files that were read by clang during dictionary + // generation (headers included directly or indirectly). + clang::SourceManager &SM = CI->getSourceManager(); + clang::FileManager &FM = SM.getFileManager(); + + llvm::SmallVector files; + FM.GetUniqueIDMapping(files); + + std::set includedFiles; + for (const auto &FEOpt : files) { + if (!FEOpt) + continue; + llvm::StringRef filename = FEOpt->getName(); + if (filename.empty()) + continue; + // Skip cling's in-memory buffers, which the FileManager also + // reports: "input_line_N", "<<< cling interactive line includer >>>", + // "", "", ... These are not real files; + // some contain spaces or angle brackets that would corrupt the + // dependency file, and all of them would make the dictionary appear + // perpetually out of date. Requiring the entry to exist on disk + // filters them out (together with the explicit angle-bracket check). + if (filename.contains('<') || filename.contains('>')) + continue; + // Make the path absolute so it is unambiguous regardless of the + // working directory: rootcling may run from a different directory + // than the one the dependency file is later consumed from (with + // CMP0116 OLD the depfile is not rewritten, and a relative entry + // like "./Foo.hxx" would be resolved against the wrong base and + // leave the dictionary permanently out of date). + llvm::SmallString<256> absPath(filename); + llvm::sys::fs::make_absolute(absPath); + if (!llvm::sys::fs::exists(absPath)) + continue; + std::string filenameStr(absPath.str()); + // Skip the output dictionary file itself (final or temporary name). + if (filenameStr == dictOutputFileName || filenameStr == gOptDictionaryFileName.getValue()) + continue; + includedFiles.insert(std::move(filenameStr)); + } + + // Each dependency line except the last ends with a backslash. + for (const auto &file : includedFiles) + depFile << " \\\n " << escapeForDepFile(file); + if (!includedFiles.empty()) + depFile << "\n"; + + depFile.close(); + if (!depFile.good()) { + ROOT::TMetaUtils::Error(nullptr, "rootcling: failed to write dependency file %s\n", gOptDepFile.c_str()); + rootclingRetCode = 1; + } + } + } + // Before returning, rename the files if no errors occurred // otherwise clean them to avoid remnants (see ROOT-10015) if(rootclingRetCode == 0) { From 3c63d0385b6a18a33c58cb40998aa6e1978ebce5 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Tue, 4 Aug 2026 15:08:53 +0200 Subject: [PATCH 2/2] [CMake] Fix dictionary depfile target under CMP0116 OLD Follows up on ae4d830f71e017, which switched ROOT_GENERATE_DICTIONARY() from IMPLICIT_DEPENDS to a proper DEPFILE, but rebuilt every dictionary on every incremental build with older Ninja (e.g. 1.8.2 on Alma 8): ```txt ninja: expected depfile '.../io/io/G__RIO.depfile' to mention 'io/io/G__RIO.cxx', got 'G__RIO.cxx' ``` rootcling writes the depfile target as the plain dictionary name it is given (`"G__RIO.cxx"`) relative to CMAKE_CURRENT_BINARY_DIR. ROOT pins `CMP0116` to OLD globally (see 7392b023d10), and under OLD the Ninja generator consumes the depfile verbatim and expects the target relative to the top-level build directory (`"io/io/G__RIO.cxx"`). The mismatch makes older Ninja treat the dictionary as perpetually out of date; newer Ninja tolerates it, which is why the regression only showed up on some platforms. This commit suggests to enable CMP0116 NEW locally around the dictionary `add_custom_command` so CMake transforms the depfile to whatever the active generator needs. --- cmake/modules/RootMacros.cmake | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmake/modules/RootMacros.cmake b/cmake/modules/RootMacros.cmake index ca87a42186965..35cc386cd89ad 100644 --- a/cmake/modules/RootMacros.cmake +++ b/cmake/modules/RootMacros.cmake @@ -728,6 +728,10 @@ function(ROOT_GENERATE_DICTIONARY dictionary) endif() #---call rootcling------------------------------------------ + # use CMP0116 NEW locally so CMake normalises the depfile + # target for the active generator regardless of the global OLD setting. + cmake_policy(PUSH) + cmake_policy(SET CMP0116 NEW) add_custom_command( OUTPUT ${dictionary}.cxx ${pcm_name} ${rootmap_name} ${cpp_module_file} COMMAND ${command} -v2 -f ${dictionary}.cxx ${newargs} ${excludepathsargs} ${rootmapargs} @@ -750,6 +754,7 @@ function(ROOT_GENERATE_DICTIONARY dictionary) ${cxx_std_stamp} COMMAND_EXPAND_LISTS ) + cmake_policy(POP) # If we are adding to an existing target and it's not the dictionary itself, # we make an object library and add its output object file as source to the target.