1 /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
2 file Copyright.txt or https://cmake.org/licensing for details. */
3 #include "cmGraphVizWriter.h"
14 #include "cmGeneratedFileStream.h"
15 #include "cmGeneratorTarget.h"
16 #include "cmGlobalGenerator.h"
17 #include "cmLinkItem.h"
18 #include "cmLocalGenerator.h"
19 #include "cmMakefile.h"
21 #include "cmStateSnapshot.h"
22 #include "cmStringAlgorithms.h"
23 #include "cmSystemTools.h"
29 char const* const GRAPHVIZ_EDGE_STYLE_PUBLIC = "solid";
30 char const* const GRAPHVIZ_EDGE_STYLE_INTERFACE = "dashed";
31 char const* const GRAPHVIZ_EDGE_STYLE_PRIVATE = "dotted";
33 char const* const GRAPHVIZ_NODE_SHAPE_EXECUTABLE = "egg"; // egg-xecutable
36 char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_STATIC = "octagon";
37 char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_SHARED = "doubleoctagon";
38 char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_MODULE = "tripleoctagon";
40 char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_INTERFACE = "pentagon";
41 char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_OBJECT = "hexagon";
42 char const* const GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN = "septagon";
44 char const* const GRAPHVIZ_NODE_SHAPE_UTILITY = "box";
46 const char* getShapeForTarget(const cmLinkItem& item)
48 if (item.Target == nullptr) {
49 return GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN;
52 switch (item.Target->GetType()) {
53 case cmStateEnums::EXECUTABLE:
54 return GRAPHVIZ_NODE_SHAPE_EXECUTABLE;
55 case cmStateEnums::STATIC_LIBRARY:
56 return GRAPHVIZ_NODE_SHAPE_LIBRARY_STATIC;
57 case cmStateEnums::SHARED_LIBRARY:
58 return GRAPHVIZ_NODE_SHAPE_LIBRARY_SHARED;
59 case cmStateEnums::MODULE_LIBRARY:
60 return GRAPHVIZ_NODE_SHAPE_LIBRARY_MODULE;
61 case cmStateEnums::OBJECT_LIBRARY:
62 return GRAPHVIZ_NODE_SHAPE_LIBRARY_OBJECT;
63 case cmStateEnums::UTILITY:
64 return GRAPHVIZ_NODE_SHAPE_UTILITY;
65 case cmStateEnums::INTERFACE_LIBRARY:
66 return GRAPHVIZ_NODE_SHAPE_LIBRARY_INTERFACE;
67 case cmStateEnums::UNKNOWN_LIBRARY:
69 return GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN;
76 static const cmLinkItem& src(const T& con)
82 static const cmLinkItem& dst(const T& con)
91 static const cmLinkItem& src(const T& con)
97 static const cmLinkItem& dst(const T& con)
104 cmGraphVizWriter::cmGraphVizWriter(std::string const& fileName,
105 const cmGlobalGenerator* globalGenerator)
107 , GlobalFileStream(fileName)
108 , GraphName(globalGenerator->GetSafeGlobalSetting("CMAKE_PROJECT_NAME"))
109 , GraphHeader("node [\n fontsize = \"12\"\n];")
110 , GraphNodePrefix("node")
111 , GlobalGenerator(globalGenerator)
115 cmGraphVizWriter::~cmGraphVizWriter()
117 this->WriteFooter(this->GlobalFileStream);
120 void cmGraphVizWriter::VisitGraph(std::string const&)
122 this->WriteHeader(this->GlobalFileStream, this->GraphName);
123 this->WriteLegend(this->GlobalFileStream);
126 void cmGraphVizWriter::OnItem(cmLinkItem const& item)
128 if (this->ItemExcluded(item)) {
132 this->NodeNames[item.AsStr()] =
133 cmStrCat(this->GraphNodePrefix, this->NextNodeId);
136 this->WriteNode(this->GlobalFileStream, item);
139 std::unique_ptr<cmGeneratedFileStream> cmGraphVizWriter::CreateTargetFile(
140 cmLinkItem const& item, std::string const& fileNameSuffix)
142 auto const pathSafeItemName = PathSafeString(item.AsStr());
143 auto const perTargetFileName =
144 cmStrCat(this->FileName, '.', pathSafeItemName, fileNameSuffix);
145 auto perTargetFileStream =
146 cm::make_unique<cmGeneratedFileStream>(perTargetFileName);
148 this->WriteHeader(*perTargetFileStream, item.AsStr());
149 this->WriteNode(*perTargetFileStream, item);
151 return perTargetFileStream;
154 void cmGraphVizWriter::OnDirectLink(cmLinkItem const& depender,
155 cmLinkItem const& dependee,
158 this->VisitLink(depender, dependee, true, GetEdgeStyle(dt));
161 void cmGraphVizWriter::OnIndirectLink(cmLinkItem const& depender,
162 cmLinkItem const& dependee)
164 this->VisitLink(depender, dependee, false);
167 void cmGraphVizWriter::VisitLink(cmLinkItem const& depender,
168 cmLinkItem const& dependee, bool isDirectLink,
169 std::string const& scopeType)
171 if (this->ItemExcluded(depender) || this->ItemExcluded(dependee)) {
179 // write global data directly
180 this->WriteConnection(this->GlobalFileStream, depender, dependee, scopeType);
182 if (this->GeneratePerTarget) {
183 this->PerTargetConnections[depender].emplace_back(depender, dependee,
187 if (this->GenerateDependers) {
188 this->TargetDependersConnections[dependee].emplace_back(dependee, depender,
193 void cmGraphVizWriter::ReadSettings(
194 const std::string& settingsFileName,
195 const std::string& fallbackSettingsFileName)
197 cmake cm(cmake::RoleScript, cmState::Unknown);
198 cm.SetHomeDirectory("");
199 cm.SetHomeOutputDirectory("");
200 cm.GetCurrentSnapshot().SetDefaultDefinitions();
201 cmGlobalGenerator ggi(&cm);
202 cmMakefile mf(&ggi, cm.GetCurrentSnapshot());
203 std::unique_ptr<cmLocalGenerator> lg(ggi.CreateLocalGenerator(&mf));
205 std::string inFileName = settingsFileName;
206 if (!cmSystemTools::FileExists(inFileName)) {
207 inFileName = fallbackSettingsFileName;
208 if (!cmSystemTools::FileExists(inFileName)) {
213 if (!mf.ReadListFile(inFileName)) {
214 cmSystemTools::Error("Problem opening GraphViz options file: " +
219 std::cout << "Reading GraphViz options file: " << inFileName << std::endl;
221 #define set_if_set(var, cmakeDefinition) \
223 cmValue value = mf.GetDefinition(cmakeDefinition); \
229 set_if_set(this->GraphName, "GRAPHVIZ_GRAPH_NAME");
230 set_if_set(this->GraphHeader, "GRAPHVIZ_GRAPH_HEADER");
231 set_if_set(this->GraphNodePrefix, "GRAPHVIZ_NODE_PREFIX");
233 #define set_bool_if_set(var, cmakeDefinition) \
235 cmValue value = mf.GetDefinition(cmakeDefinition); \
237 (var) = cmIsOn(*value); \
241 set_bool_if_set(this->GenerateForExecutables, "GRAPHVIZ_EXECUTABLES");
242 set_bool_if_set(this->GenerateForStaticLibs, "GRAPHVIZ_STATIC_LIBS");
243 set_bool_if_set(this->GenerateForSharedLibs, "GRAPHVIZ_SHARED_LIBS");
244 set_bool_if_set(this->GenerateForModuleLibs, "GRAPHVIZ_MODULE_LIBS");
245 set_bool_if_set(this->GenerateForInterfaceLibs, "GRAPHVIZ_INTERFACE_LIBS");
246 set_bool_if_set(this->GenerateForObjectLibs, "GRAPHVIZ_OBJECT_LIBS");
247 set_bool_if_set(this->GenerateForUnknownLibs, "GRAPHVIZ_UNKNOWN_LIBS");
248 set_bool_if_set(this->GenerateForCustomTargets, "GRAPHVIZ_CUSTOM_TARGETS");
249 set_bool_if_set(this->GenerateForExternals, "GRAPHVIZ_EXTERNAL_LIBS");
250 set_bool_if_set(this->GeneratePerTarget, "GRAPHVIZ_GENERATE_PER_TARGET");
251 set_bool_if_set(this->GenerateDependers, "GRAPHVIZ_GENERATE_DEPENDERS");
253 std::string ignoreTargetsRegexes;
254 set_if_set(ignoreTargetsRegexes, "GRAPHVIZ_IGNORE_TARGETS");
256 this->TargetsToIgnoreRegex.clear();
257 if (!ignoreTargetsRegexes.empty()) {
258 std::vector<std::string> ignoreTargetsRegExVector =
259 cmExpandedList(ignoreTargetsRegexes);
260 for (std::string const& currentRegexString : ignoreTargetsRegExVector) {
261 cmsys::RegularExpression currentRegex;
262 if (!currentRegex.compile(currentRegexString)) {
263 std::cerr << "Could not compile bad regex \"" << currentRegexString
264 << "\"" << std::endl;
266 this->TargetsToIgnoreRegex.push_back(std::move(currentRegex));
271 void cmGraphVizWriter::Write()
273 const auto* gg = this->GlobalGenerator;
275 this->VisitGraph(gg->GetName());
277 // We want to traverse in a determined order, such that the output is always
278 // the same for a given project (this makes tests reproducible, etc.)
279 std::set<cmGeneratorTarget const*, cmGeneratorTarget::StrictTargetComparison>
280 sortedGeneratorTargets;
282 for (const auto& lg : gg->GetLocalGenerators()) {
283 for (const auto& gt : lg->GetGeneratorTargets()) {
284 // Reserved targets have inconsistent names across platforms (e.g. 'all'
285 // vs. 'ALL_BUILD'), which can disrupt the traversal ordering.
286 // We don't need or want them anyway.
287 if (!cmGlobalGenerator::IsReservedTarget(gt->GetName())) {
288 sortedGeneratorTargets.insert(gt.get());
293 // write global data and collect all connection data for per target graphs
294 for (const auto* const gt : sortedGeneratorTargets) {
295 auto item = cmLinkItem(gt, false, gt->GetBacktrace());
296 this->VisitItem(item);
299 if (this->GeneratePerTarget) {
300 this->WritePerTargetConnections<DependeesDir>(this->PerTargetConnections);
303 if (this->GenerateDependers) {
304 this->WritePerTargetConnections<DependersDir>(
305 this->TargetDependersConnections, ".dependers");
309 void cmGraphVizWriter::FindAllConnections(const ConnectionsMap& connectionMap,
310 const cmLinkItem& rootItem,
311 Connections& extendedCons,
312 std::set<cmLinkItem>& visitedItems)
314 // some "targets" are not in map, e.g. linker flags as -lm or
315 // targets without dependency.
316 // in both cases we are finished with traversing the graph
317 if (connectionMap.find(rootItem) == connectionMap.cend()) {
321 const Connections& origCons = connectionMap.at(rootItem);
323 for (const Connection& con : origCons) {
324 extendedCons.emplace_back(con);
325 const cmLinkItem& dstItem = con.dst;
326 bool const visited = visitedItems.find(dstItem) != visitedItems.cend();
328 visitedItems.insert(dstItem);
329 this->FindAllConnections(connectionMap, dstItem, extendedCons,
335 void cmGraphVizWriter::FindAllConnections(const ConnectionsMap& connectionMap,
336 const cmLinkItem& rootItem,
337 Connections& extendedCons)
339 std::set<cmLinkItem> visitedItems = { rootItem };
340 this->FindAllConnections(connectionMap, rootItem, extendedCons,
344 template <typename DirFunc>
345 void cmGraphVizWriter::WritePerTargetConnections(
346 const ConnectionsMap& connections, const std::string& fileNameSuffix)
348 // the per target connections must be extended by indirect dependencies
349 ConnectionsMap extendedConnections;
350 for (auto const& conPerTarget : connections) {
351 const cmLinkItem& rootItem = conPerTarget.first;
352 Connections& extendedCons = extendedConnections[conPerTarget.first];
353 this->FindAllConnections(connections, rootItem, extendedCons);
356 for (auto const& conPerTarget : extendedConnections) {
357 const cmLinkItem& rootItem = conPerTarget.first;
359 // some of the nodes are excluded completely and are not written
360 if (this->ItemExcluded(rootItem)) {
364 const Connections& cons = conPerTarget.second;
366 std::unique_ptr<cmGeneratedFileStream> fileStream =
367 this->CreateTargetFile(rootItem, fileNameSuffix);
369 for (const Connection& con : cons) {
370 const cmLinkItem& src = DirFunc::src(con);
371 const cmLinkItem& dst = DirFunc::dst(con);
372 this->WriteNode(*fileStream, con.dst);
373 this->WriteConnection(*fileStream, src, dst, con.scopeType);
376 this->WriteFooter(*fileStream);
380 void cmGraphVizWriter::WriteHeader(cmGeneratedFileStream& fs,
381 const std::string& name)
383 auto const escapedGraphName = EscapeForDotFile(name);
384 fs << "digraph \"" << escapedGraphName << "\" {\n"
385 << this->GraphHeader << '\n';
388 void cmGraphVizWriter::WriteFooter(cmGeneratedFileStream& fs)
393 void cmGraphVizWriter::WriteLegend(cmGeneratedFileStream& fs)
395 // Note that the subgraph name must start with "cluster", as done here, to
396 // make Graphviz layout engines do the right thing and keep the nodes
398 /* clang-format off */
399 fs << "subgraph clusterLegend {\n"
400 " label = \"Legend\";\n"
401 // Set the color of the box surrounding the legend.
403 // We use invisible edges just to enforce the layout.
404 " edge [ style = invis ];\n"
406 " legendNode0 [ label = \"Executable\", shape = "
407 << GRAPHVIZ_NODE_SHAPE_EXECUTABLE << " ];\n"
408 " legendNode1 [ label = \"Static Library\", shape = "
409 << GRAPHVIZ_NODE_SHAPE_LIBRARY_STATIC << " ];\n"
410 " legendNode2 [ label = \"Shared Library\", shape = "
411 << GRAPHVIZ_NODE_SHAPE_LIBRARY_SHARED << " ];\n"
412 " legendNode3 [ label = \"Module Library\", shape = "
413 << GRAPHVIZ_NODE_SHAPE_LIBRARY_MODULE << " ];\n"
414 " legendNode4 [ label = \"Interface Library\", shape = "
415 << GRAPHVIZ_NODE_SHAPE_LIBRARY_INTERFACE << " ];\n"
416 " legendNode5 [ label = \"Object Library\", shape = "
417 << GRAPHVIZ_NODE_SHAPE_LIBRARY_OBJECT << " ];\n"
418 " legendNode6 [ label = \"Unknown Library\", shape = "
419 << GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN << " ];\n"
420 " legendNode7 [ label = \"Custom Target\", shape = "
421 << GRAPHVIZ_NODE_SHAPE_UTILITY << " ];\n"
423 // Some of those are dummy (invisible) edges to enforce a layout.
424 " legendNode0 -> legendNode1 [ style = "
425 << GRAPHVIZ_EDGE_STYLE_PUBLIC << " ];\n"
426 " legendNode0 -> legendNode2 [ style = "
427 << GRAPHVIZ_EDGE_STYLE_PUBLIC << " ];\n"
428 " legendNode0 -> legendNode3;\n"
429 " legendNode1 -> legendNode4 [ label = \"Interface\", style = "
430 << GRAPHVIZ_EDGE_STYLE_INTERFACE << " ];\n"
431 " legendNode2 -> legendNode5 [ label = \"Private\", style = "
432 << GRAPHVIZ_EDGE_STYLE_PRIVATE << " ];\n"
433 " legendNode3 -> legendNode6 [ style = "
434 << GRAPHVIZ_EDGE_STYLE_PUBLIC << " ];\n"
435 " legendNode0 -> legendNode7;\n"
437 /* clang-format off */
440 void cmGraphVizWriter::WriteNode(cmGeneratedFileStream& fs,
441 cmLinkItem const& item)
443 auto const& itemName = item.AsStr();
444 auto const& nodeName = this->NodeNames[itemName];
446 auto const itemNameWithAliases = this->ItemNameWithAliases(itemName);
447 auto const escapedLabel = EscapeForDotFile(itemNameWithAliases);
449 fs << " \"" << nodeName << "\" [ label = \"" << escapedLabel
450 << "\", shape = " << getShapeForTarget(item) << " ];\n";
453 void cmGraphVizWriter::WriteConnection(cmGeneratedFileStream& fs,
454 cmLinkItem const& depender,
455 cmLinkItem const& dependee,
456 std::string const& edgeStyle)
458 auto const& dependerName = depender.AsStr();
459 auto const& dependeeName = dependee.AsStr();
461 fs << " \"" << this->NodeNames[dependerName] << "\" -> \""
462 << this->NodeNames[dependeeName] << "\" "
464 << " // " << dependerName << " -> " << dependeeName << '\n';
467 bool cmGraphVizWriter::ItemExcluded(cmLinkItem const& item)
469 auto const itemName = item.AsStr();
471 if (this->ItemNameFilteredOut(itemName)) {
475 if (item.Target == nullptr) {
476 return !this->GenerateForExternals;
479 if (item.Target->GetType() == cmStateEnums::UTILITY) {
480 if (cmHasLiteralPrefix(itemName, "Nightly") ||
481 cmHasLiteralPrefix(itemName, "Continuous") ||
482 cmHasLiteralPrefix(itemName, "Experimental")) {
487 if (item.Target->IsImported() && !this->GenerateForExternals) {
491 return !this->TargetTypeEnabled(item.Target->GetType());
494 bool cmGraphVizWriter::ItemNameFilteredOut(std::string const& itemName)
496 if (itemName == ">") {
497 // FIXME: why do we even receive such a target here?
501 if (cmGlobalGenerator::IsReservedTarget(itemName)) {
505 for (cmsys::RegularExpression& regEx : this->TargetsToIgnoreRegex) {
506 if (regEx.is_valid()) {
507 if (regEx.find(itemName)) {
516 bool cmGraphVizWriter::TargetTypeEnabled(
517 cmStateEnums::TargetType targetType) const
519 switch (targetType) {
520 case cmStateEnums::EXECUTABLE:
521 return this->GenerateForExecutables;
522 case cmStateEnums::STATIC_LIBRARY:
523 return this->GenerateForStaticLibs;
524 case cmStateEnums::SHARED_LIBRARY:
525 return this->GenerateForSharedLibs;
526 case cmStateEnums::MODULE_LIBRARY:
527 return this->GenerateForModuleLibs;
528 case cmStateEnums::INTERFACE_LIBRARY:
529 return this->GenerateForInterfaceLibs;
530 case cmStateEnums::OBJECT_LIBRARY:
531 return this->GenerateForObjectLibs;
532 case cmStateEnums::UNKNOWN_LIBRARY:
533 return this->GenerateForUnknownLibs;
534 case cmStateEnums::UTILITY:
535 return this->GenerateForCustomTargets;
536 case cmStateEnums::GLOBAL_TARGET:
537 // Built-in targets like edit_cache, etc.
538 // We don't need/want those in the dot file.
546 std::string cmGraphVizWriter::ItemNameWithAliases(
547 std::string const& itemName) const
549 std::vector<std::string> items;
550 for (auto const& lg : this->GlobalGenerator->GetLocalGenerators()) {
551 for (auto const& aliasTargets : lg->GetMakefile()->GetAliasTargets()) {
552 if (aliasTargets.second == itemName) {
553 items.push_back(aliasTargets.first);
558 std::sort(items.begin(), items.end());
559 items.erase(std::unique(items.begin(), items.end()), items.end());
561 auto nameWithAliases = itemName;
562 for(auto const& item : items) {
563 nameWithAliases += "\\n(" + item + ")";
566 return nameWithAliases;
569 std::string cmGraphVizWriter::GetEdgeStyle(DependencyType dt)
573 case DependencyType::LinkPrivate:
574 style = "[ style = " + std::string(GRAPHVIZ_EDGE_STYLE_PRIVATE) + " ]";
576 case DependencyType::LinkInterface:
577 style = "[ style = " + std::string(GRAPHVIZ_EDGE_STYLE_INTERFACE) + " ]";
585 std::string cmGraphVizWriter::EscapeForDotFile(std::string const& str)
587 return cmSystemTools::EscapeChars(str.data(), "\"");
590 std::string cmGraphVizWriter::PathSafeString(std::string const& str)
592 std::string pathSafeStr;
594 // We'll only keep alphanumerical characters, plus the following ones that
595 // are common, and safe on all platforms:
596 auto const extra_chars = std::set<char>{ '.', '-', '_' };
599 if (std::isalnum(c) || extra_chars.find(c) != extra_chars.cend()) {