CMake – Another Target and the Project

Last week, we’ve started our little CMake project with a “Hello CMake” one-liner. Now it’s time to expand that example by adding another target and information about the project.

This post is the second of a series about CMake:

  1. Hello CMake!
  2. Another Target and the Project
  3. CMake Project Structure

Adding more files to the project

Simply saying “Hello CMake” is, of course, not enough for our high standards. We want to say hello to anyone, so we add header and source files for a more general hello function:

void hello(std::string const& name) {
  std::cout << "Hello, " << name << "!\n";
}

Adding the additional header and source to our CMakeLists.txt is pretty straightforward:

add_executable(prog main.cpp hello.cpp hello.h)

As you see, the add_executable command can take the whole list of files we have. See the full code on GitHub.

Why did I just add the headers?

The headers are not compiled themselves, and the compiler will find them whenever they are included. So, for a Makefile-based project, it does not make much sense to explicitly add them. However, when we generate other things, e.g. MSVC project files, we want to have the headers in those generated projects.

Therefore, we add the headers to the list of sources for our executable. The different CMake generators do the right thing: add them to IDE project files, but ignore them when it comes to actually building the project.

Add another target

You may have seen the last line of output when CMake builds our program and wondered about the meaning: [100%] Built target prog

In CMake’s terminology, targets are the things we build. In most cases, those are executables and libraries. We will now add another target to our project.

Since we are exemplary developers, we add unit tests to our project as soon as possible. Today, I’ll use Catch 2. For now, we throw everything into our main directory. We’ll restructure and clean up soon.

Adding the second target is pretty easy in CMake:

add_executable(prog main.cpp hello.cpp hello.h)

add_executable(tests testmain.cpp hello.cpp hello.h)

I have omitted catch.hpp in the tests because it does not really belong to the test project. Find the current state of our project on GitHub – but promise to not look too closely at the test case. Redirecting std::cout to test the function is ugly, especially since we are not using C++11 yet.

Variables

Looking ahead, we will probably be testing every source file of our main project except main.cpp itself. That makes a lot of duplications if we continue to list them in both executables. As any proper language, CMake has variables to solve this:

set(PROJECT_SOURCES hello.cpp hello.h)

add_executable(prog main.cpp ${PROJECT_SOURCES})

add_executable(tests testmain.cpp ${PROJECT_SOURCES})

The set command in CMake is used to define variables. The first word is the name of the variable, and the rest becomes its content. Unless it contains the words CACHE and/or PARENT_SCOPE which I’ll not cover here.

CMake variables can be lists and can be seamlessly used one after another to make longer lists. For example, if we later choose to list all test sources in a variable, the target definition could look like add_executable(tests ${TEST_SOURCES} ${PROJECT_SOURCES}).

Some housekeeping

We’ve jumped right into our CMake project without looking at the standard stuff every project should have. So let’s take a step back and quickly talk about the things that make our project a little more usable.

Comments

The CMake syntax allows comments. Like so many languages, a comment starts with a # and goes until the end of the line – like // in C++.

The minimum CMake version

CMake has evolved over the years and features have been added. New versions appear relatively quickly, at the time of this writing, the latest stable version is 3.11. If you use newer features, users with older CMake versions might get strange error messages, or worse, things might get parsed but not built correctly.

To avoid that, CMake has the cmake_minimum_required command which takes the minimum version number and should be the first thing in our project’s CMakeLists.txt. cmake_minimum_required(VERSION 3.6)

A good first candidate for the minimum version is the version of CMake you are using right now. You get it by running cmake --version.

The project

Let’s for a second use one of the Visual Studio generators instead of the Makefile generator, in a separate directory _msvc_build. We’ll see that, besides some project files for our targets, we also get a solution file named Project.sln. That name is pretty boring and can be changed. The best way to do that is using the project command: project(hello_cmake)

Besides setting the project name, the command also sets the language support. The default is C and C++, so it is what we need for now. The reason why our CMakeLists.txt worked until now is that CMake will implicitly add a project(Project) if we don’t use the command. If we now clean out our _build directory and generate for Visual Studio, we’ll get a hello_cmake.sln instead of the Project.sln.

Conclusion

Our CMakeLists.txt begins to look like a proper project file:

cmake_minimum_required(VERSION 3.6)

# The project name
project(hello_cmake)

# The sources shared between the main program and the tests
set (PROJECT_SOURCES hello.cpp hello.h)

# The main program
add_executable(prog main.cpp ${PROJECT_SOURCES})

# The tests
add_executable(tests testmain.cpp ${PROJECT_SOURCES})

See the current state of our little project here on GitHub.
Next up in my CMake blog series is the CMake Project Structure.

Previous Post
Next Post

8 Comments


  1. There are also bracket comments which can be started with #[[ and finished with a closing bracket ]].

    Reply

    1. Thanks! Haven’t seen those before 🙂

      Reply

  2. I suggest NOT to explicitly add header files to the add_executable and add_library commands. Use the target_include_directories command instead (with proper propagation argument) and use the target_link_libraries command to manage dependencies between targets. Modern CMake is about targets, not about variables. As suggested before use source_group and other commands to manage visibility for files in an IDE. I suggest watching Daniel Pfeifers talks on YouTube as well. The community really needs a convention-over-configuration framework based on modern CMake that integrates common software tools and tries to enforce file structure conventions, imo. We C++ developers either spend too much time with a build system, trying to proper manage dependencies between modules or we do not care at all and simply copy source code from one project to the other. Imo the C++ tool ecosystem is a complete mess, and c++ build tools are no exception.

    Reply

    1. Sorry for the late reply.

      source_group seems to be a good tool for grouping files in the IDE, thanks for pointing me to it. However, CMake seems to generate only those files into the groups that are listed for a target. So, if I add a header to a source_group but not to any target, it won’t be visible in the IDE. At least that’s the case with the MSVC generators.

      Similarly, indexing in CLion seems to be performed only on those files that are listed for a target. If we take e.g. hello.h out of the PROJECT_SOURCES, refactoring tooling cannot be used from the header.

      So, while including headers into targets may not be needed for the dependency scanning, it seems to be needed to work with IDEs.

      Reply

    1. I’m no expert either. But I believe the ‘correct’ way to do it is use the “target_include_directories” command and specify the header as PUBLIC, INTERFACE or PRIVATE. That way a change in INTERFACE or PUBLIC headers should trigger a recompile by dependent targets.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *