Building and Running CMake Projects
Learn the two-stage process of building a CMake project. This lesson covers configuring, generating, building, running, and troubleshooting common errors.
In our last lesson, we crafted our very first CMakeLists.txt
file. We took a simple C++ project with a library and an executable and described its structure using CMake's high-level, portable commands. That file is our blueprint, the single source of truth for our build.
But a blueprint isn't a building. Right now, our CMakeLists.txt
is just a text file. It hasn't compiled any code or produced an executable. In this lesson, we'll bring our blueprint to life. We'll walk through the two-stage process of using CMake to build our software: configuring a native build system and then building our targets.
We'll use the basic project we set up in the previous lesson, which is provided below:
Files
Running CMake to Generate Build Files
The first step in any CMake workflow is the configure step. This is where CMake reads your CMakeLists.txt
file, finds a suitable compiler, and generates the native build files for your platform (like a Makefile
on Linux or a Visual Studio .sln
file on Windows).
Out-of-Source Builds
Before we run our first command, we should introduce a common best practice: the out-of-source build.
This means that all the files generated by the build process - Makefiles, object files, executables, everything - should live in a directory that is separate from your source code. The most common convention is to create a build
directory inside your project's root folder.
Why is this important?
- Cleanliness: Your source directory remains pristine and only contains the files you actually edit.
- Easy Cleanup: To completely clean your project and start fresh, you just delete the
build
directory. There's no risk of accidentally deleting a source file. - Multiple Configurations: You can have multiple build directories for different configurations (e.g.,
build-debug
,build-release
,build-for-arm
) side-by-side, all using the same source code.
Let's set up this directory using our operating system's UI, or via a terminal. Using a terminal, we'd first navigate the root of our GreeterApp
project using the cd
command:
cd path/to/GreeterApp
We can create a build
directory in that location using mkdir
:
mkdir build
Now, we navigate into the newly created build
directory. All our build commands will be run from here:
cd build
Running the cmake
Command
From inside the build
directory, we'll run the configure step, which just involves the cmake
command and the location of our CMakeLists.txt
file:
cmake ..
The ..
tells CMake that the CMakeLists.txt
is "one level up" from our current location. So, if we're in the /GreeterApp/build
directory, ..
refers to the /GreeterApp
directory.
Assuming our CMakeLists.txt
is indeed in that directory, we'll see CMake get to work:
-- Building for: Ninja
-- The C compiler identification is GNU 15.1.0
-- The CXX compiler identification is GNU 15.1.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: D:/msys64/ucrt64/bin/cc.exe - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: D:/msys64/ucrt64/bin/c++.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (9.0s)
-- Generating done (0.0s)
-- Build files have been written to: D:/Projects/cmake/build
If this command doesn't work, we've included some troubleshooting steps at the end of this lesson. But, if it has worked, we'll see that CMake has successfully:
- Found a C++ compiler on our system.
- Performed some checks to make sure the compiler works.
- Read our
CMakeLists.txt
file. - Generated the native build files in the current directory (
/build
).
If you now look inside your build
directory, you'll find the generated files. On Linux, this will include a Makefile
. On Windows, if you have Visual Studio installed, it will likely generate a .sln
file and several .vcxproj
files.
You can check the contents of the directory using your operating system's UI, or through the terminal using the ls
command, or dir
on Windows:
ls
On my environment, CMake generated a Ninja project by default, but your output may be different:
CMakeCache.txt CMakeFiles build.ninja cmake_install.cmake
We'll learn more about what these other files are for later in the course, and will learn how to configure generation options in general. For now, we have successfully configured our project, and can move on to building it.
Building the Project
Now that we have a native build system, we can compile and link our code. We could use the native tool directly - for example, we could use ninja
or make
to process the generated file, or open the generated Visual Studio solution.
However, the modern CMake approach is to use a universal build command. We can do this by passing the --build
flag to cmake
. From the same build
directory, our command would look like this:
cmake --build .
The --build
flag tells CMake to invoke the underlying native build tool. The .
argument specifies the build directory (the current directory).
This command is completely portable. It will call make
if it generated a Makefile, Ninja
if it generated Ninja files, or MSBuild
if it generated a Visual Studio solution.
You will see the output from your build system as it builds the targets, which is often going to involve 4 actions:
Greeter.cpp
was compiled into an object file.- The object file was archived into the static library
libGreeter.a
. main.cpp
was compiled into an object file.- The main object file was linked with
libGreeter.a
to create the final executable,GreeterApp
.
Our GreeterApp
executable should now exist inside the build
directory.
Running and Verifying the Program Output
Our executable has been built and is waiting for us in the build
directory. Let's run it.
Remember, the executable is in your current directory so you may need to use ./
to tell the shell to run the program from the current location.
./GreeterApp
On Windows, you may also need to add the .exe
extension:
./GreeterApp.exe
In either case, you should see the expected output from our program, proving that our executable was built and linked with our library correctly.
Hello from the Greeter library!
Congratulations! You have successfully configured, built, and run your first CMake project.
Rebuilding the Project
We don't build our project only once. We will want to update our code, and generate new builds. Let's explore what happens when we do this.
The first thing to note is that our build system is generally going to be smart enough to not rebuild things that we haven't changed. If we change nothing at all and run our build command again, it's likely not going to take any action:
cmake --build .
ninja: no work to do.
Similarly, if we have changed some files, it's only going to rebuild the parts of our project that will be affected by those changes. For example, let's update main.cpp
and rerun cmake --build .
:
GreeterApp/src/main.cpp
#include <iostream>
#include "Greeter.h"
int main() {
std::cout << "Version 2: ";
std::cout << get_greeting();
return 0;
}
cmake --build .
Our static library has not changed, so our build system likely didn't need to rebuild it. From our previous four-step compilation process, only step 3 (compiling main.cpp
to an object file) and step 4 (linking with the existing library to create our new executable) were necessary.
Updating CMakeLists.txt
Finally, what happens if we update CMakeLists.txt
? Let's try that, by renaming our executable target from GreeterApp
to GreeterApp2
:
GreeterApp/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(GreeterApp)
add_library(Greeter src/Greeter.cpp)
target_include_directories(Greeter PUBLIC
${PROJECT_SOURCE_DIR}/include
)
target_compile_features(Greeter PUBLIC cxx_std_20)
add_executable(GreeterApp2 src/main.cpp)
target_compile_features(GreeterApp2 PUBLIC cxx_std_20)
target_link_libraries(GreeterApp2 Greeter)
This situation is one of the reasons we prefer the the cmake --build .
approach over using the generated files directly with a command like make
or ninja
.
Before invoking the underlying build system, the cmake
command will first check if our CMakeLists.txt
file has been updated, meaning the project files that the build system would use (like Makefile
or build.ninja
) will be out of date.
That is indeed the case so, before invoking the underlying build system, CMake will first re-run the configure step to generate new project files that incorporate our changes:
cmake --build .
Re-running CMake...-- Configuring done (0.1s)
-- Generating done (0.0s)
-- Build files have been written to: D:/Projects/cmake/build
Linking CXX executable GreeterApp2.exe
In this case, our build system also detected that no source code changes were made, so it didn't need to recompile anything. It simply needed to link our previously created object files to generate an executable with the new "GreeterApp2" name.
Troubleshooting Initial CMake Errors
Things don't always go so smoothly. You're likely to run into a few common issues using CMake for the first time. Here's how to diagnose and fix the most common problems.
Delete the /build
Directory
When we get some unexpected errors, one of the first things we should try is to simply delete everything in the /build
folder. Junk will accumulate over time and some if it can affect the behavior of our cmake
commands in undesirable ways.
Once it's cleaned out, we can quickly regenerate a fresh copy of only the stuff we need by rerunning the cmake
commands we covered in this lesson.
Compiler Not Found
During the configure step (cmake ..
), you might see an error like this:
-- The CXX compiler identification is unknown
...
CMake Error at CMakeLists.txt:2 (project):
No CMAKE_C_COMPILER could be found.
No CMAKE_CXX_COMPILER could be found.
This means CMake searched all the standard places and could not find a C++ compiler. There are a few potential causes of this.
You haven't installed a compiler: You need to ensure a C++ toolchain is installed and working correctly on your environment. We covered how to do this in our .
You're using a different environment: If you've verified you have a working toolchain and have been able to compile programs using it, you need to ensure you're running cmake
from that same environment.
For example, if you've been using the "Developer Command Prompt for VS2022" or the MSYS2 UCRT terminal on Windows to compile your programs, you should use that same environment for your cmake
commands.
You've just installed it: If you just installed a compiler, you may need to close and reopen your terminal for the changes to take effect.
Syntax Problems in CMakeLists.txt
CMake will also complain if there are typos or errors in your CMakeLists.txt
file. For example:
CMake Error at CMakeLists.txt:10 (target_link_librarie):
Unknown CMake command "target_link_librarie".
This is usually an easy fix. The error message is very clear:
- It tells you the file and line number (
CMakeLists.txt:10
). - It tells you what it thinks the problem is (
Unknown CMake command
).
In this case, we simply misspelled target_link_libraries()
. CMake commands are case-insensitive, but for clarity and convention, you should stick to the lowercase command_name()
style.
Summary
In this lesson, we bridged the gap between writing a CMakeLists.txt
and having a working program. We've mastered the fundamental two-step workflow that lies at the heart of every CMake project.
- Configure (
cmake ..
): This first step reads yourCMakeLists.txt
and generates a native build system in a separatebuild
directory. Always use out-of-source builds! - Build (
cmake --build .
): This second step invokes the native build tool to compile your sources, link your libraries, and create your final targets. This command is portable and should be your default way to build. - Run: The final executables are created inside the
build
directory, ready to be run and tested. - Troubleshooting: Most initial problems stem from a misconfigured environment (compiler not found) or simple typos in the
CMakeLists.txt
file.
You now have a complete, end-to-end understanding of how to take a simple C++ project from source code to a running executable using CMake.
CMake Project Structure and Subdirectories
Learn how to organize large C++ projects in CMake using subdirectories and the add_subdirectory()
command to create modular, maintainable builds.