Reader small image

You're reading from  Modern CMake for C++

Product typeBook
Published inFeb 2022
PublisherPackt
ISBN-139781801070058
Edition1st Edition
Tools
Right arrow
Author (1)
Rafał Świdziński
Rafał Świdziński
author image
Rafał Świdziński

Rafał Świdziński works as a staff engineer at Google. With over 10 years of professional experience as a full stack developer, he has been able to experiment with a vast multitude of programming languages and technologies. During this time, he has been building software under his own company and for corporations including Cisco Meraki, Amazon, and Ericsson. Originally from Łódź, Poland, he now lives in London, UK, from where he runs a YouTube channel, "Smok," discussing topics related to software development. He tackles technical problems, including real-life and work-related challenges encountered by many people in the field. Throughout his work, he explains the technical concepts in detail and demystifies the art and science behind the role of software engineer. His primary focus is on high-quality code and the craftsmanship of programming.
Read more about Rafał Świdziński

Right arrow

Chapter 3: Setting Up Your First CMake Project

We have now gathered enough information to start talking about the core function of CMake: building projects. In CMake, a project contains all the source files and configuration necessary to manage the process of bringing our solutions to life. Configuration starts by performing all the checks: whether the target platform is supported, whether it has all the necessary dependencies and tools, and whether the provided compiler works and supports required features.

When that's done, CMake will generate a buildsystem for the build tool of our choice and run it. Source files will be compiled and linked with each other and their dependencies to produce output artifacts.

Projects can be used internally by a group of developers to produce packages that users can install on their systems through package managers or they can be used to provide single-executable installers. Projects can also be shared in an open-source repository so that...

Technical requirements

You can find the code files that are present in this chapter on GitHub at https://github.com/PacktPublishing/Modern-CMake-for-Cpp/tree/main/examples/chapter03.

To build examples provided in this book always use recommended commands:

cmake -B <build tree> -S <source tree>
cmake --build <build tree>

Be sure to replace placeholders <build tree> and <source tree> with appropriate paths. As a reminder: build tree is the path to target/output directory, source tree is the path at which your source code is located.

Basic directives and commands

In Chapter 1, First Steps with CMake, we already looked at a simple project definition. Let's revisit it. It is a directory with a CMakeLists.txt file that contains a few commands configuring the language processor:

chapter01/01-hello/CMakeLists.txt: Hello world in CMake language

cmake_minimum_required(VERSION 3.20)
project(Hello)
add_executable(Hello hello.cpp)

In the same chapter, in the Project files section, we learned about a few basic commands. Let's explain them in depth.

Specifying the minimum CMake version – cmake_minimum_required()

This isn't strictly a project-specific command, as it should be used with scripts as well, but it is so important that we repeat it here. As you know, cmake_minimum_required() will check whether the system has the right CMake version, but implicitly, it will also call another command, cmake_policy(VERSION), which will tell CMake what the right policies are to use for this project...

Partitioning your project

As our solutions grow in the number of lines and files they have, we slowly understand that the inevitable is coming: either we start partitioning the project or we drown in lines of code and a multitude of files. We can approach this problem in two ways: by portioning the CMake code and by moving the source files to subdirectories. In both cases, we aim to follow the design principle called separation of concerns. Put simply, break your code into chunks, grouping code with closely related functionality while decoupling other pieces of code to create strong boundaries.

We talked a bit about partitioning CMake code when discussing listfiles in Chapter 1, First Steps with CMake. We spoke about the include() command, which allows CMake to execute the code from an external file. Calling include() doesn't introduce any scopes or isolations that are not defined within the file (if the included file contains functions, their scope will be handled correctly...

Thinking about the project structure

It's no secret that as a project grows, it becomes harder and harder to find things in it – both in listfiles and in the source code. Therefore, it is very important to maintain the project hygiene right from the get-go.

Imagine a scenario where you need to deliver some important, time-sensitive changes, and they don't fit well in either of the two directories in your project. Now, you need to quickly push a cleanup commit that introduces more directories and another level of hierarchy for your files so that your changes can have a nice place to fit. Or (what's worse), you decide to just shove them anywhere and create a ticket to deal with the issue later.

Over the course of the year, these tickets accumulate, the technical debt grows, and so does the cost of maintaining the code. This becomes extremely troublesome when there's a crippling bug in a live system that needs a quick fix and when people unfamiliar with...

Scoping the environment

CMake provides multiple ways of querying the environment with CMAKE_ variables, ENV variables, and special commands. For example, collected information can be used to support cross-platform scripts. These mechanisms allow us to avoid using platform-specific shell commands that may not be easily portable or differ in naming across environments.

For performance-critical applications, it will be useful to know all the features of the destination platform (for example, instruction sets, CPU core count, and more). This information can then be passed to the compiled binaries so that they can be tuned to perfection (we'll learn how to do that in the next chapter). Let's see what information is available in CMake natively.

Discovering the operating system

There are many occasions when it is useful to know what the target operating system is. Even as mundane a thing as a filesystem differs greatly between Windows and Unix in things such as case sensitivity...

Configuring the toolchain

For CMake projects, a toolchain consists of all of the tools used in building and running the application – for example, the working environment, the generator, the CMake executable itself, and the compilers.

Imagine what a less-experienced user feels when your build stops with some mysterious compilation and syntax errors. They have to dig into the source code and try to understand what happened. After an hour of debugging, they discover that the correct solution is to update their compiler. Could we provide a better experience for users and check if all of the required functions are present in the compiler before starting the build?

Sure! There are ways to specify these requirements. If the toolchain doesn't support all of the required features, CMake will stop early and show a clear message of what happened, asking the user to step in.

Setting the C++ standard

The first thing we might want to do is to set the C++ standard we require...

Disabling in-source builds

In Chapter 1, First Steps with CMake, we talked about in-source builds, and how it is recommended to always specify the build path to be out-of-source. This not only allows for a cleaner build tree and a simpler .gitignore file, but it also decreases the chances you'll accidentally overwrite or delete any source files.

Searching for the solution online, you may stumble on a StackOverflow thread that asks the same question: https://stackoverflow.com/q/1208681/6659218. Here, the author notices that no matter what you do, it seems like CMake will still create a CMakeFiles/ directory and a CMakeCache.txt file. Some answers suggest using undocumented variables to make sure that the user can't write in the source directory under any circumstances:

# add this options before PROJECT keyword
set(CMAKE_DISABLE_SOURCE_CHANGES ON)
set(CMAKE_DISABLE_IN_SOURCE_BUILD ON)

I'd say to be cautious when using undocumented features of any software, as...

Summary

We introduced a lot of valuable concepts in this chapter that will give us a strong foundation to go forward and build hardened, future-proof projects. We discussed how to set the minimum CMake version and how to configure the key aspects of the project – that is, the name, languages, and metadata fields.

Laying good foundations will help ensure that our projects can grow quickly. This is why we discussed the partitioning of projects. We analyzed naïve code partitioning using include() and compared it with add_subdirectory(). At this point, we learned about the benefits of managing the directory scope of variables, and we explored the use of simpler paths and increased modularity. Having an option to create a nested project and build it separately is very useful when we need to slowly break code down into more independent units.

After an overview of the partitioning mechanisms we have at our disposal, we explored how we want to use them – for example...

Further reading

For more information on the topics covered in this chapter, you can refer to the following:

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Modern CMake for C++
Published in: Feb 2022Publisher: PacktISBN-13: 9781801070058
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at £13.99/month. Cancel anytime

Author (1)

author image
Rafał Świdziński

Rafał Świdziński works as a staff engineer at Google. With over 10 years of professional experience as a full stack developer, he has been able to experiment with a vast multitude of programming languages and technologies. During this time, he has been building software under his own company and for corporations including Cisco Meraki, Amazon, and Ericsson. Originally from Łódź, Poland, he now lives in London, UK, from where he runs a YouTube channel, "Smok," discussing topics related to software development. He tackles technical problems, including real-life and work-related challenges encountered by many people in the field. Throughout his work, he explains the technical concepts in detail and demystifies the art and science behind the role of software engineer. His primary focus is on high-quality code and the craftsmanship of programming.
Read more about Rafał Świdziński