Skip to content

Visualising Module Dependencies with CMake and Graphviz

CMake uses Graphviz to generate dependency graphs between the targets of a CMake project like libraries and executables. The graphs help us get an idea of the software architecture and identify the dependency hot spots.

Generating Dependency Graphs

My running example is the walking skeleton of the harvester terminal, which I introduced in my talk Hexagonal Architecture: The Standard for Qt Embedded Applications at Meeting Embedded 2021. The project comprises the executable MainApp, the test executables, the shared libraries BusinessLogicModels and Machine, and the QML module EmUse.Models. The project depends on several external libraries like Qt and OpenGL. Although the project is pretty simple, the graph needs some pruning to reflect the relevant parts of the architecture.

The documentation page CMakeGraphVizOptions describes how to generate and visualise dependency graphs. We clone the example project eu-terminal-apps into a directory of our choice and create a sibling directory for the generated dependency graphs. This post is based on commit 1f2a678 but you should be able to work along with older and newer versions.

$ git clone https://github.com/bstubert/eu-terminal-apps.git
$ mkdir deps-eu-terminal-apps
$ cd deps-eu-terminal-apps

In QtCreator, we run the action Build > Run CMake, copy the CMake command to a Linux terminal, add the option --graphviz=eu-terminal-1.dot, and run the command. My CMake command and results look as follows:

$ cmake -S ../eu-terminal-apps/ -B ../build-eu-terminal-apps-Desktop_Qt_6_2_0_GCC_64bit-Debug/ --graphviz=eu-terminal-1.dot
...
-- Generating done
Generate graphviz: /public/Projects/deps-eu-terminal-apps/eu-terminal-1.dot
-- Build files have been written to: /public/Projects/build-eu-terminal-apps-Desktop_Qt_6_2_0_GCC_64bit-Debug

The command generates a total of 59 .dot files with the following format.

  • eu-terminal-1.dot: the dependency graph of the complete project.
  • eu-terminal-1.dot.<target> for each target of the project: the graph on which other targets the <target> depends directly or indirectly. For example: eu-terminal-1.dot.Machine and eu-terminal-1.dot.Qt6SerialBus.
  • eu-terminal-1.dot.<target>.dependers for each target of the project: the graph of the other targets that depend on <target> directly or indirectly. For example: eu-terminal-1.dot.Machine.dependers and eu-terminal-1.dot.Qt6SerialBus.dependers.

The complete dependency graph is a too big to show here in the post. So, we generate the graph for the dependants and the dependers for the target Machine from eu-terminal-1.dot.Machine and eu-terminal-1.dot.Machine.dependers, respectively. We can view the generated SVG images with the tool display.

$ dot -Tsvg -o eu-terminal-1.dot.Machine.svg eu-terminal-1.dot.Machine
$ display eu-terminal-1.dot.Machine.svg &

$ dot -Tsvg -o eu-terminal-1.dot.Machine.dependers.svg eu-terminal-1.dot.Machine.dependers
$ display eu-terminal-1.dot.Machine.dependers.svg &

Here is the legend for the dependency graphs.

Legend for dependency graphs

The next graph was generated from eu-terminal-1.dot.Machine and shows the targets on which Machine depends directly or indirectly.

Targets on which Machine depends directly or indirectly

A look at eu-machine/src/CMakeLists.txt confirms that Machine depends on the shared libraries Qt6::Core and Qt6::SerialBus privately and that it must run Qt6::moc. The external dependencies reached from Machine don’t help understand the harvester terminal. Hence, we’ll exclude them from the graphs in section Excluding External Libraries.

The next graph was generated from eu-terminal-1.dot.Machine.dependers and shows the targets that depend on Machine.

As a composition root, the executable MainApp creates the BusinessLogic and the Machine object and links privately against the respective shared libraries. The module library QmlModuleBusinessLogicModels is a QML module and makes the C++ models from its backing library BusinessLogic available to QML.

The target test_main_model is a test executable written with QtTest. As a unit test, it should not depend on the library Machine, but use test doubles. Nevertheless, we discovered a little problem that we should fix. In general, test executables litter the depency graph and should be excluded (see section Excluding Test Executables)

Excluding External Libraries

External libraries don’t add much to the understanding of an unfamiliar software project. So, we exclude them from the graph. We create the file CMakeGraphVizOptions.cmake in the directory CMAKE_SOURCE_DIR or CMAKE_BINARY_DIR with the following content.

# CMakeGraphVizOptions.cmake
set(GRAPHVIZ_EXTERNAL_LIBS FALSE)

When we run CMake with the GraphViz option, it reads the options file CMakeGraphVizOptions.cmake as shown in the output.

$ cmake -S ../eu-terminal-apps/ -B ../build-eu-terminal-apps-Desktop_Qt_6_2_0_GCC_64bit-Debug/ --graphviz=eu-terminal-2.dot
...
-- Generating done
Generate graphviz: /public/Projects/deps-eu-terminal-apps/eu-terminal-2.dot
Reading GraphViz options file: /public/Projects/eu-terminal-apps/CMakeGraphVizOptions.cmake
-- Build files have been written to: /public/Projects/build-eu-terminal-apps-Desktop_Qt_6_2_0_GCC_64bit-Debug

The command results in 7 files starting with eu-terminal-2.dot – instead of the 59 files from the first run. We generate the complete the full dependency graph this time, as it will easily fit into this post .

$ dot -Tsvg -o eu-terminal-2.dot.svg eu-terminal-2.dot
$ display eu-terminal-2.dot.svg &

This is the full dependency graph.

Full dependency graph without external libraries

Excluding Test Executables

In the final pruning step, we get rid of the test executables by adding another line to CMakeGraphVizOptions.cmake.

# CMakeGraphVizOptions.cmake
set(GRAPHVIZ_EXTERNAL_LIBS FALSE)
set(GRAPHVIZ_IGNORE_TARGETS "test_*")

The option GRAPHVIZ_IGNORE_TARGETS is assigned a list of regular expressions of target names. The above setting ignores all targets starting with test_. We generate the graph with the following commands.

$ cmake -S ../eu-terminal-apps/ -B ../build-eu-terminal-apps-Desktop_Qt_6_2_0_GCC_64bit-Debug/ --graphviz=eu-terminal-3.dot
$ dot -Tsvg -o eu-terminal-3.dot.svg eu-terminal-3.dot
$ display eu-terminal-3.dot.svg &

The test executables are gone from the dependency graph.

Full dependency graph without test executables and without external libraries

More Options for Pruning

CMake’s GraphViz module provides some more options for pruning the dependency graph.

  • GRAPHVIZ_EXECUTABLES=FALSE – excludes all executables from the graph.
  • GRAPHVIZ_STATIC_LIBS=FALSE – exclude all static libraries from the graph.
  • GRAPHVIZ_SHARED_LIBS=FALSE – exclude all shared libraries from the graph.
  • GRAPHVIZ_MODULE_LIBS=FALSE – exclude all module libraries from the graph (e.g., all QML modules).
  • GRAPHVIZ_INTERFACE_LIBS=FALSE – exclude all interface libraries from the graph (e.g., header collections like Qt::SerialPort and Qt::Core).

7 thoughts on “Visualising Module Dependencies with CMake and Graphviz”

  1. I’ve found the graphviz route a bit clunky for visualizing class and module dependencies.

    The now defunct Coati Software company provides the now free SourceTrail program for indexing C++ projects (and a few other languages). All you need is a compile_commands.json file which cmake can be instructed to generate.

    I’ve found SourceTrail very useful for quickly navigating new projects and determine the dependencies for a certain component. Very handy when I’ve wanted to add unit testing to a project that hasn’t had any previous tests.

    There is a library for interfacing with the database if you want to write your own indexers or have other custom use cases.

    Here’s a link to the projects github page https://github.com/CoatiSoftware/Sourcetrail. Online you can find demo videos.

    Speaking of tools for understanding new codebases: pernos.co is what debuggers should have been all along and various semantic grep tools like https://github.com/googleprojectzero/weggli and https://semgrep.dev/ should be used by way more people.

    Thank you for your blog. I’ve learned a ton from reading your articles!

    1. Hi Daniel,

      Thank you very much for your pointers to Pernosco, Weggli and Semgrep. I didn’t know them so far.

      1.5 years ago, I had a look at CppDepend (https://embeddeduse.com/2020/08/13/cppdepend-a-c-dependency-analyser/). It didn’t get the job done on Linux. I didn’t have the time to evaluate SourceTrail then, my second choice. Although the development on SourceTrail was stopped, I am willing to give it a try.

      I don’t know yet, whether CMake plus Graphviz will help with bigger projects. I have a project with ~100 relevant targets and some more irrelevant ones. I first had to understand how to do things on a small project, before I do it on a big project. I’ll let you know how this works out.

      Cheers,
      Burkhard

  2. Could you elaborate on the doxygen graphs.? Since I already use doxygen, which is also generating dot files/svg files of its dependencies.

    What are the differences with your solution??

    1. Hi Melroy,

      I haven’t used doxygen to generate dependency graphs. I only know that it does.

      Neither my example project nor the big real-life project, for which I want to find out the dependencies, use doxygen for comments. So, I’d guess that I cannot use doxygen to generate the graphs. Or, can I?

      Cheers,
      Burkhard

      1. Yes, you can generate graphs for projects without any doxygen style commenting. This helps a great deal to understand the call(er) graphs, the class hierarchies and navigation through the code on a browser rather than a code editor. Thank you for this post, you should definitely try doxygen as well.

  3. NO, cmake is no good currently. Graphiz requires GUI setup. If I’m on a server without GUI, I will need to get dependencies from the command line list, so that I can build a huge project one part after the other and it make take a day or more to build all.Please tell how I can get the dependencies from the command line, eg. in a text file

  4. Hi Sandra,

    CMake generates a text-based .dot file describing the dependency graph. You can write a script, say, in Python that performs any analysis you want. It could, for example, check for cyclic dependencies. Your CI/CD pipeline calls this script and fails, if the script flags a dependency cycle. So, there is no need to call Graphviz.

    If I had to tackle a build taking longer than a day, I would do the following:
    (1) Remove all cyclic dependencies. They make builds fail or take very long (repeated builds of same library).
    (2) Remove all unused dependencies from the CMake files. You can find out which dependencies are not used by defining them private in target_link_libraries.
    (3) Use dynamic linking instead of static linking. Static linking takes ages, because it removes all unused symbols from the binaries linked together.
    (4) Find the bottlenecks, that is, components that take a very long time building and on which many other components depend. This gives you a prioritised list, which components to tackle first: the biggest bottlenecks.
    (5) Relentlessly apply the pimpl pattern (pointer to implementation) and forward declaration to remove #include dependencies from the headers. This speeds up builds, because compilation has to read a lot less include files.
    (6) Use faster build servers. You could move to the cloud for builds. You could use (mostly) idle computers in your network.

    Hope this helps a bit.

    Cheers,
    Burkhard

Leave a Reply

Your email address will not be published. Required fields are marked *