CMake for Firebase Developers

CMakeLists ends with .txt, are you sure this is the right place?

Patrick Martin
Firebase Developers

--

The C++ Firebase SDK relies on CMake as its primary cross platform build system for both the open source SDK and its pre-compiled counterpart. Although this is a very popular build system, the C++ language doesn’t have a single set of build tools across all platforms. If you’ve come here wondering what a CMakeLists.txt or if .txt is some strange typo, I will give you a quick down to the basics primer on “modern CMake.”

The Setup

To start out, every CMake project has a file named CMakeLists.txt at its root. Individual subdirectories may have these as well, often to denote other libraries (such as Firebase). If at all possible, the hierarchy of these CMake files should always be top down.

Let’s start with a basic CMake project. Simply create a new folder and create a new file in it called CMakeLists.txt:

First, I define the minimum required version of CMake. I would generally recommend using the newest version possible, and for the purposes of this tutorial you should always have a version in the 3.x family. What your minimum version is will depend on your build environment. You can find out what version you have installed by typing cmake --version, but I often build Android projects and Android Studio’s CMake 3.10 is actually older than my computer’s 3.15.

Next I say that this is a project named sample_project. In Visual Studio this would be equivalent to a “solution”, and in Xcode this roughly approximates a “Workspace.”

Finally, I indicate that I’m building an executable named sample. It’s not required to have an executable in a project, typical Android applications only consist of shared libraries.

Let’s actually build this project. I’ll create a super simple main.cc:

Then I execute the following commands in a terminal open to my project’s directory (where my CMakeLists.txt is):

If I’ve done all these steps correctly, I will have a binary named sample that I can execute.

You can safely delete this build folder to remove all generated files, effectively doing a fail-proof clean build. This is why I opted to run cmake .. in a build/ directory rather than just running cmake at my project root.

CMake can create projects for build systems other than Make using the -G parameter. This specifies different “generators” to generate project files. For example, if you’re on Windows without make installed, you can type:

$ cmake -G "Visual Studio 16 2019"

to create a Windows solution. Visual Studio does natively support CMake now, so I actually recommend cutting out the middleman and just importing your CMakeLists.txt directly!

Similarly, if you’re a fan of Xcode, you can type:

$ cmake .. -G "Xcode"

Android Studio uses the “Ninja” build system. So I can type:

to build a project as Android Studio would.

Writing Libraries

A difficult part of cross platform C++ development is splitting up a project into multiple isolated reusable portions. In fact, I’d argue that the difficulty in linking together various libraries is why we consider header-only libraries a feature rather than a bug!

I’m going to create a small library that simply holds onto data about a player. So I’ll create a folder called player at the root of my project, then in that folder I’ll create a new CMakeLists.txt file that looks like this:

This defines a library named player that has one file named player.cc under the src subdirectory. I should note here that there are ways to include entire directories with wildcard matching patterns, but this seems to confuse many tools and IDEs that can work directly with CMake files. I find that it’s always easier, even on smaller prototype projects, to explicitly list all your source files.

Then I say that the player library keeps its header files under the subdirectory include. The word PUBLIC helps any target that requires the player library to find the related header files. You can also specify PRIVATE and INTERFACE for different amounts of access control.

Before I go over how to include this library, I’ll create a directory called include and one called src.

In include, create player/player.h and fill it with this:

Then in src create player.cc and fill it with this:

At this point, your directory structure should look like this:

Using Libraries

I’ll jump back up to the root of my project where my first CMakeLists.txt is and update it to read:

My first change is that I added the line add_subdirectory. This call takes a directory that contains a CMakeLists.txt file, in this case the player library I just wrote, and includes all the relevant bits from that file in this one. It is possible to include a parent directory (ex: ../), but this requires a little bit more effort (and you’ll likely want to investigate find_package instead).

Then I add the line target_link_libraries passing it my executable (sample) and my library (player). Because I had written target_include_directories(player include), target_link_libraries knows that source files in player may need to search player/include/ for header files (that is, it automatically transmitted the dependency and properly updated the relative path).

I can update main.cpp to read:

As is the sacred C++ tradition, you must use cin for a tutorial such as this one (remember that this will only work for a desktop terminal application). The important thing to call out is that I was able to call #include "player/player.h". I didn’t have to specify a relative path from main like #include "player/include/player.hpp" because of how I structure the CMakeLists.txt file.

To build this, I have to open a terminal and cd into the build directory again. Since I updated my CMakeLists.txt file, I do have to run cmake .. again before I type make. Then I can just execute sample:

Conclusion

CMake is a massive project. It’s been around since about 2000, has had three major revisions, and supports pretty much any platform or build system you’ll encounter. That means that some patterns have fallen out of favor today, and I hope that I’ve given you a good push in the direction of modern CMake. Now would be a great time to pull in that Firebase C++ SDK you’ve been eyeing or to start crafting your own Android Native Activity!

What about…

There are a lot of CMake samples that will be pretty much indecipherable if you go in only knowing what I’ve covered here. I chose to cover what’s colloquially known as “modern CMake” which is roughly analogous in concept to “modern C++.” I also covered the bare minimum you need to get off the ground.

CMake prior to CMake 3 wasn’t super declarative in structure. It felt much more like a simple scripting language than a way to define build targets and dependencies (ex: target_include_directories appeared in version 2.8.11 — one of the last releases in the 2.x branch). For the most part, the biggest changes are the various declarative constructs eliminating the need for a lot of the more procedural-looking CMakeLists.txt files you find in older projects.

Since I’ve only covered the absolute basics here, it may be hard to categorize constructs you see in the wild and separate good patterns from anti-patterns or legacy code. In addition to CMake’s official documentation, I highly recommend this guide to modern CMake to help you as needed.

Resources

--

--

Patrick Martin
Firebase Developers

I’ve been a software engineer on everything from games to connected toys. I’m now a developer advocate for Google’s Firebase.