/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or https://cmake.org/licensing for details. */ #include "cmGraphVizWriter.h" #include #include #include // IWYU pragma: keep #include #include #include "cmGeneratedFileStream.h" #include "cmGeneratorTarget.h" #include "cmGlobalGenerator.h" #include "cmLocalGenerator.h" #include "cmMakefile.h" #include "cmStateSnapshot.h" #include "cmSystemTools.h" #include "cmTarget.h" #include "cmake.h" namespace { enum LinkLibraryScopeType { LLT_SCOPE_PUBLIC, LLT_SCOPE_PRIVATE, LLT_SCOPE_INTERFACE }; const char* const GRAPHVIZ_PRIVATE_EDEGE_STYLE = "dashed"; const char* const GRAPHVIZ_INTERFACE_EDEGE_STYLE = "dotted"; std::string getLinkLibraryStyle(const LinkLibraryScopeType& type) { std::string style; switch (type) { case LLT_SCOPE_PRIVATE: style = "[style = " + std::string(GRAPHVIZ_PRIVATE_EDEGE_STYLE) + "]"; break; case LLT_SCOPE_INTERFACE: style = "[style = " + std::string(GRAPHVIZ_INTERFACE_EDEGE_STYLE) + "]"; break; default: break; } return style; } const char* getShapeForTarget(const cmGeneratorTarget* target) { if (!target) { return "ellipse"; } switch (target->GetType()) { case cmStateEnums::EXECUTABLE: return "house"; case cmStateEnums::STATIC_LIBRARY: return "diamond"; case cmStateEnums::SHARED_LIBRARY: return "polygon"; case cmStateEnums::MODULE_LIBRARY: return "octagon"; default: break; } return "box"; } std::map getScopedLinkLibrariesFromTarget( cmTarget* Target, const cmGlobalGenerator* globalGenerator) { char sep = ';'; std::map tokens; size_t start = 0, end = 0; const char* pInterfaceLinkLibraries = Target->GetProperty("INTERFACE_LINK_LIBRARIES"); const char* pLinkLibraries = Target->GetProperty("LINK_LIBRARIES"); if (!pInterfaceLinkLibraries && !pLinkLibraries) { return tokens; // target is not linked against any other libraries } // make sure we don't touch a null-ptr auto interfaceLinkLibraries = std::string(pInterfaceLinkLibraries ? pInterfaceLinkLibraries : ""); auto linkLibraries = std::string(pLinkLibraries ? pLinkLibraries : ""); // first extract interfaceLinkLibraries while (start < interfaceLinkLibraries.length()) { if ((end = interfaceLinkLibraries.find(sep, start)) == std::string::npos) { end = interfaceLinkLibraries.length(); } std::string element = interfaceLinkLibraries.substr(start, end - start); if (globalGenerator->IsAlias(element)) { const auto tgt = globalGenerator->FindTarget(element); if (tgt) { element = tgt->GetName(); } } if (std::string::npos == element.find("$IsAlias(element)) { const auto tgt = globalGenerator->FindTarget(element); if (tgt) { element = tgt->GetName(); } } if (tokens.find(element) == tokens.end()) { // this library is not found in interfaceLinkLibraries but in // linkLibraries. // this results in a private linked library. tokens[element] = LLT_SCOPE_PRIVATE; } else if (LLT_SCOPE_INTERFACE == tokens[element]) { // this library is found in interfaceLinkLibraries and linkLibraries. // this results in a public linked library. tokens[element] = LLT_SCOPE_PUBLIC; } else { // private and public linked libraries should not be changed anymore. } start = end + 1; } return tokens; } } cmGraphVizWriter::cmGraphVizWriter(const cmGlobalGenerator* globalGenerator) : GraphType("digraph") , GraphName("GG") , GraphHeader("node [\n fontsize = \"12\"\n];") , GraphNodePrefix("node") , GlobalGenerator(globalGenerator) , LocalGenerators(globalGenerator->GetLocalGenerators()) , GenerateForExecutables(true) , GenerateForStaticLibs(true) , GenerateForSharedLibs(true) , GenerateForModuleLibs(true) , GenerateForInterface(true) , GenerateForExternals(true) , GeneratePerTarget(true) , GenerateDependers(true) , HaveTargetsAndLibs(false) { } void cmGraphVizWriter::ReadSettings(const char* settingsFileName, const char* fallbackSettingsFileName) { cmake cm(cmake::RoleScript); cm.SetHomeDirectory(""); cm.SetHomeOutputDirectory(""); cm.GetCurrentSnapshot().SetDefaultDefinitions(); cmGlobalGenerator ggi(&cm); cmMakefile mf(&ggi, cm.GetCurrentSnapshot()); std::unique_ptr lg(ggi.CreateLocalGenerator(&mf)); const char* inFileName = settingsFileName; if (!cmSystemTools::FileExists(inFileName)) { inFileName = fallbackSettingsFileName; if (!cmSystemTools::FileExists(inFileName)) { return; } } if (!mf.ReadListFile(inFileName)) { cmSystemTools::Error("Problem opening GraphViz options file: ", inFileName); return; } std::cout << "Reading GraphViz options file: " << inFileName << std::endl; #define __set_if_set(var, cmakeDefinition) \ { \ const char* value = mf.GetDefinition(cmakeDefinition); \ if (value) { \ (var) = value; \ } \ } __set_if_set(this->GraphType, "GRAPHVIZ_GRAPH_TYPE"); __set_if_set(this->GraphName, "GRAPHVIZ_GRAPH_NAME"); __set_if_set(this->GraphHeader, "GRAPHVIZ_GRAPH_HEADER"); __set_if_set(this->GraphNodePrefix, "GRAPHVIZ_NODE_PREFIX"); #define __set_bool_if_set(var, cmakeDefinition) \ { \ const char* value = mf.GetDefinition(cmakeDefinition); \ if (value) { \ (var) = mf.IsOn(cmakeDefinition); \ } \ } __set_bool_if_set(this->GenerateForExecutables, "GRAPHVIZ_EXECUTABLES"); __set_bool_if_set(this->GenerateForStaticLibs, "GRAPHVIZ_STATIC_LIBS"); __set_bool_if_set(this->GenerateForSharedLibs, "GRAPHVIZ_SHARED_LIBS"); __set_bool_if_set(this->GenerateForModuleLibs, "GRAPHVIZ_MODULE_LIBS"); __set_bool_if_set(this->GenerateForInterface, "GRAPHVIZ_INTERFACE"); __set_bool_if_set(this->GenerateForExternals, "GRAPHVIZ_EXTERNAL_LIBS"); __set_bool_if_set(this->GeneratePerTarget, "GRAPHVIZ_GENERATE_PER_TARGET"); __set_bool_if_set(this->GenerateDependers, "GRAPHVIZ_GENERATE_DEPENDERS"); std::string ignoreTargetsRegexes; __set_if_set(ignoreTargetsRegexes, "GRAPHVIZ_IGNORE_TARGETS"); this->TargetsToIgnoreRegex.clear(); if (!ignoreTargetsRegexes.empty()) { std::vector ignoreTargetsRegExVector; cmSystemTools::ExpandListArgument(ignoreTargetsRegexes, ignoreTargetsRegExVector); for (std::string const& currentRegexString : ignoreTargetsRegExVector) { cmsys::RegularExpression currentRegex; if (!currentRegex.compile(currentRegexString)) { std::cerr << "Could not compile bad regex \"" << currentRegexString << "\"" << std::endl; } this->TargetsToIgnoreRegex.push_back(std::move(currentRegex)); } } } // Iterate over all targets and write for each one a graph which shows // which other targets depend on it. void cmGraphVizWriter::WriteTargetDependersFiles(const char* fileName) { if (!this->GenerateDependers) { return; } this->CollectTargetsAndLibs(); for (auto const& ptr : this->TargetPtrs) { if (ptr.second == nullptr) { continue; } if (!this->GenerateForTargetType(ptr.second->GetType())) { continue; } std::string currentFilename = fileName; currentFilename += "."; currentFilename += ptr.first; currentFilename += ".dependers"; cmGeneratedFileStream str(currentFilename); if (!str) { return; } std::set insertedConnections; std::set insertedNodes; std::cout << "Writing " << currentFilename << "..." << std::endl; this->WriteHeader(str); this->WriteDependerConnections(ptr.first, insertedNodes, insertedConnections, str); this->WriteFooter(str); } } // Iterate over all targets and write for each one a graph which shows // on which targets it depends. void cmGraphVizWriter::WritePerTargetFiles(const char* fileName) { if (!this->GeneratePerTarget) { return; } this->CollectTargetsAndLibs(); for (auto const& ptr : this->TargetPtrs) { if (ptr.second == nullptr) { continue; } if (!this->GenerateForTargetType(ptr.second->GetType())) { continue; } std::set insertedConnections; std::set insertedNodes; std::string currentFilename = fileName; currentFilename += "."; currentFilename += ptr.first; cmGeneratedFileStream str(currentFilename); if (!str) { return; } std::cout << "Writing " << currentFilename << "..." << std::endl; this->WriteHeader(str); this->WriteConnections(ptr.first, insertedNodes, insertedConnections, str); this->WriteFooter(str); } } void cmGraphVizWriter::WriteGlobalFile(const char* fileName) { this->CollectTargetsAndLibs(); cmGeneratedFileStream str(fileName); if (!str) { return; } this->WriteHeader(str); std::cout << "Writing " << fileName << "..." << std::endl; std::set insertedConnections; std::set insertedNodes; for (auto const& ptr : this->TargetPtrs) { if (ptr.second == nullptr) { continue; } if (!this->GenerateForTargetType(ptr.second->GetType())) { continue; } this->WriteConnections(ptr.first, insertedNodes, insertedConnections, str); } this->WriteFooter(str); } void cmGraphVizWriter::WriteHeader(cmGeneratedFileStream& str) const { str << this->GraphType << " \"" << this->GraphName << "\" {" << std::endl; str << this->GraphHeader << std::endl; } void cmGraphVizWriter::WriteFooter(cmGeneratedFileStream& str) const { str << "}" << std::endl; } void cmGraphVizWriter::WriteConnections( const std::string& targetName, std::set& insertedNodes, std::set& insertedConnections, cmGeneratedFileStream& str) const { std::map::const_iterator targetPtrIt = this->TargetPtrs.find(targetName); if (targetPtrIt == this->TargetPtrs.end()) // not found at all { return; } this->WriteNode(targetName, targetPtrIt->second, insertedNodes, str); if (targetPtrIt->second == nullptr) // it's an external library { return; } std::string myNodeName = this->TargetNamesNodes.find(targetName)->second; std::map ll = getScopedLinkLibrariesFromTarget(targetPtrIt->second->Target, GlobalGenerator); for (auto const& llit : ll) { const char* libName = llit.first.c_str(); std::map::const_iterator libNameIt = this->TargetNamesNodes.find(libName); // can happen e.g. if GRAPHVIZ_TARGET_IGNORE_REGEX is used if (libNameIt == this->TargetNamesNodes.end()) { continue; } std::string connectionName = myNodeName; connectionName += "-"; connectionName += libNameIt->second; if (insertedConnections.find(connectionName) == insertedConnections.end()) { insertedConnections.insert(connectionName); this->WriteNode(libName, this->TargetPtrs.find(libName)->second, insertedNodes, str); str << " \"" << myNodeName << "\" -> \"" << libNameIt->second << "\""; str << getLinkLibraryStyle(llit.second); str << " // " << targetName << " -> " << libName << std::endl; this->WriteConnections(libName, insertedNodes, insertedConnections, str); } } } void cmGraphVizWriter::WriteDependerConnections( const std::string& targetName, std::set& insertedNodes, std::set& insertedConnections, cmGeneratedFileStream& str) const { std::map::const_iterator targetPtrIt = this->TargetPtrs.find(targetName); if (targetPtrIt == this->TargetPtrs.end()) // not found at all { return; } this->WriteNode(targetName, targetPtrIt->second, insertedNodes, str); if (targetPtrIt->second == nullptr) // it's an external library { return; } std::string myNodeName = this->TargetNamesNodes.find(targetName)->second; // now search who links against me for (auto const& tptr : this->TargetPtrs) { if (tptr.second == nullptr) { continue; } if (!this->GenerateForTargetType(tptr.second->GetType())) { continue; } // Now we have a target, check whether it links against targetName. // If so, draw a connection, and then continue with dependers on that one. std::map ll = getScopedLinkLibrariesFromTarget(tptr.second->Target, GlobalGenerator); for (auto const& llit : ll) { if (llit.first == targetName) { // So this target links against targetName. std::map::const_iterator dependerNodeNameIt = this->TargetNamesNodes.find(tptr.first); if (dependerNodeNameIt != this->TargetNamesNodes.end()) { std::string connectionName = dependerNodeNameIt->second; connectionName += "-"; connectionName += myNodeName; if (insertedConnections.find(connectionName) == insertedConnections.end()) { insertedConnections.insert(connectionName); this->WriteNode(tptr.first, tptr.second, insertedNodes, str); str << " \"" << dependerNodeNameIt->second << "\" -> \"" << myNodeName << "\""; str << " // " << targetName << " -> " << tptr.first << std::endl; str << getLinkLibraryStyle(llit.second); this->WriteDependerConnections(tptr.first, insertedNodes, insertedConnections, str); } } break; } } } } void cmGraphVizWriter::WriteNode(const std::string& targetName, const cmGeneratorTarget* target, std::set& insertedNodes, cmGeneratedFileStream& str) const { if (insertedNodes.find(targetName) == insertedNodes.end()) { insertedNodes.insert(targetName); std::map::const_iterator nameIt = this->TargetNamesNodes.find(targetName); str << " \"" << nameIt->second << "\" [ label=\"" << targetName << "\" shape=\"" << getShapeForTarget(target) << "\"];" << std::endl; } } void cmGraphVizWriter::CollectTargetsAndLibs() { if (!this->HaveTargetsAndLibs) { this->HaveTargetsAndLibs = true; int cnt = this->CollectAllTargets(); if (this->GenerateForExternals) { this->CollectAllExternalLibs(cnt); } } } int cmGraphVizWriter::CollectAllTargets() { int cnt = 0; // First pass get the list of all cmake targets for (cmLocalGenerator* lg : this->LocalGenerators) { const std::vector& targets = lg->GetGeneratorTargets(); for (cmGeneratorTarget* target : targets) { const char* realTargetName = target->GetName().c_str(); if (this->IgnoreThisTarget(realTargetName)) { // Skip ignored targets continue; } // std::cout << "Found target: " << tit->first << std::endl; std::ostringstream ostr; ostr << this->GraphNodePrefix << cnt++; this->TargetNamesNodes[realTargetName] = ostr.str(); this->TargetPtrs[realTargetName] = target; } } return cnt; } int cmGraphVizWriter::CollectAllExternalLibs(int cnt) { // Ok, now find all the stuff we link to that is not in cmake for (cmLocalGenerator* lg : this->LocalGenerators) { const std::vector& targets = lg->GetGeneratorTargets(); for (cmGeneratorTarget* target : targets) { const char* realTargetName = target->GetName().c_str(); if (this->IgnoreThisTarget(realTargetName)) { // Skip ignored targets continue; } const cmTarget::LinkLibraryVectorType* ll = &(target->Target->GetOriginalLinkLibraries()); for (auto const& llit : *ll) { const char* libName = llit.first.c_str(); if (this->IgnoreThisTarget(libName)) { // Skip ignored targets continue; } if (GlobalGenerator->IsAlias(libName)) { const auto tgt = GlobalGenerator->FindTarget(libName); if (tgt) { libName = tgt->GetName().c_str(); } } std::map::const_iterator tarIt = this->TargetPtrs.find(libName); if (tarIt == this->TargetPtrs.end()) { std::ostringstream ostr; ostr << this->GraphNodePrefix << cnt++; this->TargetNamesNodes[libName] = ostr.str(); this->TargetPtrs[libName] = nullptr; // str << " \"" << ostr << "\" [ label=\"" << libName // << "\" shape=\"ellipse\"];" << std::endl; } } } } return cnt; } bool cmGraphVizWriter::IgnoreThisTarget(const std::string& name) { for (cmsys::RegularExpression& regEx : this->TargetsToIgnoreRegex) { if (regEx.is_valid()) { if (regEx.find(name)) { return true; } } } return false; } bool cmGraphVizWriter::GenerateForTargetType( cmStateEnums::TargetType targetType) const { switch (targetType) { case cmStateEnums::EXECUTABLE: return this->GenerateForExecutables; case cmStateEnums::STATIC_LIBRARY: return this->GenerateForStaticLibs; case cmStateEnums::SHARED_LIBRARY: return this->GenerateForSharedLibs; case cmStateEnums::MODULE_LIBRARY: return this->GenerateForModuleLibs; case cmStateEnums::INTERFACE_LIBRARY: return this->GenerateForInterface; default: break; } return false; }