Browse Source

Merge topic 'graphviz'

553658393c Graphviz: added test suite, fixes, enhancements

Acked-by: Kitware Robot <[email protected]>
Acked-by: slodki <[email protected]>
Merge-request: !3766
Brad King 6 years ago
parent
commit
5417737fac
50 changed files with 1692 additions and 501 deletions
  1. 86 60
      Modules/CMakeGraphVizOptions.cmake
  2. 2 0
      Source/CMakeLists.txt
  3. 323 400
      Source/cmGraphVizWriter.cxx
  4. 56 37
      Source/cmGraphVizWriter.h
  5. 142 0
      Source/cmLinkItemGraphVisitor.cxx
  6. 75 0
      Source/cmLinkItemGraphVisitor.h
  7. 3 4
      Source/cmake.cxx
  8. 1 0
      Tests/RunCMake/CMakeLists.txt
  9. 1 0
      Tests/RunCMake/Graphviz/CMakeGraphVizOptions.cmake.in
  10. 3 0
      Tests/RunCMake/Graphviz/CMakeLists.txt
  11. 58 0
      Tests/RunCMake/Graphviz/GraphvizTestProject.cmake
  12. 82 0
      Tests/RunCMake/Graphviz/RunCMakeTest.cmake
  13. 5 0
      Tests/RunCMake/Graphviz/default_options-check.cmake
  14. 52 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_custom_targets.dot
  15. 50 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_default_options.dot
  16. 50 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_dependers_files.dot
  17. 44 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_executables.dot
  18. 46 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_external_libs.dot
  19. 35 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_graphic_libs.dot
  20. 43 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_interface_libs.dot
  21. 44 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_module_libs.dot
  22. 48 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_object_libs.dot
  23. 50 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_per_target_files.dot
  24. 44 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_shared_libs.dot
  25. 42 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_static_libs.dot
  26. 48 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_unknown_libs.dot
  27. 50 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_set_graph_header.dot
  28. 50 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_set_graph_name.dot
  29. 50 0
      Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_set_node_prefix.dot
  30. 4 0
      Tests/RunCMake/Graphviz/no_dependers_files-check.cmake
  31. 5 0
      Tests/RunCMake/Graphviz/no_executables-check.cmake
  32. 5 0
      Tests/RunCMake/Graphviz/no_external_libs-check.cmake
  33. 5 0
      Tests/RunCMake/Graphviz/no_graphic_libs-check.cmake
  34. 5 0
      Tests/RunCMake/Graphviz/no_interface_libs-check.cmake
  35. 5 0
      Tests/RunCMake/Graphviz/no_module_libs-check.cmake
  36. 5 0
      Tests/RunCMake/Graphviz/no_object_libs-check.cmake
  37. 5 0
      Tests/RunCMake/Graphviz/no_per_target_files-check.cmake
  38. 5 0
      Tests/RunCMake/Graphviz/no_shared_libs-check.cmake
  39. 5 0
      Tests/RunCMake/Graphviz/no_static_libs-check.cmake
  40. 5 0
      Tests/RunCMake/Graphviz/no_unknown_libs-check.cmake
  41. 5 0
      Tests/RunCMake/Graphviz/set_graph_header-check.cmake
  42. 5 0
      Tests/RunCMake/Graphviz/set_graph_name-check.cmake
  43. 5 0
      Tests/RunCMake/Graphviz/set_node_prefix-check.cmake
  44. 3 0
      Tests/RunCMake/Graphviz/test_project/core_library.c
  45. 3 0
      Tests/RunCMake/Graphviz/test_project/graphic_library.c
  46. 4 0
      Tests/RunCMake/Graphviz/test_project/main.c
  47. 3 0
      Tests/RunCMake/Graphviz/test_project/module.c
  48. 3 0
      Tests/RunCMake/Graphviz/test_project/third_party_project/CMakeLists.txt
  49. 23 0
      Tests/RunCMake/RunCMake.cmake
  50. 1 0
      bootstrap

+ 86 - 60
Modules/CMakeGraphVizOptions.cmake

@@ -5,119 +5,145 @@
 CMakeGraphVizOptions
 --------------------
 
-The builtin graphviz support of CMake.
+The builtin Graphviz support of CMake.
 
-Variables specific to the graphviz support
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Generating Graphviz files
+^^^^^^^^^^^^^^^^^^^^^^^^^
 
-CMake
-can generate `graphviz <http://www.graphviz.org/>`_ files, showing the dependencies between the
-targets in a project and also external libraries which are linked
-against.  When CMake is run with the ``--graphviz=foo.dot`` option, it will
-produce:
+CMake can generate `Graphviz <https://www.graphviz.org/>`_ files showing the
+dependencies between the targets in a project, as well as external libraries
+which are linked against.
 
-* a ``foo.dot`` file showing all dependencies in the project
-* a ``foo.dot.<target>`` file for each target, file showing on which other targets the respective target depends
-* a ``foo.dot.<target>.dependers`` file, showing which other targets depend on the respective target
+When running CMake with the ``--graphviz=foo.dot`` option, it produces:
 
-The different dependency types ``PUBLIC``, ``PRIVATE`` and ``INTERFACE``
-are represented as solid, dashed and dotted edges.
+* a ``foo.dot`` file, showing all dependencies in the project
+* a ``foo.dot.<target>`` file for each target, showing on which other targets
+  it depends
+* a ``foo.dot.<target>.dependers`` file for each target, showing which other
+  targets depend on it
 
-This can result in huge graphs.  Using the file
-``CMakeGraphVizOptions.cmake`` the look and content of the generated
-graphs can be influenced.  This file is searched first in
-:variable:`CMAKE_BINARY_DIR` and then in :variable:`CMAKE_SOURCE_DIR`.  If found, it is
-read and the variables set in it are used to adjust options for the
-generated graphviz files.
+Those .dot files can be converted to images using the *dot* command from the
+Graphviz package:
 
-.. variable:: GRAPHVIZ_GRAPH_TYPE
+.. code-block:: shell
 
- The graph type.
+  dot -Tpng -o foo.png foo.dot
 
- * Mandatory : NO
- * Default   : "digraph"
+The different dependency types ``PUBLIC``, ``INTERFACE`` and ``PRIVATE``
+are represented as solid, dashed and dotted edges.
 
- Valid graph types are:
+Variables specific to the Graphviz support
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
- * "graph" : Nodes are joined with lines
- * "digraph" : Nodes are joined with arrows showing direction
- * "strict graph" : Like "graph" but max one line between each node
- * "strict digraph" : Like "graph" but max one line between each node in each direction
+The resulting graphs can be huge.  The look and content of the generated graphs
+can be controlled using the file ``CMakeGraphVizOptions.cmake``.  This file is
+first searched in :variable:`CMAKE_BINARY_DIR`, and then in
+:variable:`CMAKE_SOURCE_DIR`.  If found, the variables set in it are used to
+adjust options for the generated Graphviz files.
 
 .. variable:: GRAPHVIZ_GRAPH_NAME
 
  The graph name.
 
- * Mandatory : NO
- * Default   : "GG"
+ * Mandatory: NO
+ * Default: value of :variable:`CMAKE_PROJECT_NAME`
 
 .. variable:: GRAPHVIZ_GRAPH_HEADER
 
- The header written at the top of the graphviz file.
+ The header written at the top of the Graphviz files.
 
- * Mandatory : NO
- * Default   : "node [n  fontsize = "12"];"
+ * Mandatory: NO
+ * Default: "node [ fontsize = "12" ];"
 
 .. variable:: GRAPHVIZ_NODE_PREFIX
 
- The prefix for each node in the graphviz file.
+ The prefix for each node in the Graphviz files.
 
- * Mandatory : NO
- * Default   : "node"
+ * Mandatory: NO
+ * Default: "node"
 
 .. variable:: GRAPHVIZ_EXECUTABLES
 
- Set this to FALSE to exclude executables from the generated graphs.
+ Set to FALSE to exclude executables from the generated graphs.
 
- * Mandatory : NO
- * Default   : TRUE
+ * Mandatory: NO
+ * Default: TRUE
 
 .. variable:: GRAPHVIZ_STATIC_LIBS
 
- Set this to FALSE to exclude static libraries from the generated graphs.
+ Set to FALSE to exclude static libraries from the generated graphs.
 
- * Mandatory : NO
- * Default   : TRUE
+ * Mandatory: NO
+ * Default: TRUE
 
 .. variable:: GRAPHVIZ_SHARED_LIBS
 
- Set this to FALSE to exclude shared libraries from the generated graphs.
+ Set to FALSE to exclude shared libraries from the generated graphs.
 
- * Mandatory : NO
- * Default   : TRUE
+ * Mandatory: NO
+ * Default: TRUE
 
 .. variable:: GRAPHVIZ_MODULE_LIBS
 
- Set this to FALSE to exclude module libraries from the generated graphs.
+ Set to FALSE to exclude module libraries from the generated graphs.
+
+ * Mandatory: NO
+ * Default: TRUE
+
+.. variable:: GRAPHVIZ_INTERFACE_LIBS
+
+ Set to FALSE to exclude interface libraries from the generated graphs.
+
+ * Mandatory: NO
+ * Default: TRUE
 
- * Mandatory : NO
- * Default   : TRUE
+.. variable:: GRAPHVIZ_OBJECT_LIBS
+
+ Set to FALSE to exclude object libraries from the generated graphs.
+
+ * Mandatory: NO
+ * Default: TRUE
+
+.. variable:: GRAPHVIZ_UNKNOWN_LIBS
+
+ Set to FALSE to exclude unknown libraries from the generated graphs.
+
+ * Mandatory: NO
+ * Default: TRUE
 
 .. variable:: GRAPHVIZ_EXTERNAL_LIBS
 
- Set this to FALSE to exclude external libraries from the generated graphs.
+ Set to FALSE to exclude external libraries from the generated graphs.
+
+ * Mandatory: NO
+ * Default: TRUE
+
+.. variable:: GRAPHVIZ_CUSTOM_TARGETS
+
+ Set to TRUE to include custom targets in the generated graphs.
 
- * Mandatory : NO
- * Default   : TRUE
+ * Mandatory: NO
+ * Default: FALSE
 
 .. variable:: GRAPHVIZ_IGNORE_TARGETS
 
- A list of regular expressions for ignoring targets.
+ A list of regular expressions for names of targets to exclude from the
+ generated graphs.
 
- * Mandatory : NO
- * Default   : empty
+ * Mandatory: NO
+ * Default: empty
 
 .. variable:: GRAPHVIZ_GENERATE_PER_TARGET
 
- Set this to FALSE to exclude per target graphs ``foo.dot.<target>``.
+ Set to FALSE to not generate per-target graphs ``foo.dot.<target>``.
 
- * Mandatory : NO
- * Default   : TRUE
+ * Mandatory: NO
+ * Default: TRUE
 
 .. variable:: GRAPHVIZ_GENERATE_DEPENDERS
 
- Set this to FALSE to exclude depender graphs ``foo.dot.<target>.dependers``.
+ Set to FALSE to not generate depender graphs ``foo.dot.<target>.dependers``.
 
- * Mandatory : NO
- * Default   : TRUE
+ * Mandatory: NO
+ * Default: TRUE
 #]=======================================================================]

+ 2 - 0
Source/CMakeLists.txt

@@ -289,6 +289,8 @@ set(SRCS
   cmGeneratorExpression.h
   cmGeneratorTarget.cxx
   cmGeneratorTarget.h
+  cmLinkItemGraphVisitor.cxx
+  cmLinkItemGraphVisitor.h
   cmGetPipes.cxx
   cmGetPipes.h
   cmGlobalCommonGenerator.cxx

+ 323 - 400
Source/cmGraphVizWriter.cxx

@@ -2,174 +2,190 @@
    file Copyright.txt or https://cmake.org/licensing for details.  */
 #include "cmGraphVizWriter.h"
 
-#include <cstddef>
+#include <cctype>
 #include <iostream>
 #include <memory>
-#include <sstream>
+#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 "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";
+char const* const GRAPHVIZ_EDGE_STYLE_PUBLIC = "solid";
+char const* const GRAPHVIZ_EDGE_STYLE_INTERFACE = "dashed";
+char const* const GRAPHVIZ_EDGE_STYLE_PRIVATE = "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;
-}
+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";
 
-const char* getShapeForTarget(const cmGeneratorTarget* target)
+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 (!target) {
-    return "ellipse";
+  if (item.Target == nullptr) {
+    return GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN;
   }
 
-  switch (target->GetType()) {
+  switch (item.Target->GetType()) {
     case cmStateEnums::EXECUTABLE:
-      return "house";
+      return GRAPHVIZ_NODE_SHAPE_EXECUTABLE;
     case cmStateEnums::STATIC_LIBRARY:
-      return "diamond";
+      return GRAPHVIZ_NODE_SHAPE_LIBRARY_STATIC;
     case cmStateEnums::SHARED_LIBRARY:
-      return "polygon";
+      return GRAPHVIZ_NODE_SHAPE_LIBRARY_SHARED;
     case cmStateEnums::MODULE_LIBRARY:
-      return "octagon";
+      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:
-      break;
+      return GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN;
   }
+}
+}
 
-  return "box";
+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)
+  , NextNodeId(0)
+  , GenerateForExecutables(true)
+  , GenerateForStaticLibs(true)
+  , GenerateForSharedLibs(true)
+  , GenerateForModuleLibs(true)
+  , GenerateForInterfaceLibs(true)
+  , GenerateForObjectLibs(true)
+  , GenerateForUnknownLibs(true)
+  , GenerateForCustomTargets(false)
+  , GenerateForExternals(true)
+  , GeneratePerTarget(true)
+  , GenerateDependers(true)
+{
 }
 
-std::map<std::string, LinkLibraryScopeType> getScopedLinkLibrariesFromTarget(
-  cmTarget* Target, const cmGlobalGenerator* globalGenerator)
+cmGraphVizWriter::~cmGraphVizWriter()
 {
-  char sep = ';';
-  std::map<std::string, LinkLibraryScopeType> tokens;
-  size_t start = 0;
-  size_t end = 0;
+  this->WriteFooter(this->GlobalFileStream);
 
-  const char* pInterfaceLinkLibraries =
-    Target->GetProperty("INTERFACE_LINK_LIBRARIES");
-  const char* pLinkLibraries = Target->GetProperty("LINK_LIBRARIES");
+  for (auto& fileStream : this->PerTargetFileStreams) {
+    this->WriteFooter(*fileStream.second);
+  }
 
-  if (!pInterfaceLinkLibraries && !pLinkLibraries) {
-    return tokens; // target is not linked against any other libraries
+  for (auto& fileStream : this->TargetDependersFileStreams) {
+    this->WriteFooter(*fileStream.second);
   }
+}
 
-  // make sure we don't touch a null-ptr
-  auto interfaceLinkLibraries =
-    std::string(pInterfaceLinkLibraries ? pInterfaceLinkLibraries : "");
-  auto linkLibraries = std::string(pLinkLibraries ? pLinkLibraries : "");
+void cmGraphVizWriter::VisitGraph(std::string const&)
+{
+  this->WriteHeader(GlobalFileStream, this->GraphName);
+  this->WriteLegend(GlobalFileStream);
+}
 
-  // first extract interfaceLinkLibraries
-  while (start < interfaceLinkLibraries.length()) {
+void cmGraphVizWriter::OnItem(cmLinkItem const& item)
+{
+  if (this->ItemExcluded(item)) {
+    return;
+  }
 
-    if ((end = interfaceLinkLibraries.find(sep, start)) == std::string::npos) {
-      end = interfaceLinkLibraries.length();
-    }
+  NodeNames[item.AsStr()] = cmStrCat(GraphNodePrefix, NextNodeId);
+  ++NextNodeId;
 
-    std::string element = interfaceLinkLibraries.substr(start, end - start);
-    if (globalGenerator->IsAlias(element)) {
-      const auto tgt = globalGenerator->FindTarget(element);
-      if (tgt) {
-        element = tgt->GetName();
-      }
-    }
+  this->WriteNode(this->GlobalFileStream, item);
 
-    if (std::string::npos == element.find("$<LINK_ONLY:", 0)) {
-      // we assume first, that this library is an interface library.
-      // if we find it again in the linklibraries property, we promote it to an
-      // public library.
-      tokens[element] = LLT_SCOPE_INTERFACE;
-    } else {
-      // this is an private linked static library.
-      // we take care of this case in the second iterator.
-    }
-    start = end + 1;
+  if (this->GeneratePerTarget) {
+    this->CreateTargetFile(this->PerTargetFileStreams, item);
   }
 
-  // second extract linkLibraries
-  start = 0;
-  while (start < linkLibraries.length()) {
-
-    if ((end = linkLibraries.find(sep, start)) == std::string::npos) {
-      end = linkLibraries.length();
-    }
+  if (this->GenerateDependers) {
+    this->CreateTargetFile(this->TargetDependersFileStreams, item,
+                           ".dependers");
+  }
+}
 
-    std::string element = linkLibraries.substr(start, end - start);
-    if (globalGenerator->IsAlias(element)) {
-      const auto tgt = globalGenerator->FindTarget(element);
-      if (tgt) {
-        element = tgt->GetName();
-      }
-    }
+void cmGraphVizWriter::CreateTargetFile(FileStreamMap& fileStreamMap,
+                                        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);
 
-    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.
-    }
+  this->WriteHeader(*perTargetFileStream, item.AsStr());
+  this->WriteNode(*perTargetFileStream, item);
 
-    start = end + 1;
-  }
+  fileStreamMap.emplace(item.AsStr(), std::move(perTargetFileStream));
+}
 
-  return tokens;
+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);
 }
 
-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::VisitLink(cmLinkItem const& depender,
+                                 cmLinkItem const& dependee, bool isDirectLink,
+                                 std::string const& scopeType)
 {
+  if (this->ItemExcluded(depender) || this->ItemExcluded(dependee)) {
+    return;
+  }
+
+  if (!isDirectLink) {
+    return;
+  }
+
+  this->WriteConnection(this->GlobalFileStream, depender, dependee, scopeType);
+
+  if (this->GeneratePerTarget) {
+    auto fileStream = PerTargetFileStreams[depender.AsStr()].get();
+    this->WriteNode(*fileStream, dependee);
+    this->WriteConnection(*fileStream, depender, dependee, scopeType);
+  }
+
+  if (this->GenerateDependers) {
+    auto fileStream = TargetDependersFileStreams[dependee.AsStr()].get();
+    this->WriteNode(*fileStream, depender);
+    this->WriteConnection(*fileStream, depender, dependee, scopeType);
+  }
 }
 
 void cmGraphVizWriter::ReadSettings(
@@ -208,7 +224,6 @@ void cmGraphVizWriter::ReadSettings(
     }                                                                         \
   } while (false)
 
-  __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");
@@ -225,7 +240,10 @@ void cmGraphVizWriter::ReadSettings(
   __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->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");
@@ -248,329 +266,170 @@ void cmGraphVizWriter::ReadSettings(
   }
 }
 
-// Iterate over all targets and write for each one a graph which shows
-// which other targets depend on it.
-void cmGraphVizWriter::WriteTargetDependersFiles(const std::string& fileName)
+void cmGraphVizWriter::Write()
 {
-  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 =
-      cmStrCat(fileName, '.', ptr.first, ".dependers");
-
-    cmGeneratedFileStream str(currentFilename);
-    if (!str) {
-      return;
+  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 (cmLocalGenerator const* lg : gg->GetLocalGenerators()) {
+    for (cmGeneratorTarget 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())) {
+        sortedGeneratorTargets.insert(gt);
+      }
     }
-
-    std::set<std::string> insertedConnections;
-    std::set<std::string> 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 std::string& 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<std::string> insertedConnections;
-    std::set<std::string> insertedNodes;
-
-    std::string currentFilename = cmStrCat(fileName, '.', 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);
+  for (auto const gt : sortedGeneratorTargets) {
+    auto item = cmLinkItem(gt, gt->GetBacktrace());
+    this->VisitItem(item);
   }
 }
 
-void cmGraphVizWriter::WriteGlobalFile(const std::string& fileName)
+void cmGraphVizWriter::WriteHeader(cmGeneratedFileStream& fs,
+                                   const std::string& name)
 {
-  this->CollectTargetsAndLibs();
-
-  cmGeneratedFileStream str(fileName);
-  if (!str) {
-    return;
-  }
-  this->WriteHeader(str);
-
-  std::cout << "Writing " << fileName << "..." << std::endl;
-
-  std::set<std::string> insertedConnections;
-  std::set<std::string> 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);
+  auto const escapedGraphName = EscapeForDotFile(name);
+  fs << "digraph \"" << escapedGraphName << "\" {" << std::endl;
+  fs << this->GraphHeader << std::endl;
 }
 
-void cmGraphVizWriter::WriteHeader(cmGeneratedFileStream& str) const
+void cmGraphVizWriter::WriteFooter(cmGeneratedFileStream& fs)
 {
-  str << this->GraphType << " \"" << this->GraphName << "\" {" << std::endl;
-  str << this->GraphHeader << std::endl;
+  fs << "}" << std::endl;
 }
 
-void cmGraphVizWriter::WriteFooter(cmGeneratedFileStream& str) const
+void cmGraphVizWriter::WriteLegend(cmGeneratedFileStream& fs)
 {
-  str << "}" << std::endl;
+  // 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.
+  fs << "subgraph clusterLegend {" << std::endl;
+  fs << "  label = \"Legend\";" << std::endl;
+  // Set the color of the box surrounding the legend.
+  fs << "  color = black;" << std::endl;
+  // We use invisible edges just to enforce the layout.
+  fs << "  edge [ style = invis ];" << std::endl;
+
+  // Nodes.
+  fs << "  legendNode0 [ label = \"Executable\", shape = "
+     << GRAPHVIZ_NODE_SHAPE_EXECUTABLE << " ];" << std::endl;
+
+  fs << "  legendNode1 [ label = \"Static Library\", shape = "
+     << GRAPHVIZ_NODE_SHAPE_LIBRARY_STATIC << " ];" << std::endl;
+  fs << "  legendNode2 [ label = \"Shared Library\", shape = "
+     << GRAPHVIZ_NODE_SHAPE_LIBRARY_SHARED << " ];" << std::endl;
+  fs << "  legendNode3 [ label = \"Module Library\", shape = "
+     << GRAPHVIZ_NODE_SHAPE_LIBRARY_MODULE << " ];" << std::endl;
+
+  fs << "  legendNode4 [ label = \"Interface Library\", shape = "
+     << GRAPHVIZ_NODE_SHAPE_LIBRARY_INTERFACE << " ];" << std::endl;
+  fs << "  legendNode5 [ label = \"Object Library\", shape = "
+     << GRAPHVIZ_NODE_SHAPE_LIBRARY_OBJECT << " ];" << std::endl;
+  fs << "  legendNode6 [ label = \"Unknown Library\", shape = "
+     << GRAPHVIZ_NODE_SHAPE_LIBRARY_UNKNOWN << " ];" << std::endl;
+
+  fs << "  legendNode7 [ label = \"Custom Target\", shape = "
+     << GRAPHVIZ_NODE_SHAPE_UTILITY << " ];" << std::endl;
+
+  // Edges.
+  // Some of those are dummy (invisible) edges to enforce a layout.
+  fs << "  legendNode0 -> legendNode1 [ style = " << GRAPHVIZ_EDGE_STYLE_PUBLIC
+     << " ];" << std::endl;
+  fs << "  legendNode0 -> legendNode2 [ style = " << GRAPHVIZ_EDGE_STYLE_PUBLIC
+     << " ];" << std::endl;
+  fs << "  legendNode0 -> legendNode3;" << std::endl;
+
+  fs << "  legendNode1 -> legendNode4 [ label = \"Interface\", style = "
+     << GRAPHVIZ_EDGE_STYLE_INTERFACE << " ];" << std::endl;
+  fs << "  legendNode2 -> legendNode5 [ label = \"Private\", style = "
+     << GRAPHVIZ_EDGE_STYLE_PRIVATE << " ];" << std::endl;
+  fs << "  legendNode3 -> legendNode6 [ style = " << GRAPHVIZ_EDGE_STYLE_PUBLIC
+     << " ];" << std::endl;
+
+  fs << "  legendNode0 -> legendNode7;" << std::endl;
+
+  fs << "}" << std::endl;
 }
 
-void cmGraphVizWriter::WriteConnections(
-  const std::string& targetName, std::set<std::string>& insertedNodes,
-  std::set<std::string>& insertedConnections, cmGeneratedFileStream& str) const
+void cmGraphVizWriter::WriteNode(cmGeneratedFileStream& fs,
+                                 cmLinkItem const& item)
 {
-  auto targetPtrIt = this->TargetPtrs.find(targetName);
+  auto const& itemName = item.AsStr();
+  auto const& nodeName = this->NodeNames[itemName];
 
-  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;
-  }
+  auto const itemNameWithAliases = ItemNameWithAliases(itemName);
+  auto const escapedLabel = EscapeForDotFile(itemNameWithAliases);
 
-  std::string myNodeName = this->TargetNamesNodes.find(targetName)->second;
-  std::map<std::string, LinkLibraryScopeType> ll =
-    getScopedLinkLibrariesFromTarget(targetPtrIt->second->Target,
-                                     GlobalGenerator);
-
-  for (auto const& llit : ll) {
-    const std::string& libName = llit.first;
-    auto libNameIt = this->TargetNamesNodes.find(libName);
-
-    // can happen e.g. if GRAPHVIZ_TARGET_IGNORE_REGEX is used
-    if (libNameIt == this->TargetNamesNodes.end()) {
-      continue;
-    }
+  fs << "    \"" << nodeName << "\" [ label = \"" << escapedLabel
+     << "\", shape = " << getShapeForTarget(item) << " ];" << std::endl;
+}
 
-    std::string connectionName = cmStrCat(myNodeName, '-', libNameIt->second);
-    if (insertedConnections.find(connectionName) ==
-        insertedConnections.end()) {
-      insertedConnections.insert(connectionName);
-      this->WriteNode(libName, this->TargetPtrs.find(libName)->second,
-                      insertedNodes, str);
+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();
 
-      str << "    \"" << myNodeName << "\" -> \"" << libNameIt->second << "\"";
+  fs << "    \"" << this->NodeNames[dependerName] << "\" -> \""
+     << this->NodeNames[dependeeName] << "\" ";
 
-      str << getLinkLibraryStyle(llit.second);
+  fs << edgeStyle;
 
-      str << " // " << targetName << " -> " << libName << std::endl;
-      this->WriteConnections(libName, insertedNodes, insertedConnections, str);
-    }
-  }
+  fs << " // " << dependerName << " -> " << dependeeName << std::endl;
 }
 
-void cmGraphVizWriter::WriteDependerConnections(
-  const std::string& targetName, std::set<std::string>& insertedNodes,
-  std::set<std::string>& insertedConnections, cmGeneratedFileStream& str) const
+bool cmGraphVizWriter::ItemExcluded(cmLinkItem const& item)
 {
-  auto targetPtrIt = this->TargetPtrs.find(targetName);
+  auto const itemName = item.AsStr();
 
-  if (targetPtrIt == this->TargetPtrs.end()) // not found at all
-  {
-    return;
+  if (this->ItemNameFilteredOut(itemName)) {
+    return true;
   }
 
-  this->WriteNode(targetName, targetPtrIt->second, insertedNodes, str);
-
-  if (targetPtrIt->second == nullptr) // it's an external library
-  {
-    return;
+  if (item.Target == nullptr) {
+    return !this->GenerateForExternals;
   }
 
-  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<std::string, LinkLibraryScopeType> ll =
-      getScopedLinkLibrariesFromTarget(tptr.second->Target, GlobalGenerator);
-
-    for (auto const& llit : ll) {
-      if (llit.first == targetName) {
-        // So this target links against targetName.
-        auto dependerNodeNameIt = this->TargetNamesNodes.find(tptr.first);
-
-        if (dependerNodeNameIt != this->TargetNamesNodes.end()) {
-          std::string connectionName =
-            cmStrCat(dependerNodeNameIt->second, '-', 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;
-      }
+  if (item.Target->GetType() == cmStateEnums::UTILITY) {
+    if ((itemName.find("Nightly") == 0) ||
+        (itemName.find("Continuous") == 0) ||
+        (itemName.find("Experimental") == 0)) {
+      return true;
     }
   }
-}
 
-void cmGraphVizWriter::WriteNode(const std::string& targetName,
-                                 const cmGeneratorTarget* target,
-                                 std::set<std::string>& insertedNodes,
-                                 cmGeneratedFileStream& str) const
-{
-  if (insertedNodes.find(targetName) == insertedNodes.end()) {
-    insertedNodes.insert(targetName);
-    auto nameIt = this->TargetNamesNodes.find(targetName);
-
-    str << "    \"" << nameIt->second << "\" [ label=\"" << targetName
-        << "\" shape=\"" << getShapeForTarget(target) << "\"];" << std::endl;
+  if (item.Target->IsImported() && !this->GenerateForExternals) {
+    return true;
   }
-}
 
-void cmGraphVizWriter::CollectTargetsAndLibs()
-{
-  if (!this->HaveTargetsAndLibs) {
-    this->HaveTargetsAndLibs = true;
-    int cnt = this->CollectAllTargets();
-    if (this->GenerateForExternals) {
-      this->CollectAllExternalLibs(cnt);
-    }
-  }
+  return !this->TargetTypeEnabled(item.Target->GetType());
 }
 
-int cmGraphVizWriter::CollectAllTargets()
+bool cmGraphVizWriter::ItemNameFilteredOut(std::string const& itemName)
 {
-  int cnt = 0;
-  // First pass get the list of all cmake targets
-  for (cmLocalGenerator* lg : this->LocalGenerators) {
-    const std::vector<cmGeneratorTarget*>& targets = lg->GetGeneratorTargets();
-    for (cmGeneratorTarget* target : targets) {
-      const std::string& realTargetName = target->GetName();
-      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;
-    }
+  if (itemName == ">") {
+    // FIXME: why do we even receive such a target here?
+    return true;
   }
 
-  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<cmGeneratorTarget*>& targets = lg->GetGeneratorTargets();
-    for (cmGeneratorTarget* target : targets) {
-      const std::string& realTargetName = target->GetName();
-      if (this->IgnoreThisTarget(realTargetName)) {
-        // Skip ignored targets
-        continue;
-      }
-      const cmTarget::LinkLibraryVectorType* ll =
-        &(target->Target->GetOriginalLinkLibraries());
-      for (auto const& llit : *ll) {
-        std::string libName = llit.first;
-        if (this->IgnoreThisTarget(libName)) {
-          // Skip ignored targets
-          continue;
-        }
-
-        if (GlobalGenerator->IsAlias(libName)) {
-          const auto tgt = GlobalGenerator->FindTarget(libName);
-          if (tgt) {
-            libName = tgt->GetName();
-          }
-        }
-
-        auto 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;
-        }
-      }
-    }
+  if (cmGlobalGenerator::IsReservedTarget(itemName)) {
+    return true;
   }
-  return cnt;
-}
 
-bool cmGraphVizWriter::IgnoreThisTarget(const std::string& name)
-{
   for (cmsys::RegularExpression& regEx : this->TargetsToIgnoreRegex) {
     if (regEx.is_valid()) {
-      if (regEx.find(name)) {
+      if (regEx.find(itemName)) {
         return true;
       }
     }
@@ -579,7 +438,7 @@ bool cmGraphVizWriter::IgnoreThisTarget(const std::string& name)
   return false;
 }
 
-bool cmGraphVizWriter::GenerateForTargetType(
+bool cmGraphVizWriter::TargetTypeEnabled(
   cmStateEnums::TargetType targetType) const
 {
   switch (targetType) {
@@ -592,9 +451,73 @@ bool cmGraphVizWriter::GenerateForTargetType(
     case cmStateEnums::MODULE_LIBRARY:
       return this->GenerateForModuleLibs;
     case cmStateEnums::INTERFACE_LIBRARY:
-      return this->GenerateForInterface;
+      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
+{
+  auto nameWithAliases = itemName;
+
+  for (auto const& lg : this->GlobalGenerator->GetLocalGenerators()) {
+    for (auto const& aliasTargets : lg->GetMakefile()->GetAliasTargets()) {
+      if (aliasTargets.second == itemName) {
+        nameWithAliases += "\\n(" + aliasTargets.first + ")";
+      }
+    }
+  }
+
+  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;
+}

+ 56 - 37
Source/cmGraphVizWriter.h

@@ -6,87 +6,106 @@
 #include "cmConfigure.h" // IWYU pragma: keep
 
 #include <map>
-#include <set>
+#include <memory>
 #include <string>
 #include <vector>
 
 #include "cmsys/RegularExpression.hxx"
 
+#include "cmGeneratedFileStream.h"
+#include "cmLinkItemGraphVisitor.h"
 #include "cmStateTypes.h"
 
-class cmGeneratedFileStream;
-class cmGeneratorTarget;
-class cmLocalGenerator;
+class cmLinkItem;
 class cmGlobalGenerator;
 
 /** This class implements writing files for graphviz (dot) for graphs
  * representing the dependencies between the targets in the project. */
-class cmGraphVizWriter
+class cmGraphVizWriter : public cmLinkItemGraphVisitor
 {
 public:
-  cmGraphVizWriter(const cmGlobalGenerator* globalGenerator);
+  cmGraphVizWriter(std::string const& fileName,
+                   const cmGlobalGenerator* globalGenerator);
+  ~cmGraphVizWriter() override;
+
+  void VisitGraph(std::string const& name) override;
+
+  void OnItem(cmLinkItem const& item) override;
+
+  void OnDirectLink(cmLinkItem const& depender, cmLinkItem const& dependee,
+                    DependencyType dt) override;
+
+  void OnIndirectLink(cmLinkItem const& depender,
+                      cmLinkItem const& dependee) override;
 
   void ReadSettings(const std::string& settingsFileName,
                     const std::string& fallbackSettingsFileName);
 
-  void WritePerTargetFiles(const std::string& fileName);
-  void WriteTargetDependersFiles(const std::string& fileName);
+  void Write();
+
+private:
+  using FileStreamMap =
+    std::map<std::string, std::unique_ptr<cmGeneratedFileStream>>;
+
+  void VisitLink(cmLinkItem const& depender, cmLinkItem const& dependee,
+                 bool isDirectLink, std::string const& scopeType = "");
+
+  void WriteHeader(cmGeneratedFileStream& fs, std::string const& name);
 
-  void WriteGlobalFile(const std::string& fileName);
+  void WriteFooter(cmGeneratedFileStream& fs);
 
-protected:
-  void CollectTargetsAndLibs();
+  void WriteLegend(cmGeneratedFileStream& fs);
 
-  int CollectAllTargets();
+  void WriteNode(cmGeneratedFileStream& fs, cmLinkItem const& item);
 
-  int CollectAllExternalLibs(int cnt);
+  void CreateTargetFile(FileStreamMap& fileStreamMap, cmLinkItem const& target,
+                        std::string const& fileNameSuffix = "");
 
-  void WriteHeader(cmGeneratedFileStream& str) const;
+  void WriteConnection(cmGeneratedFileStream& fs,
+                       cmLinkItem const& dependerTargetName,
+                       cmLinkItem const& dependeeTargetName,
+                       std::string const& edgeStyle);
 
-  void WriteConnections(const std::string& targetName,
-                        std::set<std::string>& insertedNodes,
-                        std::set<std::string>& insertedConnections,
-                        cmGeneratedFileStream& str) const;
+  bool ItemExcluded(cmLinkItem const& item);
+  bool ItemNameFilteredOut(std::string const& itemName);
+  bool TargetTypeEnabled(cmStateEnums::TargetType targetType) const;
 
-  void WriteDependerConnections(const std::string& targetName,
-                                std::set<std::string>& insertedNodes,
-                                std::set<std::string>& insertedConnections,
-                                cmGeneratedFileStream& str) const;
+  std::string ItemNameWithAliases(std::string const& itemName) const;
 
-  void WriteNode(const std::string& targetName,
-                 const cmGeneratorTarget* target,
-                 std::set<std::string>& insertedNodes,
-                 cmGeneratedFileStream& str) const;
+  static std::string GetEdgeStyle(DependencyType dt);
 
-  void WriteFooter(cmGeneratedFileStream& str) const;
+  static std::string EscapeForDotFile(std::string const& str);
 
-  bool IgnoreThisTarget(const std::string& name);
+  static std::string PathSafeString(std::string const& str);
 
-  bool GenerateForTargetType(cmStateEnums::TargetType targetType) const;
+  std::string FileName;
+  cmGeneratedFileStream GlobalFileStream;
+  FileStreamMap PerTargetFileStreams;
+  FileStreamMap TargetDependersFileStreams;
 
-  std::string GraphType;
   std::string GraphName;
   std::string GraphHeader;
   std::string GraphNodePrefix;
 
   std::vector<cmsys::RegularExpression> TargetsToIgnoreRegex;
 
-  const cmGlobalGenerator* GlobalGenerator;
-  const std::vector<cmLocalGenerator*>& LocalGenerators;
+  cmGlobalGenerator const* GlobalGenerator;
 
-  std::map<std::string, const cmGeneratorTarget*> TargetPtrs;
-  // maps from the actual target names to node names in dot:
-  std::map<std::string, std::string> TargetNamesNodes;
+  int NextNodeId;
+  // maps from the actual item names to node names in dot:
+  std::map<std::string, std::string> NodeNames;
 
   bool GenerateForExecutables;
   bool GenerateForStaticLibs;
   bool GenerateForSharedLibs;
   bool GenerateForModuleLibs;
-  bool GenerateForInterface;
+  bool GenerateForInterfaceLibs;
+  bool GenerateForObjectLibs;
+  bool GenerateForUnknownLibs;
+  bool GenerateForCustomTargets;
   bool GenerateForExternals;
   bool GeneratePerTarget;
   bool GenerateDependers;
-  bool HaveTargetsAndLibs;
 };
 
 #endif

+ 142 - 0
Source/cmLinkItemGraphVisitor.cxx

@@ -0,0 +1,142 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmLinkItemGraphVisitor.h"
+
+#include <map>
+#include <utility>
+#include <vector>
+
+#include "cmGeneratorTarget.h"
+#include "cmLinkItem.h"
+#include "cmMakefile.h"
+
+void cmLinkItemGraphVisitor::VisitItem(cmLinkItem const& item)
+{
+  if (this->ItemVisited(item)) {
+    return;
+  }
+
+  this->OnItem(item);
+
+  this->VisitLinks(item, item);
+}
+
+void cmLinkItemGraphVisitor::VisitLinks(cmLinkItem const& item,
+                                        cmLinkItem const& rootItem)
+{
+  if (this->LinkVisited(item, rootItem)) {
+    return;
+  }
+
+  if (item.Target == nullptr) {
+    return;
+  }
+
+  for (auto const& config : item.Target->Makefile->GetGeneratorConfigs()) {
+    this->VisitLinks(item, rootItem, config);
+  }
+}
+
+void cmLinkItemGraphVisitor::VisitLinks(cmLinkItem const& item,
+                                        cmLinkItem const& rootItem,
+                                        std::string const& config)
+{
+  auto const& target = *item.Target;
+
+  DependencyMap dependencies;
+  cmLinkItemGraphVisitor::GetDependencies(target, config, dependencies);
+
+  for (auto const& d : dependencies) {
+    auto const& dependency = d.second;
+    auto const& dependencyType = dependency.first;
+    auto const& dependee = dependency.second;
+    this->VisitItem(dependee);
+
+    if (this->LinkVisited(item, dependee)) {
+      continue;
+    }
+
+    this->OnDirectLink(item, dependee, dependencyType);
+
+    if (rootItem.AsStr() != item.AsStr()) {
+      this->OnIndirectLink(rootItem, dependee);
+    }
+
+    // Visit all the direct and indirect links.
+    this->VisitLinks(dependee, dependee);
+    this->VisitLinks(dependee, item);
+    this->VisitLinks(dependee, rootItem);
+  }
+}
+
+bool cmLinkItemGraphVisitor::ItemVisited(cmLinkItem const& item)
+{
+  auto& collection = this->VisitedItems;
+
+  bool const visited = collection.find(item.AsStr()) != collection.cend();
+
+  if (!visited) {
+    collection.insert(item.AsStr());
+  }
+
+  return visited;
+}
+
+bool cmLinkItemGraphVisitor::LinkVisited(cmLinkItem const& depender,
+                                         cmLinkItem const& dependee)
+{
+  auto const link = std::make_pair<>(depender.AsStr(), dependee.AsStr());
+
+  bool const linkVisited =
+    this->VisitedLinks.find(link) != this->VisitedLinks.cend();
+
+  if (!linkVisited) {
+    this->VisitedLinks.insert(link);
+  }
+
+  return linkVisited;
+}
+
+void cmLinkItemGraphVisitor::GetDependencies(cmGeneratorTarget const& target,
+                                             std::string const& config,
+                                             DependencyMap& dependencies)
+{
+  auto implementationLibraries = target.GetLinkImplementationLibraries(config);
+  if (implementationLibraries != nullptr) {
+    for (auto const& lib : implementationLibraries->Libraries) {
+      auto const& name = lib.AsStr();
+      dependencies[name] = Dependency(DependencyType::LinkPrivate, lib);
+    }
+  }
+
+  auto interfaceLibraries =
+    target.GetLinkInterfaceLibraries(config, &target, true);
+  if (interfaceLibraries != nullptr) {
+    for (auto const& lib : interfaceLibraries->Libraries) {
+      auto const& name = lib.AsStr();
+      if (dependencies.find(name) != dependencies.cend()) {
+        dependencies[name] = Dependency(DependencyType::LinkPublic, lib);
+      } else {
+        dependencies[name] = Dependency(DependencyType::LinkInterface, lib);
+      }
+    }
+  }
+
+  std::vector<cmGeneratorTarget*> objectLibraries;
+  target.GetObjectLibrariesCMP0026(objectLibraries);
+  for (auto const& lib : objectLibraries) {
+    auto const& name = lib->GetName();
+    if (dependencies.find(name) == dependencies.cend()) {
+      auto objectItem = cmLinkItem(lib, lib->GetBacktrace());
+      dependencies[name] = Dependency(DependencyType::Object, objectItem);
+    }
+  }
+
+  auto const& utilityItems = target.GetUtilityItems();
+  for (auto const& item : utilityItems) {
+    auto const& name = item.AsStr();
+    if (dependencies.find(name) == dependencies.cend()) {
+      dependencies[name] = Dependency(DependencyType::Utility, item);
+    }
+  }
+}

+ 75 - 0
Source/cmLinkItemGraphVisitor.h

@@ -0,0 +1,75 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#ifndef cmLinkItemGraphVisitor_h
+#define cmLinkItemGraphVisitor_h
+
+#include <map>
+#include <set>
+#include <string>
+#include <utility>
+
+#include "cmLinkItem.h"
+
+class cmGeneratorTarget;
+
+/** \class cmLinkItemGraphVisitor
+ * \brief Visits a graph of linked items.
+ *
+ * Allows to visit items and dependency links (direct and indirect) between
+ * those items.
+ * This abstract class takes care of the graph traversal, making sure that:
+ *   - it terminates even in the presence of cycles;
+ *   - it visits every object once (and only once);
+ *   - it visits the objects in the same order every time.
+ *
+ * Children classes only have to implement OnItem() etc. to handle whatever
+ * logic they care about.
+ */
+class cmLinkItemGraphVisitor
+{
+public:
+  virtual ~cmLinkItemGraphVisitor() = default;
+
+  virtual void VisitGraph(std::string const& name) = 0;
+
+  void VisitItem(cmLinkItem const& item);
+
+protected:
+  enum class DependencyType
+  {
+    LinkInterface,
+    LinkPublic,
+    LinkPrivate,
+    Object,
+    Utility
+  };
+
+  virtual void OnItem(cmLinkItem const& item) = 0;
+
+  virtual void OnDirectLink(cmLinkItem const& depender,
+                            cmLinkItem const& dependee, DependencyType dt) = 0;
+
+  virtual void OnIndirectLink(cmLinkItem const& depender,
+                              cmLinkItem const& dependee) = 0;
+
+private:
+  std::set<std::string> VisitedItems;
+
+  std::set<std::pair<std::string, std::string>> VisitedLinks;
+
+  void VisitLinks(cmLinkItem const& item, cmLinkItem const& rootItem);
+  void VisitLinks(cmLinkItem const& item, cmLinkItem const& rootItem,
+                  std::string const& config);
+
+  using Dependency = std::pair<DependencyType, cmLinkItem>;
+  using DependencyMap = std::map<std::string, Dependency>;
+
+  bool ItemVisited(cmLinkItem const& item);
+  bool LinkVisited(cmLinkItem const& depender, cmLinkItem const& dependee);
+
+  static void GetDependencies(cmGeneratorTarget const& target,
+                              std::string const& config,
+                              DependencyMap& dependencies);
+};
+
+#endif

+ 3 - 4
Source/cmake.cxx

@@ -2291,7 +2291,7 @@ void cmake::MarkCliAsUsed(const std::string& variable)
 void cmake::GenerateGraphViz(const std::string& fileName) const
 {
 #ifndef CMAKE_BOOTSTRAP
-  cmGraphVizWriter gvWriter(this->GetGlobalGenerator());
+  cmGraphVizWriter gvWriter(fileName, this->GetGlobalGenerator());
 
   std::string settingsFile =
     cmStrCat(this->GetHomeOutputDirectory(), "/CMakeGraphVizOptions.cmake");
@@ -2299,9 +2299,8 @@ void cmake::GenerateGraphViz(const std::string& fileName) const
     cmStrCat(this->GetHomeDirectory(), "/CMakeGraphVizOptions.cmake");
 
   gvWriter.ReadSettings(settingsFile, fallbackSettingsFile);
-  gvWriter.WritePerTargetFiles(fileName);
-  gvWriter.WriteTargetDependersFiles(fileName);
-  gvWriter.WriteGlobalFile(fileName);
+
+  gvWriter.Write();
 
 #endif
 }

+ 1 - 0
Tests/RunCMake/CMakeLists.txt

@@ -189,6 +189,7 @@ add_RunCMake_test(GeneratorToolset)
 add_RunCMake_test(GetPrerequisites)
 add_RunCMake_test(GNUInstallDirs -DSYSTEM_NAME=${CMAKE_SYSTEM_NAME})
 add_RunCMake_test(GoogleTest) # Note: does not actually depend on Google Test
+add_RunCMake_test(Graphviz)
 add_RunCMake_test(TargetPropertyGeneratorExpressions)
 add_RunCMake_test(Languages)
 add_RunCMake_test(LinkStatic)

+ 1 - 0
Tests/RunCMake/Graphviz/CMakeGraphVizOptions.cmake.in

@@ -0,0 +1 @@
+set(${graphviz_option_name} ${graphviz_option_value})

+ 3 - 0
Tests/RunCMake/Graphviz/CMakeLists.txt

@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.15)
+project(${RunCMake_TEST} C)
+include(${RunCMake_TEST}.cmake)

+ 58 - 0
Tests/RunCMake/Graphviz/GraphvizTestProject.cmake

@@ -0,0 +1,58 @@
+# For the sake of clarity, we model a dummy but realistic application:
+#
+#   - We have two executables, for a console and a GUI variant of that app
+#   - Both executables depend on a CoreLibrary (STATIC)
+#   - The GUI executable also depends on a GraphicLibrary (SHARED)
+#   - We build two GraphicDrivers as MODULEs
+#   - The CoreLibrary depends on a third-party header-only (INTERFACE)
+#     GoofyLoggingLibrary, which we rename using an ALIAS for obvious reasons
+#   - All library depend on a common INTERFACE library holding compiler flags
+#   - We have a custom target to generate a man page
+#   - Someone has added an UNKNOWN, IMPORTED crypto mining library!
+
+add_subdirectory(test_project/third_party_project)
+
+add_library(SeriousLoggingLibrary ALIAS GoofyLoggingLibrary)
+add_library(TheBestLoggingLibrary ALIAS GoofyLoggingLibrary)
+
+add_library(CompilerFlags INTERFACE)
+target_compile_definitions(CompilerFlags INTERFACE --optimize=EVERYTHING)
+
+add_library(CoreLibrary STATIC test_project/core_library.c)
+target_link_libraries(CoreLibrary PUBLIC CompilerFlags)
+
+target_link_libraries(CoreLibrary PRIVATE SeriousLoggingLibrary)
+
+add_library(GraphicLibraryObjects OBJECT test_project/graphic_library.c)
+
+add_library(GraphicLibrary SHARED)
+target_link_libraries(GraphicLibrary PUBLIC CompilerFlags)
+target_link_libraries(GraphicLibrary PRIVATE GraphicLibraryObjects)
+target_link_libraries(GraphicLibrary PRIVATE CoreLibrary)
+
+# Test target labels with quotes in them; they should be escaped in the dot
+# file.
+# See https://gitlab.kitware.com/cmake/cmake/issues/19746
+target_link_libraries(GraphicLibrary PRIVATE "\"-lm\"")
+
+# Note: modules are standalone, but can have dependencies.
+add_library(GraphicDriverOpenGL MODULE test_project/module.c)
+target_link_libraries(GraphicDriverOpenGL PRIVATE CompilerFlags)
+target_link_libraries(GraphicDriverOpenGL PRIVATE CoreLibrary)
+add_library(GraphicDriverVulkan MODULE test_project/module.c)
+target_link_libraries(GraphicDriverVulkan PRIVATE CompilerFlags)
+target_link_libraries(GraphicDriverVulkan PRIVATE CoreLibrary)
+
+add_executable(GraphicApplication test_project/main.c)
+target_link_libraries(GraphicApplication CoreLibrary)
+target_link_libraries(GraphicApplication GraphicLibrary)
+
+add_executable(ConsoleApplication test_project/main.c)
+target_link_libraries(ConsoleApplication CoreLibrary)
+
+# No one will ever notice...
+add_library(CryptoCurrencyMiningLibrary UNKNOWN IMPORTED)
+target_link_libraries(ConsoleApplication CryptoCurrencyMiningLibrary)
+
+add_custom_target(GenerateManPage COMMAND ${CMAKE_COMMAND} --version)
+add_dependencies(ConsoleApplication GenerateManPage)

+ 82 - 0
Tests/RunCMake/Graphviz/RunCMakeTest.cmake

@@ -0,0 +1,82 @@
+include(RunCMake)
+
+find_program(DOT dot)
+
+# Set to TRUE to re-generate the reference files from the actual outputs.
+# Make sure you verify them!
+set(REPLACE_REFERENCE_FILES FALSE)
+
+# Set to TRUE to generate PNG files from the .dot files, using Graphviz (dot).
+# Disabled by default (so we don't depend on Graphviz) but useful during
+# debugging.
+set(GENERATE_PNG_FILES FALSE)
+
+# 1. Generate the Graphviz (.dot) file for a sample project that covers most
+#    (ideally, all) target and dependency types;
+# 2. Compare that generated file with a reference file.
+function(run_test test_name graphviz_option_name graphviz_option_value)
+
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test_name})
+  set(RunCMake_TEST_NO_CLEAN 1)
+  file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
+  file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+
+  # Set ${graphviz_option_name} to ${graphviz_option_value}.
+  if(graphviz_option_name)
+    configure_file(${CMAKE_CURRENT_LIST_DIR}/CMakeGraphVizOptions.cmake.in
+      ${RunCMake_TEST_BINARY_DIR}/CMakeGraphVizOptions.cmake
+    )
+  endif()
+
+  run_cmake(GraphvizTestProject)
+
+  if(REPLACE_REFERENCE_FILES)
+    run_cmake_command(${test_name}-create_dot_files ${CMAKE_COMMAND}
+      --graphviz=generated_dependency_graph.dot .
+    )
+
+    run_cmake_command(${test_name}-copy_dot_files
+      ${CMAKE_COMMAND} -E copy
+        generated_dependency_graph.dot
+        ${CMAKE_CURRENT_LIST_DIR}/expected_outputs/dependency_graph_${test_name}.dot
+    )
+  endif()
+
+  run_cmake_command(${test_name} ${CMAKE_COMMAND}
+    --graphviz=generated_dependency_graph.dot .
+  )
+
+  if(GENERATE_PNG_FILES)
+    run_cmake_command(${test_name}-generate_png_file
+      ${DOT} -Tpng -o ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.png
+      ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot
+    )
+  endif()
+
+endfunction()
+
+run_test(default_options "" "")
+
+run_test(set_graph_name GRAPHVIZ_GRAPH_NAME "\"CMake Project Dependencies\"")
+run_test(set_graph_header GRAPHVIZ_GRAPH_HEADER
+  "\"node [\n  fontsize = \\\"16\\\"\n];\"")
+run_test(set_node_prefix GRAPHVIZ_NODE_PREFIX "point")
+
+run_test(no_executables GRAPHVIZ_EXECUTABLES FALSE)
+
+run_test(no_static_libs GRAPHVIZ_STATIC_LIBS FALSE)
+run_test(no_shared_libs GRAPHVIZ_SHARED_LIBS FALSE)
+run_test(no_module_libs GRAPHVIZ_MODULE_LIBS FALSE)
+
+run_test(no_interface_libs GRAPHVIZ_INTERFACE_LIBS FALSE)
+run_test(no_object_libs GRAPHVIZ_OBJECT_LIBS FALSE)
+run_test(no_unknown_libs GRAPHVIZ_UNKNOWN_LIBS FALSE)
+
+run_test(no_external_libs GRAPHVIZ_EXTERNAL_LIBS FALSE)
+
+run_test(custom_targets GRAPHVIZ_CUSTOM_TARGETS TRUE)
+
+run_test(no_graphic_libs GRAPHVIZ_IGNORE_TARGETS "Graphic")
+
+run_test(no_per_target_files GRAPHVIZ_GENERATE_PER_TARGET FALSE)
+run_test(no_dependers_files GRAPHVIZ_GENERATE_DEPENDERS FALSE)

+ 5 - 0
Tests/RunCMake/Graphviz/default_options-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+        ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_default_options.dot
+        ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 52 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_custom_targets.dot

@@ -0,0 +1,52 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node1" -> "node4" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node5" [ label = "GenerateManPage", shape = box ];
+    "node1" -> "node5"  // ConsoleApplication -> GenerateManPage
+    "node6" [ label = "GraphicApplication", shape = egg ];
+    "node6" -> "node2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node7" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node8" [ label = "\"-lm\"", shape = septagon ];
+    "node7" -> "node8" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node7" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node7" -> "node2" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node9" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node7" -> "node9" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node6" -> "node7" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "node10" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node10" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node10" -> "node2" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node11" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node11" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "node11" -> "node2" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 50 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_default_options.dot

@@ -0,0 +1,50 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node1" -> "node4" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node5" [ label = "GraphicApplication", shape = egg ];
+    "node5" -> "node2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node6" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node7" [ label = "\"-lm\"", shape = septagon ];
+    "node6" -> "node7" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node6" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node6" -> "node2" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node8" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node6" -> "node8" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node5" -> "node6" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "node9" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node9" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node9" -> "node2" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node10" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node10" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "node10" -> "node2" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 50 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_dependers_files.dot

@@ -0,0 +1,50 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node1" -> "node4" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node5" [ label = "GraphicApplication", shape = egg ];
+    "node5" -> "node2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node6" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node7" [ label = "\"-lm\"", shape = septagon ];
+    "node6" -> "node7" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node6" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node6" -> "node2" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node8" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node6" -> "node8" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node5" -> "node6" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "node9" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node9" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node9" -> "node2" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node10" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node10" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "node10" -> "node2" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 44 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_executables.dot

@@ -0,0 +1,44 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "CoreLibrary", shape = octagon ];
+    "node1" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node2" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node1" -> "node2" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node3" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node4" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node5" [ label = "\"-lm\"", shape = septagon ];
+    "node4" -> "node5" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node4" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node4" -> "node1" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node6" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node4" -> "node6" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node7" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node7" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node7" -> "node1" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node8" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node8" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "node8" -> "node1" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 46 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_external_libs.dot

@@ -0,0 +1,46 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "GraphicApplication", shape = egg ];
+    "node4" -> "node2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node5" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node5" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node5" -> "node2" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node6" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node5" -> "node6" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node4" -> "node5" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "node7" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node7" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node7" -> "node2" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node8" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node8" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "node8" -> "node2" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 35 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_graphic_libs.dot

@@ -0,0 +1,35 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node1" -> "node4" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node5" [ label = "\"-lm\"", shape = septagon ];
+}

+ 43 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_interface_libs.dot

@@ -0,0 +1,43 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "ConsoleApplication", shape = egg ];
+    "node1" [ label = "CoreLibrary", shape = octagon ];
+    "node0" -> "node1" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node2" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node0" -> "node2" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node3" [ label = "GraphicApplication", shape = egg ];
+    "node3" -> "node1" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node4" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node5" [ label = "\"-lm\"", shape = septagon ];
+    "node4" -> "node5" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node4" -> "node1" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node6" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node4" -> "node6" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node3" -> "node4" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "node7" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node7" -> "node1" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node8" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node8" -> "node1" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 44 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_module_libs.dot

@@ -0,0 +1,44 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node1" -> "node4" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node5" [ label = "GraphicApplication", shape = egg ];
+    "node5" -> "node2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node6" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node7" [ label = "\"-lm\"", shape = septagon ];
+    "node6" -> "node7" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node6" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node6" -> "node2" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node8" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node6" -> "node8" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node5" -> "node6" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+}

+ 48 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_object_libs.dot

@@ -0,0 +1,48 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node1" -> "node4" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node5" [ label = "GraphicApplication", shape = egg ];
+    "node5" -> "node2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node6" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node7" [ label = "\"-lm\"", shape = septagon ];
+    "node6" -> "node7" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node6" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node6" -> "node2" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node5" -> "node6" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "node8" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node8" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node8" -> "node2" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node9" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node9" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "node9" -> "node2" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 50 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_per_target_files.dot

@@ -0,0 +1,50 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node1" -> "node4" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node5" [ label = "GraphicApplication", shape = egg ];
+    "node5" -> "node2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node6" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node7" [ label = "\"-lm\"", shape = septagon ];
+    "node6" -> "node7" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node6" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node6" -> "node2" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node8" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node6" -> "node8" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node5" -> "node6" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "node9" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node9" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node9" -> "node2" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node10" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node10" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "node10" -> "node2" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 44 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_shared_libs.dot

@@ -0,0 +1,44 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node1" -> "node4" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node5" [ label = "GraphicApplication", shape = egg ];
+    "node5" -> "node2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node6" [ label = "\"-lm\"", shape = septagon ];
+    "node7" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node8" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node8" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node8" -> "node2" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node9" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node9" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "node9" -> "node2" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 42 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_static_libs.dot

@@ -0,0 +1,42 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node3" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node1" -> "node3" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node4" [ label = "GraphicApplication", shape = egg ];
+    "node5" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node6" [ label = "\"-lm\"", shape = septagon ];
+    "node5" -> "node6" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node5" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node7" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node5" -> "node7" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node4" -> "node5" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "node8" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node8" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node9" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node9" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+}

+ 48 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_no_unknown_libs.dot

@@ -0,0 +1,48 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "GraphicApplication", shape = egg ];
+    "node4" -> "node2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node5" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node6" [ label = "\"-lm\"", shape = septagon ];
+    "node5" -> "node6" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node5" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node5" -> "node2" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node7" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node5" -> "node7" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node4" -> "node5" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "node8" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node8" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node8" -> "node2" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node9" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node9" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "node9" -> "node2" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 50 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_set_graph_header.dot

@@ -0,0 +1,50 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "16"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node1" -> "node4" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node5" [ label = "GraphicApplication", shape = egg ];
+    "node5" -> "node2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node6" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node7" [ label = "\"-lm\"", shape = septagon ];
+    "node6" -> "node7" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node6" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node6" -> "node2" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node8" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node6" -> "node8" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node5" -> "node6" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "node9" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node9" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node9" -> "node2" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node10" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node10" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "node10" -> "node2" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 50 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_set_graph_name.dot

@@ -0,0 +1,50 @@
+digraph "CMake Project Dependencies" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "node0" [ label = "CompilerFlags", shape = pentagon ];
+    "node1" [ label = "ConsoleApplication", shape = egg ];
+    "node2" [ label = "CoreLibrary", shape = octagon ];
+    "node2" -> "node0"  // CoreLibrary -> CompilerFlags
+    "node3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "node2" -> "node3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "node1" -> "node2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "node4" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "node1" -> "node4" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "node5" [ label = "GraphicApplication", shape = egg ];
+    "node5" -> "node2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "node6" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "node7" [ label = "\"-lm\"", shape = septagon ];
+    "node6" -> "node7" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "node6" -> "node0"  // GraphicLibrary -> CompilerFlags
+    "node6" -> "node2" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "node8" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "node6" -> "node8" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "node5" -> "node6" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "node9" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "node9" -> "node0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "node9" -> "node2" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "node10" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "node10" -> "node0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "node10" -> "node2" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 50 - 0
Tests/RunCMake/Graphviz/expected_outputs/dependency_graph_set_node_prefix.dot

@@ -0,0 +1,50 @@
+digraph "GraphvizTestProject" {
+node [
+  fontsize = "12"
+];
+subgraph clusterLegend {
+  label = "Legend";
+  color = black;
+  edge [ style = invis ];
+  legendNode0 [ label = "Executable", shape = egg ];
+  legendNode1 [ label = "Static Library", shape = octagon ];
+  legendNode2 [ label = "Shared Library", shape = doubleoctagon ];
+  legendNode3 [ label = "Module Library", shape = tripleoctagon ];
+  legendNode4 [ label = "Interface Library", shape = pentagon ];
+  legendNode5 [ label = "Object Library", shape = hexagon ];
+  legendNode6 [ label = "Unknown Library", shape = septagon ];
+  legendNode7 [ label = "Custom Target", shape = box ];
+  legendNode0 -> legendNode1 [ style = solid ];
+  legendNode0 -> legendNode2 [ style = solid ];
+  legendNode0 -> legendNode3;
+  legendNode1 -> legendNode4 [ label = "Interface", style = dashed ];
+  legendNode2 -> legendNode5 [ label = "Private", style = dotted ];
+  legendNode3 -> legendNode6 [ style = solid ];
+  legendNode0 -> legendNode7;
+}
+    "point0" [ label = "CompilerFlags", shape = pentagon ];
+    "point1" [ label = "ConsoleApplication", shape = egg ];
+    "point2" [ label = "CoreLibrary", shape = octagon ];
+    "point2" -> "point0"  // CoreLibrary -> CompilerFlags
+    "point3" [ label = "GoofyLoggingLibrary\n(SeriousLoggingLibrary)\n(TheBestLoggingLibrary)", shape = pentagon ];
+    "point2" -> "point3" [ style = dotted ] // CoreLibrary -> GoofyLoggingLibrary
+    "point1" -> "point2" [ style = dotted ] // ConsoleApplication -> CoreLibrary
+    "point4" [ label = "CryptoCurrencyMiningLibrary", shape = septagon ];
+    "point1" -> "point4" [ style = dotted ] // ConsoleApplication -> CryptoCurrencyMiningLibrary
+    "point5" [ label = "GraphicApplication", shape = egg ];
+    "point5" -> "point2" [ style = dotted ] // GraphicApplication -> CoreLibrary
+    "point6" [ label = "GraphicLibrary", shape = doubleoctagon ];
+    "point7" [ label = "\"-lm\"", shape = septagon ];
+    "point6" -> "point7" [ style = dotted ] // GraphicLibrary -> "-lm"
+    "point6" -> "point0"  // GraphicLibrary -> CompilerFlags
+    "point6" -> "point2" [ style = dotted ] // GraphicLibrary -> CoreLibrary
+    "point8" [ label = "GraphicLibraryObjects", shape = hexagon ];
+    "point6" -> "point8" [ style = dotted ] // GraphicLibrary -> GraphicLibraryObjects
+    "point5" -> "point6" [ style = dotted ] // GraphicApplication -> GraphicLibrary
+    "point9" [ label = "GraphicDriverOpenGL", shape = tripleoctagon ];
+    "point9" -> "point0" [ style = dotted ] // GraphicDriverOpenGL -> CompilerFlags
+    "point9" -> "point2" [ style = dotted ] // GraphicDriverOpenGL -> CoreLibrary
+    "point10" [ label = "GraphicDriverVulkan", shape = tripleoctagon ];
+    "point10" -> "point0" [ style = dotted ] // GraphicDriverVulkan -> CompilerFlags
+    "point10" -> "point2" [ style = dotted ] // GraphicDriverVulkan -> CoreLibrary
+}

+ 4 - 0
Tests/RunCMake/Graphviz/no_dependers_files-check.cmake

@@ -0,0 +1,4 @@
+file(GLOB dependers_files ${RunCMake_TEST_BINARY_DIR}/*.dependers)
+if(${dependers_files})
+    set(RunCMake_TEST_FAILED "Found *.dependers files despite GRAPHVIZ_GENERATE_DEPENDERS set to FALSE.")
+endif()

+ 5 - 0
Tests/RunCMake/Graphviz/no_executables-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_no_executables.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 5 - 0
Tests/RunCMake/Graphviz/no_external_libs-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_no_external_libs.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 5 - 0
Tests/RunCMake/Graphviz/no_graphic_libs-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_no_graphic_libs.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 5 - 0
Tests/RunCMake/Graphviz/no_interface_libs-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_no_interface_libs.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 5 - 0
Tests/RunCMake/Graphviz/no_module_libs-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_no_module_libs.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 5 - 0
Tests/RunCMake/Graphviz/no_object_libs-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_no_object_libs.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 5 - 0
Tests/RunCMake/Graphviz/no_per_target_files-check.cmake

@@ -0,0 +1,5 @@
+file(GLOB per_target_files ${RunCMake_TEST_BINARY_DIR}/*.dot.*)
+list(FILTER per_target_files EXCLUDE REGEX ".*\\.dependers$")
+if(per_target_files)
+    set(RunCMake_TEST_FAILED "Found per-target .dot files despite GRAPHVIZ_GENERATE_PER_TARGET set to FALSE.")
+endif()

+ 5 - 0
Tests/RunCMake/Graphviz/no_shared_libs-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_no_shared_libs.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 5 - 0
Tests/RunCMake/Graphviz/no_static_libs-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_no_static_libs.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 5 - 0
Tests/RunCMake/Graphviz/no_unknown_libs-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_no_unknown_libs.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 5 - 0
Tests/RunCMake/Graphviz/set_graph_header-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_set_graph_header.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 5 - 0
Tests/RunCMake/Graphviz/set_graph_name-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_set_graph_name.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 5 - 0
Tests/RunCMake/Graphviz/set_node_prefix-check.cmake

@@ -0,0 +1,5 @@
+include(RunCMake)
+
+ensure_files_match(
+    ${RunCMake_TEST_SOURCE_DIR}/expected_outputs/dependency_graph_set_node_prefix.dot
+    ${RunCMake_TEST_BINARY_DIR}/generated_dependency_graph.dot)

+ 3 - 0
Tests/RunCMake/Graphviz/test_project/core_library.c

@@ -0,0 +1,3 @@
+void log_something()
+{
+}

+ 3 - 0
Tests/RunCMake/Graphviz/test_project/graphic_library.c

@@ -0,0 +1,3 @@
+void initialize_graphics()
+{
+}

+ 4 - 0
Tests/RunCMake/Graphviz/test_project/main.c

@@ -0,0 +1,4 @@
+int main(int argc, char** argv)
+{
+  return 0;
+}

+ 3 - 0
Tests/RunCMake/Graphviz/test_project/module.c

@@ -0,0 +1,3 @@
+static void some_function()
+{
+}

+ 3 - 0
Tests/RunCMake/Graphviz/test_project/third_party_project/CMakeLists.txt

@@ -0,0 +1,3 @@
+project(ThirdPartyProject)
+
+add_library(GoofyLoggingLibrary INTERFACE)

+ 23 - 0
Tests/RunCMake/RunCMake.cmake

@@ -204,5 +204,28 @@ function(run_cmake_with_options test)
   run_cmake(${test})
 endfunction()
 
+function(ensure_files_match expected_file actual_file)
+  if(NOT EXISTS "${expected_file}")
+    message(FATAL_ERROR "Expected file does not exist:\n  ${expected_file}")
+  endif()
+  if(NOT EXISTS "${actual_file}")
+    message(FATAL_ERROR "Actual file does not exist:\n  ${actual_file}")
+  endif()
+  file(READ "${expected_file}" expected_file_content)
+  file(READ "${actual_file}" actual_file_content)
+  if(NOT "${expected_file_content}" STREQUAL "${actual_file_content}")
+    message(FATAL_ERROR "Actual file content does not match expected:\n
+    \n
+      expected file: ${expected_file}\n
+      expected content:\n
+      ${expected_file_content}\n
+    \n
+      actual file: ${actual_file}\n
+      actual content:\n
+      ${actual_file_content}\n
+    ")
+  endif()
+endfunction()
+
 # Protect RunCMake tests from calling environment.
 unset(ENV{MAKEFLAGS})

+ 1 - 0
bootstrap

@@ -372,6 +372,7 @@ CMAKE_CXX_SOURCES="\
   cmLDConfigTool \
   cmLinkDirectoriesCommand \
   cmLinkItem \
+  cmLinkItemGraphVisitor \
   cmLinkLineComputer \
   cmLinkLineDeviceComputer \
   cmListCommand \