March 15, 2019

Simplifying your C/C++ dependency graph (with CMake)

Sometimes, you'll come upon a set of libraries that just decide to include everything directly, always. This can help minimize and visualize those dependencies.

The Before

Typically first, looking at how the dependencies are laid out is the first objective. Luckily CMake, has integrated support for graphviz for building a visual graph showing this information, at least for the libraries under it’s own management.

CMake can generate a .dot file that can be used by the dot program from graphviz to output a file (of which there are many possible target types) to view, where directly added dependencies are represented by arrows.

$ cmake <SOURCE_DIR> --graphviz=test.dot
$ dot -Tpng test.dot -o out.png

Of course, like many things, this can be automated and turned into a CMake target to make the whole process just a tad easier:

find_program(DOT_EXE "dot")
if(DOT_EXE)
    message(STATUS "dot found: ${DOT_EXE}")
else()
    message(STATUS "dot not found!")
endif()

set(DOT_OUTPUT_TYPE "" CACHE STRING "Build a dependency graph. Options are dot output types: ps, png, pdf..." )

if(DOT_EXE)
    add_custom_target(dependency-graph
        COMMAND ${CMAKE_COMMAND} ${CMAKE_SOURCE_DIR} --graphviz=${CMAKE_BINARY_DIR}/graphviz/${PROJECT_NAME}.dot
        COMMAND ${DOT_EXE} -T${DOT_OUTPUT_TYPE} ${CMAKE_BINARY_DIR}/graphviz/${PROJECT_NAME}.dot -o ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.${DOT_OUTPUT_TYPE}
    )

    add_custom_command(
        TARGET dependency-graph POST_BUILD
        COMMAND ;
        COMMENT
        "Dependency graph generated and located at ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.${DOT_OUTPUT_TYPE}"
    )
endif()

Then calling it, such as make dependency-graph will generate something like below:

As can be seen it’s a little bit of a mess. Seemingly everyone decided to include most other items, Such as demosSandbox including dMath directly itself, through dCustomJoins, dNewton and dVehicle as grand children anyways. In one case includes the same library twice (dScene -> dContainers). Surely this can be simplified?

Simplification

Now, given the above image, one’s first step would normally be to go around through the make files and start removing direct dependencies wherever they may be, and this is normally effective. However, there can be times when the dependencies aren’t quite clear, for example where a library may list one that is higher up the tree than is required. For this, one can check what libraries a shared executable or shared object actually uses, by checking the inverse, or what is included but not used and can be removed. This can be done with the ldd tool, and using the -u option for ‘unused’ items:

$ ldd -u libname.so
Unused direct dependencies:
        /lib/lib1.so
        /lib/lib2.so

Armed with this information, you can prune down the imported items even further, and even more accurately determine how to structure links.

The After

So, doing the above for a while, one will be able to sort out who actually needs what, and simplify, and make clear the dependency ordering, and hopefully come up with something not only more visually appealing, but easier to keep in mind and use:

Implementation

A ready-to-go form of this can be found here as the dependency-graph.cmake file.