CMake Multi-Project Template With Library, App, Tests

CMake is a powerful tool but can also be very complicated and daunting when starting out. Much of my C++ career took place in Microsoft’s Visual Studio on Windows, so I am mainly used to the IDE maintaining the build system and relying on a graphical interface to configure dependencies. I started my WorkTracker utility this way – Visual Studio in combination with the Qt plugin.

Eventually, I migrated to Qt’s build system, qmake, and after that, to CMake. This is how I managed to build WorkTracker on macOS. If I am honest, though, I took a minimalist approach and learned only as much as was necessary to get it working. I like building an application, not knowing about build tools.

As a result, the resulting build script was mostly a hodgepodge of somewhat modern and outdated CMake. My lack of more profound knowledge – which I still do not claim to have – and the convoluted CMakeLists file of WorkTracker somehow presented a mental obstacle for me to start improving it or build other C++ tools.

To remedy this situation, I started looking at the bare minimum modern CMake. I set up a template repository containing a library, an application based on that library, and a Googletest-based testing application. This should provide a good starting point for new projects and give me enough knowledge to slowly start dissecting parts out of WorkTracker and create one or more libraries from it.

The Parent

The concept is actually straightforward. The main CMakeLists defines the project and then includes the subprojects.

cmake_minimum_required(VERSION 3.24)

project(
    CMake-Lib-App-Template
    VERSION 1.0
    DESCRIPTION “CMake template project containing a library, an app using it, and tests”
    LANGUAGES CXX
)

add_subdirectory(lib)
add_subdirectory(app)
add_subdirectory(test)

The Bookworm

The first thing you must always do is define a target, irrespective of the target type (lib or app). Think of it as an object with several build-related properties.

For a library, you use add_library().

add_library(cmake-template-lib STATIC "")

The target’s name is “cmake-template-lib”, and it will also be used for the binary. There are ways to set a different output name, but I have not yet looked that up. For now, the target name is what I want for my binary. Also, for simplicity, I chose a static library. This saves me the additional work of managing a DLL or shared library file when running the tests and application.

Typically, you would specify the source and header files in the call to add_library(). However, you can omit the source files and add them later with target_sources(). This is more flexible and simplifies adding OS-specific files in if-else cases, for example.

The sample library has a single header and a source file to keep it simple.

target_sources(cmake-template-lib
    PRIVATE
        "src/fancy_function.cpp"
    PUBLIC
        "include/fancy_function.h"
)

So, what’s this about PRIVATE and PUBLIC? Does it have to do with inheritance? Yes and no, I suppose. Do not think of these keywords in the C++ sense. They are relevant to CMake in figuring out compile and link time dependencies – transitive usage requirements. CMake also defines a third keyword, INTERFACE.

  • PRIVATE – Used only for building the current target.
  • PUBLIC – Used for building the current target and other targets depending on the current target.
  • INTERFACE – Used for building other targets depending on the current target.

What does that mean for this sample library?

The PRIVATE sources or header files are internal to the “cmake-template-lib” target and will not be available to applications that link against this library. PUBLIC sources and headers are used for building “cmake-template-lib”, but they will also be made available for applications linking “cmake-template-lib”, like tests. Finally, INTERFACE defines files not used by “cmake-template-lib” itself but required when building applications linking against “cmake-template-lib”. I cannot come up with an example to which this would apply. I’m sure the smart folks at Kitware added this for a reason. There must be use cases where INTERFACE makes sense.

The final missing piece is setting the include path for the target. You do not necessarily require it for the library itself. When you want to use it, though, especially when it is not part of your source tree, you need a way to include its headers. Without an include path, you must set an explicit path for each and every header you include – which is not what you want.

target_include_directories(cmake-template-lib PUBLIC "include")

The Favorite Child

The application’s CMakeLists file is almost identical. Swap add_library() for add_executable() and declare a link dependency on “cmake-template-lib”.

add_executable(cmake-template-app "")

target_sources(cmake-template-app
    PRIVATE
        “src/main.cpp”
)

target_link_libraries(cmake-template-app 
    cmake-template-lib
)

Because the “cmake-template-lib” target is a form of an object with properties, CMake can extract the include path and the static library from it. All this happens by declaring the dependency in target_link_libraries().

The Black Sheep

Who likes to write tests? Raise your hands! I thought as much. However, we all know that they are helpful. Apart from the specifics required by Googletest, building the tests works in the same way as building the application.

add_executable(cmake-template-test "")

target_sources(cmake-template-test
    PRIVATE
        "src/test_fancy_function.cpp"
)

target_link_libraries(cmake-template-test 
    GTest::gtest_main 
    cmake-template-lib
)

I left out the Googletest pieces except for the link dependency, as target_link_libraries() is required anyway.

Famous Last Words

And that was it. As I wrote earlier, it is actually straightforward. Define targets, set “properties”, define dependencies between the targets, and CMake figures out the rest.

One brief note about setting header files in target_sources(): it is not required, yet recommended if you want the files to appear in your IDE. The compiler will find the files through the include directives and setting the target_include_directories().

This little exercise helped me better comprehend the bare minimum, and I will definitely make use of this template here and there. Remember that I only scratched the surface of what CMake is capable of. It can create header files from user configuration if your project requires that. I recall working with the DCMTK library at my first job, and it had soooo maaaany options. Qt is no different. I am still beginning my journey, and I could not adequately explain the differences between old CMake and modern CMake despite watching a few presentations on the topic.

If you want to learn more, here are several resources I use:

I hope this small template is useful to you, too. Here’s the repository on GitHub.

Thank you for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.