| /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying | |
| file LICENSE.rst or https://cmake.org/licensing for details. */ | |
| #include "cmGraphVizWriter.h" | |
| #include <algorithm> | |
| #include <iostream> | |
| #include <memory> | |
| #include <set> | |
| #include <unordered_set> | |
| #include <utility> | |
| #include <cm/memory> | |
| #include "cmsys/RegularExpression.hxx" | |
| #include "cmsys/String.h" | |
| #include "cmGeneratedFileStream.h" | |
| #include "cmGeneratorTarget.h" | |
| #include "cmGlobalGenerator.h" | |
| #include "cmLinkItem.h" | |
| #include "cmList.h" | |
| #include "cmLocalGenerator.h" | |
| #include "cmMakefile.h" | |
| #include "cmState.h" | |
| #include "cmStateSnapshot.h" | |
| #include "cmStringAlgorithms.h" | |
| #include "cmSystemTools.h" | |
| #include "cmTargetTypes.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"; | |
| char const* getShapeForTarget(cmLinkItem const& item) | |
| { | |
| if (!item.Target) { | |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN; | |
| } | |
| switch (item.Target->GetType()) { | |
| case cm::TargetType::EXECUTABLE: | |
| return GRAPHVIZ_NODE_SHAPE_EXECUTABLE; | |
| case cm::TargetType::STATIC_LIBRARY: | |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_STATIC; | |
| case cm::TargetType::SHARED_LIBRARY: | |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_SHARED; | |
| case cm::TargetType::MODULE_LIBRARY: | |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_MODULE; | |
| case cm::TargetType::OBJECT_LIBRARY: | |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_OBJECT; | |
| case cm::TargetType::UTILITY: | |
| return GRAPHVIZ_NODE_SHAPE_UTILITY; | |
| case cm::TargetType::INTERFACE_LIBRARY: | |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_INTERFACE; | |
| case cm::TargetType::UNKNOWN_LIBRARY: | |
| default: | |
| return GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN; | |
| } | |
| } | |
| struct DependeesDir | |
| { | |
| template <typename T> | |
| static cmLinkItem const& src(T const& con) | |
| { | |
| return con.src; | |
| } | |
| template <typename T> | |
| static cmLinkItem const& dst(T const& con) | |
| { | |
| return con.dst; | |
| } | |
| }; | |
| struct DependersDir | |
| { | |
| template <typename T> | |
| static cmLinkItem const& src(T const& con) | |
| { | |
| return con.dst; | |
| } | |
| template <typename T> | |
| static cmLinkItem const& dst(T const& con) | |
| { | |
| return con.src; | |
| } | |
| }; | |
| } | |
| cmGraphVizWriter::cmGraphVizWriter(std::string const& fileName, | |
| cmGlobalGenerator const* 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( | |
| std::string const& settingsFileName, | |
| std::string const& fallbackSettingsFileName) | |
| { | |
| cmake cm(cmState::Role::Script); | |
| 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()) { | |
| cmList ignoreTargetsRegExList{ ignoreTargetsRegexes }; | |
| for (std::string const& currentRegexString : ignoreTargetsRegExList) { | |
| 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() | |
| { | |
| auto const* 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 (auto const& lg : gg->GetLocalGenerators()) { | |
| for (auto const& 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()) && | |
| !cmHasLiteralPrefix(gt->GetName(), "__cmake_")) { | |
| sortedGeneratorTargets.insert(gt.get()); | |
| } | |
| } | |
| } | |
| // write global data and collect all connection data for per target graphs | |
| for (auto const* 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(ConnectionsMap const& connectionMap, | |
| cmLinkItem const& 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; | |
| } | |
| Connections const& origCons = connectionMap.at(rootItem); | |
| for (Connection const& con : origCons) { | |
| extendedCons.emplace_back(con); | |
| cmLinkItem const& 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(ConnectionsMap const& connectionMap, | |
| cmLinkItem const& rootItem, | |
| Connections& extendedCons) | |
| { | |
| std::set<cmLinkItem> visitedItems = { rootItem }; | |
| this->FindAllConnections(connectionMap, rootItem, extendedCons, | |
| visitedItems); | |
| } | |
| template <typename DirFunc> | |
| void cmGraphVizWriter::WritePerTargetConnections( | |
| ConnectionsMap const& connections, std::string const& fileNameSuffix) | |
| { | |
| // the per target connections must be extended by indirect dependencies | |
| ConnectionsMap extendedConnections; | |
| for (auto const& conPerTarget : connections) { | |
| cmLinkItem const& rootItem = conPerTarget.first; | |
| Connections& extendedCons = extendedConnections[conPerTarget.first]; | |
| this->FindAllConnections(connections, rootItem, extendedCons); | |
| } | |
| for (auto const& conPerTarget : extendedConnections) { | |
| cmLinkItem const& rootItem = conPerTarget.first; | |
| // some of the nodes are excluded completely and are not written | |
| if (this->ItemExcluded(rootItem)) { | |
| continue; | |
| } | |
| Connections const& cons = conPerTarget.second; | |
| std::unique_ptr<cmGeneratedFileStream> fileStream = | |
| this->CreateTargetFile(rootItem, fileNameSuffix); | |
| // avoid write same node multiple times | |
| std::unordered_set<std::string> writtenNodes = { rootItem.AsStr() }; | |
| for (Connection const& con : cons) { | |
| cmLinkItem const& src = DirFunc::src(con); | |
| cmLinkItem const& dst = DirFunc::dst(con); | |
| if (writtenNodes.emplace(con.dst.AsStr()).second) { | |
| this->WriteNode(*fileStream, con.dst); | |
| } | |
| this->WriteConnection(*fileStream, src, dst, con.scopeType); | |
| } | |
| this->WriteFooter(*fileStream); | |
| } | |
| } | |
| void cmGraphVizWriter::WriteHeader(cmGeneratedFileStream& fs, | |
| std::string const& 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) { | |
| return !this->GenerateForExternals; | |
| } | |
| if (item.Target->GetType() == cm::TargetType::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( | |
| cm::TargetType targetType) const | |
| { | |
| switch (targetType) { | |
| case cm::TargetType::EXECUTABLE: | |
| return this->GenerateForExecutables; | |
| case cm::TargetType::STATIC_LIBRARY: | |
| return this->GenerateForStaticLibs; | |
| case cm::TargetType::SHARED_LIBRARY: | |
| return this->GenerateForSharedLibs; | |
| case cm::TargetType::MODULE_LIBRARY: | |
| return this->GenerateForModuleLibs; | |
| case cm::TargetType::INTERFACE_LIBRARY: | |
| return this->GenerateForInterfaceLibs; | |
| case cm::TargetType::OBJECT_LIBRARY: | |
| return this->GenerateForObjectLibs; | |
| case cm::TargetType::UNKNOWN_LIBRARY: | |
| return this->GenerateForUnknownLibs; | |
| case cm::TargetType::UTILITY: | |
| return this->GenerateForCustomTargets; | |
| case cm::TargetType::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 = cmStrCat(std::move(nameWithAliases), "\\n(" , item , ')'); | |
| } | |
| return nameWithAliases; | |
| } | |
| std::string cmGraphVizWriter::GetEdgeStyle(DependencyType dt) | |
| { | |
| std::string style; | |
| switch (dt) { | |
| case DependencyType::LinkPrivate: | |
| style = cmStrCat("[ style = ", GRAPHVIZ_EDGE_STYLE_PRIVATE, " ]"); | |
| break; | |
| case DependencyType::LinkInterface: | |
| style = cmStrCat("[ style = ", 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 (cmsysString_isalnum(c) || extra_chars.find(c) != extra_chars.cend()) { | |
| pathSafeStr += c; | |
| } | |
| } | |
| return pathSafeStr; | |
| } |