diff --git a/cmake/modules/RootMacros.cmake b/cmake/modules/RootMacros.cmake index 864f5f6c81a2d..35cc386cd89ad 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}) @@ -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} @@ -741,7 +745,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} @@ -749,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. 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) {