Introduction to CMake

CMake is incredible software you should learn and use. Why? It enables you to build C, C++, and other languages for almost any situation you can imagine. Moreover, you can do so more efficiently than with other tools because one tool works everywhere. And it’s used in many famous projects, so chances are high it will be a cinch to integrate your CMake-based project with others.

In the past, you may have used GNU Make or one of many IDEs to build your code. Luckily CMake works with many IDEs, such as Apple’s Xcode and Microsoft’s Visual Studio. In addition, where projects like QT had developed their build system called qmake, they are now switching to CMake. As a result, it has become a ubiquitous and universal tool everyone can use for many environments.

I contemplated using CMake ten years ago and decided it wasn’t something I wanted to learn. The ROI didn’t seem high enough. While it addressed simple projects quite handily, extending it to more complicated tasks or debugging problems with CMake seemed nebulous. Many things have changed since then. CMake has radically improved, and many people have jumped on the bandwagon. Indeed, its high level of adoption has become one of its strengths. I may have taken the plunge a little sooner.

I include a chart from JetBrains on the level of adoption of various build systems below. I’ve seen other similar studies elsewhere. Cppcon 2022 estimates CMake usage at 81%.

Cons

Let’s list some negatives before we go back to singing praises for CMake.

  • It’s large and sometimes complex
  • It lacked good documentation
  • It can be not easy to debug or troubleshoot

CMake gets larger and larger by the week. It’s aggressively supported and developed. It will likely never get smaller. However, this is also good as it has dramatically improved and become more potent with every release. If you’ve ever tried to fix issues with build systems in other large projects, you’ll realize CMake is not alone.

The documentation problem has turned a corner. There are more books about CMake today than a couple of years ago. It’s much easier to find websites to help you. This blog is another source of information to help you learn it. Check back using the tag cloud on the homepage for more info as I plan to continually add more info.

I have my share of CMake war stories. Supporting Windows builds has been non-trivial sometimes. I wish authors of more libraries would do a great job writing their CMake solutions. Debugging “find modules” has also been a pain point for a time or two. As CMake progresses, it has addressed many of these issues. For example, CMake gained CMAKE_FIND_DEBUG_MODE in version 3.17.

CMake addresses a larger set of challenges and situations than any other solution in this space. Therefore, it’s bound to have some rough-spots, but it is mature, feature-rich, and improving rapidly.

What Does it Do For Me?

CMake’s purpose is more evident once you’ve used it for a while. We call it a build system generator because it works in concert with tools like GNU Make, Ninja, or others. It converts instructions in the CMake language into the underlying build systems’ language. CMake is a higher level language than tools like GNU Make, so it’s more efficient and easier to use.

You instruct CMake about your project using C-like functions. Modern CMake primarily focuses on targets, so your job is to define what kind of target you have and how to build it. Your target definition includes files to compile into a binary, libraries to link it to, and directories to use. In addition, you can specify the C++ standard for your code, macros for building it, and the like.

CMake’s first job in a build is to convert its CMakeLists.txt file into a build system that works for the platform you’re targeting. The platform could be Windows, or flavors of Linux, Unix, MacOS, or Android. This is called config stage. Once the config is done you can do full and incremental builds of your solution in iterative fashion.

I recommend you leverage out-of-source builds. It keeps your workspace clean by keeping build artifacts separate from source code. It also enables you to configure multiple parallel builds with different settings in by using more than one build directory. You get this automatically with CMake. It can be a big time-saver for large projects.

More complex projects will require selectable features and options. CMake enables users to switch these on or off via the command line with -D arguments or ccmake, or using a visual method called cmake-gui. Features and options are saved in the build directory cache during the config phase.

CMake produces much more modular solutions than GNU Make. I find more flexibility in grouping related things together. It’s easier to give your meta-data a cleaner conceptual layout than when using lower-level tools.

CMake is mighty and flexible. Ideally, you can write one meta-data for your project and use your code everywhere. It isolates you from the variables required to support many potential platforms. CMake often just “magically” does the right thing for you. You’ll find times when things do not go so smoothly, but many others have gone before you. So there’s bound to be a way to complete your task.

Code

Let’s end this short introduction with a small example. In future articles, we’ll contrast it with GNU Make and show you how to use this single script to build on Linux, Windows, and MacOS. You can find this code in a repo at https://github.com/polishedprogrammer/polished-cmake/tree/main/cmake-intro.

Here’s the absolute minimum you need to build a small executable. It includes proper dependency generation, the ability to do out-of-source builds, and the ability to do a Debug and Release build.

add_executable(helloworld main.cpp info.h)

With a few more additions, we have a more typical CMakeLists.txt file. This solution includes constraints for the version of CMake users must use, a project directive that’s helpful for generators like Xcode, and an install target. The destination of the install target is not hard-coded like many Make projects, either. Instead, you can control the destination of your install targets using common CMake directives. So once you learn CMake, its consistency will be a comfortable tool to make you more efficient and effective.

cmake_require_minimum(version 3.14)
project(helloworld)
add_executable(helloworld main.cpp info.h)
install(TARGETS helloworld)

Summary

Next time in a series of posts about CMake, we’ll contrast this solution with a typical GNU Make file to look at the pros and cons in more detail. What do you think? Would you like to focus on something specific? Let us know.