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).