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.

Greg Filak
Published

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

GreeterApp
Select a file to view its content

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:

  1. Found a C++ compiler on our system.
  2. Performed some checks to make sure the compiler works.
  3. Read our CMakeLists.txt file.
  4. 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:

  1. Greeter.cpp was compiled into an object file.
  2. The object file was archived into the static library libGreeter.a.
  3. main.cpp was compiled into an object file.
  4. 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 your CMakeLists.txt and generates a native build system in a separate build 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.

Next Lesson
Lesson 12 of 12

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.

Have a question about this lesson?
Answers are generated by AI models and may not have been reviewed for accuracy