Using CMake

Although I have quite a long experience in C++ – I started using it in 1998 – most of my experience have been in Windows, and I have hardly worked with makefiles.
Visual Studio is not only an IDE: it’s also a complete frontend for the make system. For instance, it’s enough to add a source file to automatically have it added to the build.
Of course, there are a lot of issues with this approach; and using CI systems required a bit of juggling, but for all projects I have been working with, this was already a solved problem.

This time, I wanted to tackle the problem and finally use a build system.
Theoretically, I need to just create Makefiles. However, it seems that nowadays no one uses them, because unsuitable for large projects. Not being an expert here, I will just take this as “fact”, and move on.

There are several tools that will handle the build – very often, just by creating the makefiles based on a DSL.
The most common is certainly CMake, which is the de-facto standard when talking about build systems. However, there are also alternatives; the most commonly mentioned is Bezel, from Google, which seems gaining more and more consensus as the better alternative.
Unfortunately it seems to have also a higher initial setup cost (also in terms of learning), so this time I decided to stay on the “lesser alternative”, that is CMake.

Basic usage

I will start with an example:

add_executable(tests "")
target_sources(tests PRIVATE tests/test-catch.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)

This will create a target test, with test-catch.cpp and linked against the Catch2 test library. Note that using Catch2 was not that straightforward, but that’s the topic of another blog post.

Now, for a bit more detail.

The default name for the configuration file is CMakeLists.txt.
The base file was created by the CMake Tools extension in VS Code. the first execution on the project is a bit annoying, but it will create the basic file and then allows building with one click. Most probably it has more uses, but so far, its utility appeared a bit limited.

You add a target using add_executable (there is also the equivalent to add a library), and then add sources using include or target_sources. Likewise, add libraries using target_link_libraries.

Adding sources

Adding all sources in that way is not really maintainable, especially in very large projects, with thousands of files. There must be a better way!

In particular, I was looking for a way to automatically add all sources. Again, this might be because of my legacy with IDEs, but I did not expect having to add a file twice: once on the filesystem, and once on the make system.
Unfortunately, I was not able to find something like this.

Truth to be told, there is a way, using file(GLOB) and file (GLOB_RECURSE), but it has issues and the documentation does not recommend it:

  • by default, it might not detect new/deleted files in a project
  • using the CONFIGURE_DEPENDS additional option will force CMake to re-check the directory every time, on every build, and it might not work reliably at all.

The recommended way is to split the file into smaller units, one for each directory, and then include them. This helps in containing the complexity of large builds, because there is no single file with maybe thousands of sources, but still requires the double booking I was talking before.

Maybe the solution is just in some better tooling: in the end, maintenance of that list is a work of the IDE. So far, I could not find a way to have that working in VSCode: that might be just my lack of google-fu, otoh the documentation does not seem to mention that kind of usage.

Adding libraries

In order to install a library, that must be available on the local system.
It might be because it was part of the operating system, some external package, or just built on the fly.

Libraries already available are not very interesting, because they usually just work. But what if the library is not available? CMake has the FetchContent module, which ensures all dependencies are actually populated and made available to the project. Note: it’s also possible to use ExternalProject with similar results, but I did not try it.

For example:

Include(FetchContent)
FetchContent_Declare(
  Catch2
  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
  GIT_TAG        v3.5.1 # or a later release
)
FetchContent_MakeAvailable(Catch2)

First of all, we need to import the module.
Then we declare the additional content, together with a way to fetch it; in this case, it’s git.
Finally, we tell to make the content available; this ensures all the dependencies are actually populated.

Unfortunately, I could not get that working: the library was being built in some “private” directory (in the project dir, but managed by CMake), and it was not available.
Eventually, I gave up and went for option #2: install them with a package manager and make them available in that way.
On my Mac, it meant using brew:

brew install catch2 

This was still not working, as we need to tell CMake where it should fetch the libraries. This means first of all knowing [where homebrew installs libs] 9, and then telling that location to CMake (warning: link to Medium).

# add extra include directories
include_directories(/opt/homebrew/include)
# add extra lib directories from the library folder
link_directories(/opt/homebrew/lib)

# ...

target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)

Finally, now I have a project in VSCode, which not only builds, but has multiple targets, and tests.
It took me literally few hours of experimentation, and through all that time I could not stop thinking why things could not be a bit simpler.
On the other hand, that’s a one time cost. I am pretty sure the next time I will set up a C++ project I will just be using this one as a blueprint.