Build systems are a must-have for any cross-platform project. For C and C++, CMake is the defacto standard. However, CMake can be difficult to learn for beginners, especially given the amount of outdated or incorrect information on the internet.

This guide provides a simple but complete tutorial for modern CMake. Open your favorite text editor and let's go!

Note: this guide assumes you have CMake installed. See CMake Downloads if you have not installed it.

File structure

A CMake project typically has the following structure:

└── 📂 your-project/
    ├── 📄 CMakeLists.txt
    ├── 📂 src/
    │   ├── 📝 main.cpp
    │   └── 📝 another.cpp
    ├── 📂 include/
    │   └── 📂 your-project
    │       ├── 📝 main.hpp
    │       └── 📝 another.hpp
    ├── 📂 helper_library/
    |   ├──  📂 src/
    |   |   └── 📝 lib.cpp
    |   └── 📂 include/
    |       └── 📂 helper_library/
    |           └── 📝 lib.hpp
    └── 📂 3rdparty
        ├── 📂 external_library1
        └── 📂 external_library2
            
If your project does not have a similar structure, It's a good idea to reorganize it to match. This guide will refer to items in the above diagram.


Initial setup

The first thing a CMakeLists.txt needs is the minimum CMake version your project works with. I recommend setting this to the version of your currently installed CMake, which you can get by running cmake --version. In my case, I have CMake 3.17 intalled, so at the top of my CMakeLists.txt, I write:

cmake_minimum_required(VERSION 3.17)
Next, we need to declare our project. We do this with the project command:
project("my-project")
This creates a project named my-project. Use your project's name.


Creating an Executable

Once you have your main project set up, the next step is to create an executable. To do this, we need to tell CMake which source files are part of our executable.

add_executable("${PROJECT_NAME}" "src/main.cpp" "src/another.cpp" "include/your-project/main.hpp" "include/your-project/another.hpp")
This will create an executable program using the four files listed above. Paths are relative to the location of the CMakeLists.txt. Note the use of ${PROJECT_NAME} CMake will substitute this variable to the name of your project, declared in the project() statement.. This way, if you update the name of your project, your targets also update.

"But what if I have a lot of files? I don't want to type all of their names manually!" Fortunately, we can have CMake find the files we want automatically using a GLOB. Replace the above line with the code below:

file(GLOB MY_SOURCES "src/*.cpp" "src/*.hpp" "src/*.h" "include/your-project/*.h" "include/your-project/*.hpp")
add_executable("${PROJECT_NAME}" ${MY_SOURCES})
This will instruct CMake to find all of the files with the file extensions .cpp, .h, and .hpp, in the directories src and include/your-project, and add them to a variable named MY_SOURCES. We then instruct CMake to create the executable using the filenames stored within MY_SOURCES.

There's really no right or wrong answer regarding when you should or should not use GLOB. In general, I recommend using GLOB if:
  1. Your project has a large number of files
  2. You anticipate adding many new files or renaming files
Here, GLOB will help you by automatically adding the new files as you add or rename them. Note that GLOB does not search recursively by default. GLOB has a recursive mode but it is not covered in this guide.

and I recommend hard-coding filenames if:
  1. Your project has a small number of files
  2. You do not anticipate adding many new files or renaming many files
  3. You want to control which files are included with other logic


Creating a Library

Creating a library is nearly identical to creating an executable. However, instead of using add_executable, use add_library:

add_library("${PROJECT_NAME}_helper_lib" "helper_library/src/lib.cpp" "helper_library/include/helper_library/lib.hpp")
By default, this will create a static library. For the purposes of this guide, a static library is fine.

For libraries, we have one more step. We want to make our header files available to executables or other libraries that link to it. We do this with the target_include_directories command:

target_include_directories("${PROJECT_NAME}_helper_lib"
    PUBLIC 
    "helper_library/include"
    PRIVATE
    "helper_library/include/helper_library"
)
Notice that we seem to add the include directories twice. This serves two purposes:
  1. Expose our helper_library/include directory as a public include directory, so that code using our library can include files from it using #include <helper_library/lib.hpp>
  2. Expose files in our helper_library/include directory as private-includable items. This will allow us to simply #include "lib.hpp" directly within any C++ file that is part of the library. However, external code using the library must include it with the angle brackets and specify the directlory.


Setting the C++ Version

Once we have created an executable or library, we must set what version of C++ you want it to use. We do that with the target_compile_features function:
target_compile_features(your_exe_or_library PRIVATE cxx_std_17)
This will set that target to use C++17.

Note: Do not use the global CMAKE_CXX_STANDARD variable to set the C++ version. This variable will pollute across your whole solution, including into higher scopes! It has wrecked my build multiple times and cost me countless hours. Always use target_compile_features.


Adding third-party CMake Libraries

C++ has a large ecosystem of third party libraries which you probably want to use. You probably know the pain of manually figuring out how to configure those libraries to your project for each platform you support. Fortunately, CMake makes configuring libraries in a cross-platform way easy, with the add_subdirectory command.

add_subdirectory("3rdparty/external_library1")
add_subdirectory("3rdparty/external_library2")
This will instruct CMake to visit the directories 3rdparty/external_library1 and 3rdparty/external_library2, execute their CMakeLists.txt files, and make the resulting libraries, executables, and targets available to your CMakeLists.txt. add_subdirectory only works if that directory has a CMakeLists.txt, but it does not have to be used exclusively for third party libraries. In fact, combining add_subdirectory with separated CMake projects is an excellent way to modularize your code.


Linking libraries

Now that we've added our own library and two third party libraries, we want to make them available to our executable. We do this by linking them.

target_link_libraries("${PROJECT_NAME}"
    PRIVATE 
    "${PROJECT_NAME}_helper_lib"
    external_library1
    external_library2
)
This will take our executable and link it to our three libraries. Notice the PRIVATE access specifier. This has no effect for executable targets, but when linking libraries to other libraries, you can enforce encapsulation by preventing include directories, build settings, and other configuration data from the component libraries being made available to the target that links to the main library. In general, you should use PRIVATE as much as possible when linking libraries.

Note: Some older CMake libraries do not work with PRIVATE. In that case, you will need to use PUBLIC when linking those libraries.


The complete sample CMakeLists.txt

Compare your CMakeLists.txt with this one. If something goes wrong, compare your CMakeLists with this one:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.17)   

# Declare our project
project("my-project") 

# Add our main executable
file(GLOB MY_SOURCES "src/*.cpp" "src/*.hpp" "src/*.h" "include/your-project/*.h" "include/your-project/*.hpp")
add_executable("${PROJECT_NAME}" ${MY_SOURCES})

# Add our library and make its headers available
add_library("${PROJECT_NAME}_helper_lib" "helper_library/src/lib.cpp" "helper_library/include/helper_library/lib.hpp")
target_include_directories("${PROJECT_NAME}_helper_lib"
    PUBLIC 
    "helper_library/include"
    PRIVATE
    "include/helper_library/include"
)

# set C++ version for both targets
target_compile_features("${PROJECT_NAME}" PRIVATE cxx_std_17)
target_compile_features("${PROJECT_NAME}_helper_lib" PRIVATE cxx_std_17)

# Add third party libraries
add_subdirectory("3rdparty/external_library1")
add_subdirectory("3rdparty/external_library2")

# Link everything together
target_link_libraries("${PROJECT_NAME}"
    PRIVATE 
    "${PROJECT_NAME}_helper_lib"
    external_library1
    external_library2
)

Generating build systems with CMake

Now that our CMakeLists.txt is complete, we can use it to generate our build system and compile our program. We can instruct CMake to generate a build system for us. CMake supports a wide variety of IDEs and build systems for many platforms. To get the list of supported systems, known as generators, simply run cmake --help and look at the bottom of the output. On a typical Linux or macOS computer, you will see something similar to the following

    * Unix Makefiles             = Generates standard UNIX makefiles.
    Ninja                        = Generates build.ninja files.
    Ninja Multi-Config           = Generates build<Config>.ninja files.
    Xcode                        = Generate Xcode project files.
    CodeBlocks - Ninja           = Generates CodeBlocks project files.
    CodeBlocks - Unix Makefiles  = Generates CodeBlocks project files.
    CodeLite - Ninja             = Generates CodeLite project files.
    CodeLite - Unix Makefiles    = Generates CodeLite project files.
    Sublime Text 2 - Ninja       = Generates Sublime Text 2 project files.
    Sublime Text 2 - Unix Makefiles
                                 = Generates Sublime Text 2 project files.
    Kate - Ninja                 = Generates Kate project files.
    Kate - Unix Makefiles        = Generates Kate project files.
    Eclipse CDT4 - Ninja         = Generates Eclipse CDT 4.0 project files.
    Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.
To generate a build system, run the following: cmake -S . -B build -G "generator name here" ... This will instruct CMake to use the generator you chose to generate a build system for the parent directory (..), which is where our top-level CMakeLists.txt is. If you leave out the -G flag, CMake will use the default generator for your computer.

Notice the -S . -B build flags. The -S flag tells CMake where the root CMakeLists.txt is, and the -B flag tells CMake where to place the compiled data. This is known as an "out-of-source" build. We always want to build out-of-source, or in other words, we want to isolate all of the compiled data from our project from our source code. This is in contrast to many older libraries that use the ./configure && make workflow which places build artifacts intermingled with the source code. Out-of-source bulids are easier to manage. For example, to git-ignore all compiled data we just add our build/ directory to our .gitignore, and to create a completely fresh build, we just delete the build directory.

Upon executing the command, you will see CMake looking for compiler features. This may take a while depending on the generator you chose. Once it has completed we are ready to build!


Compiling with CMake

If you want to compile with an IDE, simply open the generated project file for that IDE which CMake placed in build/ and press build inside it as you normally would. For example, if you used the Xcode generator, you would open build/your-project.xcodeproj

If you want to build from the shell, you should not invoke the build system's commands directly. Instead, use CMake's build command inside your build directory:

cmake --build build/ --config debug --target yourexe --parallel
This will work regardless of the build system you chose, even if it is an IDE. Replace "yourexe" with the name of your executable.

After that completes, look inside the subfolder for the configuration you chose, and execute your program!