| /* 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 <algorithm> |
| #include <cctype> |
| #include <iostream> |
| #include <memory> |
| #include <set> |
| #include <utility> |
| |
| #include <cm/memory> |
| |
| #include "cmGeneratedFileStream.h" |
| #include "cmGeneratorTarget.h" |
| #include "cmGlobalGenerator.h" |
| #include "cmLinkItem.h" |
| #include "cmLocalGenerator.h" |
| #include "cmMakefile.h" |
| #include "cmState.h" |
| #include "cmStateSnapshot.h" |
| #include "cmStringAlgorithms.h" |
| #include "cmSystemTools.h" |
| #include "cmValue.h" |
| #include "cmake.h" |
| |
| namespace { |
| |
| char const* const GRAPHVIZ_EDGE_STYLE_PUBLIC = "solid"; |
| char const* const GRAPHVIZ_EDGE_STYLE_INTERFACE = "dashed"; |
| char const* const GRAPHVIZ_EDGE_STYLE_PRIVATE = "dotted"; |
| |
| char const* const GRAPHVIZ_NODE_SHAPE_EXECUTABLE = "egg"; // egg-xecutable |
| |
| // Normal libraries. |
| char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_STATIC = "octagon"; |
| char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_SHARED = "doubleoctagon"; |
| char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_MODULE = "tripleoctagon"; |
| |
| char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_INTERFACE = "pentagon"; |
| char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_OBJECT = "hexagon"; |
| char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN = "septagon"; |
| |
| char const* const GRAPHVIZ_NODE_SHAPE_UTILITY = "box"; |
| |
| const char* getShapeForTarget(const cmLinkItem& item) |
| { |
| if (item.Target == nullptr) { |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN; |
| } |
| |
| switch (item.Target->GetType()) { |
| case cmStateEnums::EXECUTABLE: |
| return GRAPHVIZ_NODE_SHAPE_EXECUTABLE; |
| case cmStateEnums::STATIC_LIBRARY: |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_STATIC; |
| case cmStateEnums::SHARED_LIBRARY: |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_SHARED; |
| case cmStateEnums::MODULE_LIBRARY: |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_MODULE; |
| case cmStateEnums::OBJECT_LIBRARY: |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_OBJECT; |
| case cmStateEnums::UTILITY: |
| return GRAPHVIZ_NODE_SHAPE_UTILITY; |
| case cmStateEnums::INTERFACE_LIBRARY: |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_INTERFACE; |
| case cmStateEnums::UNKNOWN_LIBRARY: |
| default: |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN; |
| } |
| } |
| |
| struct DependeesDir |
| { |
| template <typename T> |
| static const cmLinkItem& src(const T& con) |
| { |
| return con.src; |
| } |
| |
| template <typename T> |
| static const cmLinkItem& dst(const T& con) |
| { |
| return con.dst; |
| } |
| }; |
| |
| struct DependersDir |
| { |
| template <typename T> |
| static const cmLinkItem& src(const T& con) |
| { |
| return con.dst; |
| } |
| |
| template <typename T> |
| static const cmLinkItem& dst(const T& con) |
| { |
| return con.src; |
| } |
| }; |
| } |
| |
| cmGraphVizWriter::cmGraphVizWriter(std::string const& fileName, |
| const cmGlobalGenerator* globalGenerator) |
| : FileName(fileName) |
| , GlobalFileStream(fileName) |
| , GraphName(globalGenerator->GetSafeGlobalSetting("CMAKE_PROJECT_NAME")) |
| , GraphHeader("node [\n fontsize = \"12\"\n];") |
| , GraphNodePrefix("node") |
| , GlobalGenerator(globalGenerator) |
| { |
| } |
| |
| cmGraphVizWriter::~cmGraphVizWriter() |
| { |
| this->WriteFooter(this->GlobalFileStream); |
| } |
| |
| void cmGraphVizWriter::VisitGraph(std::string const&) |
| { |
| this->WriteHeader(this->GlobalFileStream, this->GraphName); |
| this->WriteLegend(this->GlobalFileStream); |
| } |
| |
| void cmGraphVizWriter::OnItem(cmLinkItem const& item) |
| { |
| if (this->ItemExcluded(item)) { |
| return; |
| } |
| |
| this->NodeNames[item.AsStr()] = |
| cmStrCat(this->GraphNodePrefix, this->NextNodeId); |
| ++this->NextNodeId; |
| |
| this->WriteNode(this->GlobalFileStream, item); |
| } |
| |
| std::unique_ptr<cmGeneratedFileStream> cmGraphVizWriter::CreateTargetFile( |
| cmLinkItem const& item, std::string const& fileNameSuffix) |
| { |
| auto const pathSafeItemName = PathSafeString(item.AsStr()); |
| auto const perTargetFileName = |
| cmStrCat(this->FileName, '.', pathSafeItemName, fileNameSuffix); |
| auto perTargetFileStream = |
| cm::make_unique<cmGeneratedFileStream>(perTargetFileName); |
| |
| this->WriteHeader(*perTargetFileStream, item.AsStr()); |
| this->WriteNode(*perTargetFileStream, item); |
| |
| return perTargetFileStream; |
| } |
| |
| void cmGraphVizWriter::OnDirectLink(cmLinkItem const& depender, |
| cmLinkItem const& dependee, |
| DependencyType dt) |
| { |
| this->VisitLink(depender, dependee, true, GetEdgeStyle(dt)); |
| } |
| |
| void cmGraphVizWriter::OnIndirectLink(cmLinkItem const& depender, |
| cmLinkItem const& dependee) |
| { |
| this->VisitLink(depender, dependee, false); |
| } |
| |
| void cmGraphVizWriter::VisitLink(cmLinkItem const& depender, |
| cmLinkItem const& dependee, bool isDirectLink, |
| std::string const& scopeType) |
| { |
| if (this->ItemExcluded(depender) || this->ItemExcluded(dependee)) { |
| return; |
| } |
| |
| if (!isDirectLink) { |
| return; |
| } |
| |
| // write global data directly |
| this->WriteConnection(this->GlobalFileStream, depender, dependee, scopeType); |
| |
| if (this->GeneratePerTarget) { |
| this->PerTargetConnections[depender].emplace_back(depender, dependee, |
| scopeType); |
| } |
| |
| if (this->GenerateDependers) { |
| this->TargetDependersConnections[dependee].emplace_back(dependee, depender, |
| scopeType); |
| } |
| } |
| |
| void cmGraphVizWriter::ReadSettings( |
| const std::string& settingsFileName, |
| const std::string& fallbackSettingsFileName) |
| { |
| cmake cm(cmake::RoleScript, cmState::Unknown); |
| cm.SetHomeDirectory(""); |
| cm.SetHomeOutputDirectory(""); |
| cm.GetCurrentSnapshot().SetDefaultDefinitions(); |
| cmGlobalGenerator ggi(&cm); |
| cmMakefile mf(&ggi, cm.GetCurrentSnapshot()); |
| std::unique_ptr<cmLocalGenerator> lg(ggi.CreateLocalGenerator(&mf)); |
| |
| std::string 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) \ |
| do { \ |
| cmValue value = mf.GetDefinition(cmakeDefinition); \ |
| if (value) { \ |
| (var) = *value; \ |
| } \ |
| } while (false) |
| |
| 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) \ |
| do { \ |
| cmValue value = mf.GetDefinition(cmakeDefinition); \ |
| if (value) { \ |
| (var) = cmIsOn(*value); \ |
| } \ |
| } while (false) |
| |
| 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->GenerateForInterfaceLibs, "GRAPHVIZ_INTERFACE_LIBS"); |
| set_bool_if_set(this->GenerateForObjectLibs, "GRAPHVIZ_OBJECT_LIBS"); |
| set_bool_if_set(this->GenerateForUnknownLibs, "GRAPHVIZ_UNKNOWN_LIBS"); |
| set_bool_if_set(this->GenerateForCustomTargets, "GRAPHVIZ_CUSTOM_TARGETS"); |
| 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<std::string> ignoreTargetsRegExVector = |
| cmExpandedList(ignoreTargetsRegexes); |
| 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)); |
| } |
| } |
| } |
| |
| void cmGraphVizWriter::Write() |
| { |
| const auto* gg = this->GlobalGenerator; |
| |
| this->VisitGraph(gg->GetName()); |
| |
| // We want to traverse in a determined order, such that the output is always |
| // the same for a given project (this makes tests reproducible, etc.) |
| std::set<cmGeneratorTarget const*, cmGeneratorTarget::StrictTargetComparison> |
| sortedGeneratorTargets; |
| |
| for (const auto& lg : gg->GetLocalGenerators()) { |
| for (const auto& gt : lg->GetGeneratorTargets()) { |
| // Reserved targets have inconsistent names across platforms (e.g. 'all' |
| // vs. 'ALL_BUILD'), which can disrupt the traversal ordering. |
| // We don't need or want them anyway. |
| if (!cmGlobalGenerator::IsReservedTarget(gt->GetName())) { |
| sortedGeneratorTargets.insert(gt.get()); |
| } |
| } |
| } |
| |
| // write global data and collect all connection data for per target graphs |
| for (const auto* const gt : sortedGeneratorTargets) { |
| auto item = cmLinkItem(gt, false, gt->GetBacktrace()); |
| this->VisitItem(item); |
| } |
| |
| if (this->GeneratePerTarget) { |
| this->WritePerTargetConnections<DependeesDir>(this->PerTargetConnections); |
| } |
| |
| if (this->GenerateDependers) { |
| this->WritePerTargetConnections<DependersDir>( |
| this->TargetDependersConnections, ".dependers"); |
| } |
| } |
| |
| void cmGraphVizWriter::FindAllConnections(const ConnectionsMap& connectionMap, |
| const cmLinkItem& rootItem, |
| Connections& extendedCons, |
| std::set<cmLinkItem>& visitedItems) |
| { |
| // some "targets" are not in map, e.g. linker flags as -lm or |
| // targets without dependency. |
| // in both cases we are finished with traversing the graph |
| if (connectionMap.find(rootItem) == connectionMap.cend()) { |
| return; |
| } |
| |
| const Connections& origCons = connectionMap.at(rootItem); |
| |
| for (const Connection& con : origCons) { |
| extendedCons.emplace_back(con); |
| const cmLinkItem& dstItem = con.dst; |
| bool const visited = visitedItems.find(dstItem) != visitedItems.cend(); |
| if (!visited) { |
| visitedItems.insert(dstItem); |
| this->FindAllConnections(connectionMap, dstItem, extendedCons, |
| visitedItems); |
| } |
| } |
| } |
| |
| void cmGraphVizWriter::FindAllConnections(const ConnectionsMap& connectionMap, |
| const cmLinkItem& rootItem, |
| Connections& extendedCons) |
| { |
| std::set<cmLinkItem> visitedItems = { rootItem }; |
| this->FindAllConnections(connectionMap, rootItem, extendedCons, |
| visitedItems); |
| } |
| |
| template <typename DirFunc> |
| void cmGraphVizWriter::WritePerTargetConnections( |
| const ConnectionsMap& connections, const std::string& fileNameSuffix) |
| { |
| // the per target connections must be extended by indirect dependencies |
| ConnectionsMap extendedConnections; |
| for (auto const& conPerTarget : connections) { |
| const cmLinkItem& rootItem = conPerTarget.first; |
| Connections& extendedCons = extendedConnections[conPerTarget.first]; |
| this->FindAllConnections(connections, rootItem, extendedCons); |
| } |
| |
| for (auto const& conPerTarget : extendedConnections) { |
| const cmLinkItem& rootItem = conPerTarget.first; |
| |
| // some of the nodes are excluded completely and are not written |
| if (this->ItemExcluded(rootItem)) { |
| continue; |
| } |
| |
| const Connections& cons = conPerTarget.second; |
| |
| std::unique_ptr<cmGeneratedFileStream> fileStream = |
| this->CreateTargetFile(rootItem, fileNameSuffix); |
| |
| for (const Connection& con : cons) { |
| const cmLinkItem& src = DirFunc::src(con); |
| const cmLinkItem& dst = DirFunc::dst(con); |
| this->WriteNode(*fileStream, con.dst); |
| this->WriteConnection(*fileStream, src, dst, con.scopeType); |
| } |
| |
| this->WriteFooter(*fileStream); |
| } |
| } |
| |
| void cmGraphVizWriter::WriteHeader(cmGeneratedFileStream& fs, |
| const std::string& name) |
| { |
| auto const escapedGraphName = EscapeForDotFile(name); |
| fs << "digraph \"" << escapedGraphName << "\" {\n" |
| << this->GraphHeader << '\n'; |
| } |
| |
| void cmGraphVizWriter::WriteFooter(cmGeneratedFileStream& fs) |
| { |
| fs << "}\n"; |
| } |
| |
| void cmGraphVizWriter::WriteLegend(cmGeneratedFileStream& fs) |
| { |
| // Note that the subgraph name must start with "cluster", as done here, to |
| // make Graphviz layout engines do the right thing and keep the nodes |
| // together. |
| /* clang-format off */ |
| fs << "subgraph clusterLegend {\n" |
| " label = \"Legend\";\n" |
| // Set the color of the box surrounding the legend. |
| " color = black;\n" |
| // We use invisible edges just to enforce the layout. |
| " edge [ style = invis ];\n" |
| // Nodes. |
| " legendNode0 [ label = \"Executable\", shape = " |
| << GRAPHVIZ_NODE_SHAPE_EXECUTABLE << " ];\n" |
| " legendNode1 [ label = \"Static Library\", shape = " |
| << GRAPHVIZ_NODE_SHAPE_LIBRARY_STATIC << " ];\n" |
| " legendNode2 [ label = \"Shared Library\", shape = " |
| << GRAPHVIZ_NODE_SHAPE_LIBRARY_SHARED << " ];\n" |
| " legendNode3 [ label = \"Module Library\", shape = " |
| << GRAPHVIZ_NODE_SHAPE_LIBRARY_MODULE << " ];\n" |
| " legendNode4 [ label = \"Interface Library\", shape = " |
| << GRAPHVIZ_NODE_SHAPE_LIBRARY_INTERFACE << " ];\n" |
| " legendNode5 [ label = \"Object Library\", shape = " |
| << GRAPHVIZ_NODE_SHAPE_LIBRARY_OBJECT << " ];\n" |
| " legendNode6 [ label = \"Unknown Library\", shape = " |
| << GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN << " ];\n" |
| " legendNode7 [ label = \"Custom Target\", shape = " |
| << GRAPHVIZ_NODE_SHAPE_UTILITY << " ];\n" |
| // Edges. |
| // Some of those are dummy (invisible) edges to enforce a layout. |
| " legendNode0 -> legendNode1 [ style = " |
| << GRAPHVIZ_EDGE_STYLE_PUBLIC << " ];\n" |
| " legendNode0 -> legendNode2 [ style = " |
| << GRAPHVIZ_EDGE_STYLE_PUBLIC << " ];\n" |
| " legendNode0 -> legendNode3;\n" |
| " legendNode1 -> legendNode4 [ label = \"Interface\", style = " |
| << GRAPHVIZ_EDGE_STYLE_INTERFACE << " ];\n" |
| " legendNode2 -> legendNode5 [ label = \"Private\", style = " |
| << GRAPHVIZ_EDGE_STYLE_PRIVATE << " ];\n" |
| " legendNode3 -> legendNode6 [ style = " |
| << GRAPHVIZ_EDGE_STYLE_PUBLIC << " ];\n" |
| " legendNode0 -> legendNode7;\n" |
| "}\n"; |
| /* clang-format off */ |
| } |
| |
| void cmGraphVizWriter::WriteNode(cmGeneratedFileStream& fs, |
| cmLinkItem const& item) |
| { |
| auto const& itemName = item.AsStr(); |
| auto const& nodeName = this->NodeNames[itemName]; |
| |
| auto const itemNameWithAliases = this->ItemNameWithAliases(itemName); |
| auto const escapedLabel = EscapeForDotFile(itemNameWithAliases); |
| |
| fs << " \"" << nodeName << "\" [ label = \"" << escapedLabel |
| << "\", shape = " << getShapeForTarget(item) << " ];\n"; |
| } |
| |
| void cmGraphVizWriter::WriteConnection(cmGeneratedFileStream& fs, |
| cmLinkItem const& depender, |
| cmLinkItem const& dependee, |
| std::string const& edgeStyle) |
| { |
| auto const& dependerName = depender.AsStr(); |
| auto const& dependeeName = dependee.AsStr(); |
| |
| fs << " \"" << this->NodeNames[dependerName] << "\" -> \"" |
| << this->NodeNames[dependeeName] << "\" " |
| << edgeStyle |
| << " // " << dependerName << " -> " << dependeeName << '\n'; |
| } |
| |
| bool cmGraphVizWriter::ItemExcluded(cmLinkItem const& item) |
| { |
| auto const itemName = item.AsStr(); |
| |
| if (this->ItemNameFilteredOut(itemName)) { |
| return true; |
| } |
| |
| if (item.Target == nullptr) { |
| return !this->GenerateForExternals; |
| } |
| |
| if (item.Target->GetType() == cmStateEnums::UTILITY) { |
| if (cmHasLiteralPrefix(itemName, "Nightly") || |
| cmHasLiteralPrefix(itemName, "Continuous") || |
| cmHasLiteralPrefix(itemName, "Experimental")) { |
| return true; |
| } |
| } |
| |
| if (item.Target->IsImported() && !this->GenerateForExternals) { |
| return true; |
| } |
| |
| return !this->TargetTypeEnabled(item.Target->GetType()); |
| } |
| |
| bool cmGraphVizWriter::ItemNameFilteredOut(std::string const& itemName) |
| { |
| if (itemName == ">") { |
| // FIXME: why do we even receive such a target here? |
| return true; |
| } |
| |
| if (cmGlobalGenerator::IsReservedTarget(itemName)) { |
| return true; |
| } |
| |
| for (cmsys::RegularExpression& regEx : this->TargetsToIgnoreRegex) { |
| if (regEx.is_valid()) { |
| if (regEx.find(itemName)) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| bool cmGraphVizWriter::TargetTypeEnabled( |
| 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->GenerateForInterfaceLibs; |
| case cmStateEnums::OBJECT_LIBRARY: |
| return this->GenerateForObjectLibs; |
| case cmStateEnums::UNKNOWN_LIBRARY: |
| return this->GenerateForUnknownLibs; |
| case cmStateEnums::UTILITY: |
| return this->GenerateForCustomTargets; |
| case cmStateEnums::GLOBAL_TARGET: |
| // Built-in targets like edit_cache, etc. |
| // We don't need/want those in the dot file. |
| return false; |
| default: |
| break; |
| } |
| return false; |
| } |
| |
| std::string cmGraphVizWriter::ItemNameWithAliases( |
| std::string const& itemName) const |
| { |
| std::vector<std::string> items; |
| for (auto const& lg : this->GlobalGenerator->GetLocalGenerators()) { |
| for (auto const& aliasTargets : lg->GetMakefile()->GetAliasTargets()) { |
| if (aliasTargets.second == itemName) { |
| items.push_back(aliasTargets.first); |
| } |
| } |
| } |
| |
| std::sort(items.begin(), items.end()); |
| items.erase(std::unique(items.begin(), items.end()), items.end()); |
| |
| auto nameWithAliases = itemName; |
| for(auto const& item : items) { |
| nameWithAliases += "\\n(" + item + ")"; |
| } |
| |
| return nameWithAliases; |
| } |
| |
| std::string cmGraphVizWriter::GetEdgeStyle(DependencyType dt) |
| { |
| std::string style; |
| switch (dt) { |
| case DependencyType::LinkPrivate: |
| style = "[ style = " + std::string(GRAPHVIZ_EDGE_STYLE_PRIVATE) + " ]"; |
| break; |
| case DependencyType::LinkInterface: |
| style = "[ style = " + std::string(GRAPHVIZ_EDGE_STYLE_INTERFACE) + " ]"; |
| break; |
| default: |
| break; |
| } |
| return style; |
| } |
| |
| std::string cmGraphVizWriter::EscapeForDotFile(std::string const& str) |
| { |
| return cmSystemTools::EscapeChars(str.data(), "\""); |
| } |
| |
| std::string cmGraphVizWriter::PathSafeString(std::string const& str) |
| { |
| std::string pathSafeStr; |
| |
| // We'll only keep alphanumerical characters, plus the following ones that |
| // are common, and safe on all platforms: |
| auto const extra_chars = std::set<char>{ '.', '-', '_' }; |
| |
| for (char c : str) { |
| if (std::isalnum(c) || extra_chars.find(c) != extra_chars.cend()) { |
| pathSafeStr += c; |
| } |
| } |
| |
| return pathSafeStr; |
| } |