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.
The next graph was generated from eu-terminal-1.dot.Machine and shows the 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_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.
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_*")
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.
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).