CMake is a powerful tool for building C/C++ projects to working apps, but learning how to use it can be hard. It’s big complex, and surrounded by a sea of poor or outdated tutorials.
A lot of developers end up hating CMake, including me. I avoided it like the plague, going so far as writing my own replacement build scripts just to avoid using it.
Then one day something clicked, and I figured out how to use it the right way – and it changed everything.
If you’re a C++ developer (or aspiring developer) struggling to get started with CMake like I was — or just tired of copy-pasting confusing CMakeLists.txt files — then this tutorial is for you. We’ll get your first CMake build script written fast, and build things up from there.
Whether you’re a beginner or looking to sharpen your skills, this CMake tutorial will give you a solid foundation — and if you’re ready to go further, it’s based on part of my full book/course: The CMake Tutorial.
Table of Contents
What is CMake?
People refer to CMake as a “cross-platform meta build system.” Sounds complicated, doesn’t it? But it’s actually pretty simple.
CMake doesn’t compile your code directly. Instead, it generates native build files (or scripts) for for whatever compiler of build system you’re using, and then uses that to do the actual build. On Linux, it might generate Makefiles for GCC. On Windows, it can generate Visual Studio project files. On MacOS, it might generate XCode projects. Or, you can specify exactly which tools you want to use.
The idea is this: you write one CMake build script, and CMake takes care of generating the right build setup for the platform you’re on. This means you can build your C++ project on Linux, Windows, MacOS — or even all three — using the same CMakeLists.txt script.
Why go through this extra step? Because it lets you create cross-platform builds without having to rewrite your build logic for every OS or compiler. Of course, your actual C++ code still has to support multiple platforms — but that’s another story.
CMake’s ability to build code on a wide array of OSes and compilers is likely why it’s become the de-facto standard build system. There’s no official build system in the C++ ecosystem, but CMake is so widely adopted that you’re almost guaranteed to encounter it in real-world projects.
Why CMake is Confusing? (And How to Fix That)
If you’ve ever tried to learn CMake and felt lost, you’re not alone. CMake can feel like a mess of cryptic syntax, confusing error messages, outdated examples, and trial-and-error debugging.
A lot of things that get in the way, including:
- A scripting language that’s unique to CMake,
- Documentation that’s often hard to follow,
- And a ton of outdated advice floating around online…
However, those are just contributing factors. Here’s the real problem: CMake is different from what most developers expect.
Instead of compiling source files directly like you might do with a Makefile or a script, with CMake you describe your build in a higher-level, abstract way. You don’t say “compile main.cpp.” You say “here’s a list of source files, build me an executable.”
It’s a subtle shift in thinking — but once it clicks, everything becomes much easier.
The Fix: Start With the Right Mental Model
The key to learning CMake is understanding what it’s trying to do. CMake’s job is to generate build instructions for your compiler and platform — not to replace them. Once you realize that you’re writing a blueprint, not the actual build steps, things start to click.
From there, you just need to learn:
- The basic structure of a
CMakeLists.txt
file, - How to define “build targets” (e.g., the executable(s) that you want to build),
- How to link to third party libraries.
And that’s exactly what this tutorial will walk you through — step by step, without the fluff or confusion.
What do I Need to Learn CMake?
- A computer capable of running CMake, obviously. A standard Windows, Linux, or MacOS X machine will do. Something like a Raspberry Pi is also an option
- An internet connection (to download CMake and other software we need)
- A willingness to learn
That’s it. You don’t need to know how to program in C/C++, although it’ll certainly help. C/C++ is something that you will want to learn though, because CMake isn’t very useful if you can’t write code.
Step-by-Step Tutorial
Installing CMake & Setting Up Your Development Environment
You want to write software, right? For that, you’re going to need some tools. Not just CMake. You need a complete C/C++ development environment:
- A code editor or Integrated Development Environment (IDE)
- A C/C++ compiler
- CMake
I recommend Visual Studio Code (VS Code) as IDE, because it’s beginner friendly and works on all common platforms (Windows, MacOS X, and Linux). If you prefer a different IDE, then use whatever you prefer.
Installation is different on each system. In brief, here’s what you need to install:
- Visual Studio Code – Either download here, or follow their installation instructions for your platform
- Install VS Code’s “C/C++ Extension Pack” – Click the Extensions button (
) in the left column, and enter “C/C++ Extension Pack” in the search bar (without the quotes). Select the extension pack, click the blue “Install button,” and it’ll install everything you need
- CMake – Download and install it from this page. Linux users can install it using something like
sudo apt install cmake
- A C/C++ compiler
- On Windows, download and install “Build Tools for Visual Studio“
- On MacOS X, install XCode. This can be done from a terminal window by entering the following command:
xcode-select --install
- On Linux & the Raspberry Pi, install the GCC compiler. The exact commands to enter in a terminal depends on your Linux distribution:
- For Debian/Ubuntu:
sudo apt-get install build-essential gdb cmake
- For RHEL, Fedora, & CentOS:
sudo dnf install code gcc gcc-c++ kernel-devel make gdb cmake
- On the Raspberry Pi: in one go, with:
sudo apt-get install code build-essential gdb cmake
NOTE: The line above installs everything; VS Code, CMake, & the compiler. It’s all included
- For Debian/Ubuntu:
If you need more detailed instructions, then there are detailed instructions for Windows, MacOS X, Linux, and Raspberry Pi in The CMake Tutorial.
HINT: You don’t need the full book/course; just get the free sample. Click the “free sample” button on this page, and download the book sample.
Writing Your First CMakeLists.txt
I believe in getting results ASAP, so let’s get started. Create a new directory/folder called “Hello-CMake” (put it wherever you want your software projects to be kept). Then, open up VS Code and select File ⇒ Open Folder
from the menu, and choosing the “Hello-CMake” directory.
Every project needs at least two items:
- At least one source file containing the program’s code
- A build script containing instructions on how to build the project
Let’s start with the source file. Choose File ⇒ New File
from the menu, and call the new file Main.cpp.
Type the following into the new file (i.e., Main.cpp):
/** Hello CMake
*
* A simple hello world program.
*/
#include <cstdio>
#include <cstdlib>
int main(int argc, const char **argv) {
printf("Hello CMake!\n");
return EXIT_SUCCESS;
}
You should be able to guess what this program does even if you’ve never seen C++ code before. There’s a printf()
statement that prints “Hello CMake!” That’s all you need to know for now.
Now we want to compile Main.cpp
into a runnable program. So, create a new file called CMakeLists.txt
, and type the following into the file:
cmake_minimum_required(VERSION 3.24)
project(hello_cmake)
add_executable(${PROJECT_NAME} Main.cpp)
CAUTION: File names are case sensitive on OSes such as Linux, so make sure it’s CMakeLists.txt
(and Main.cpp) exactly. Otherwise, it’ll fail with what may be a confusing error (e.g., “The source directory <x> does not appear to contain CMakeLists.txt”). If you get such an error, then check that you capitalized the correct letters.
This is likely the shortest CMake script you’ll ever write. Let’s go through it line by line:
cmake_minimum_required(VERSION 3.24)
The first line above sets the minimum version of CMake that is needed to build our project. This ensures that all features that the build script needs are available. We don’t actually need a recent version for this simple project, but we’ll be using modern CMake for everything we do from here on out. So, lets stick with a recent version.
NOTE: A common rookie question is “what minimum version should I choose?” Please do not agonize over which version is the absolute minimum required. Your time is more valuable than that. A good starting point is whatever version of CMake you’re currently using (type cmake --version
). That way the minimum version supports all features that you currently have available.
The next line declares our project’s name:
project(hello_cmake)
CMake will set the PROJECT_NAME
variable to name we give here.
add_executable(${PROJECT_NAME} Main.cpp)
The final line adds an output “target” executable to the “hello_cmake” project that gets built from one source file: Main.cpp
.
NOTE: “Executable” means a computer program that can be executed (a.k.a., run).
Building and Running Your First Program in VS Code
For some reason VS Code needs to be reset before the CMake plugin will notice and use the new CMakeLists.txt
file. So, choose File
from the menus, followed by ⇒
Close FolderFile
.⇒
Open Recent ⇒
Hello-CMake
NOTE: The “Open Recent” sub-menu will show the full path to your Hello-CMake folder instead of just “Hello-CMake”.
The CMake controls are in the blue bar at the bottom of the window:

The controls are:
- The build button (
)
- The debug button (
- The run button (
)
Click the run button (). You should see text scrolling in an “Output” sub-window directly above the blue bar. That’s CMake building hello_cmake.
NOTE: There’s no need to click the build button, because VS Code will automatically build the program if necessary (e.g., you changed one of the build scripts or source files).
All going well, it’ll switch to the “Terminal” tab once building is complete, and you’ll see “Hello CMake!” in all it’s glory (e.g., see below). Congratulations on compiling your first program with CMake!

Building From the Command Line
While we’ll be doing everything from VS Code, it’s important that you know what’s going on “under the hood.” So let’s build and run hello_cmake again, but from the command line.
If you look at a typical CMake project’s build instructions, they’ll usually tell you to enter the following from the command line:
cmake -B build
cd build
cmake --build . --parallel
The first line creates a build directory, and configures the project. The second line does the actual build. We’ll get into why CMake is run twice later.
First, look in the “Explorer” column, notice how your project now has a “build” folder (see below). That’s where all the build files live. Click on it to see what’s inside. You’ll see a bunch of files and directories. It’s a bit of a mess. A mess that you want to keep out of your root directory. That’s why we create a dedicated build directory for it.

Let’s build hello_cmake manually right now. Close VS Code, delete the build folder, and then open a terminal window. Change the directory to your hello_cmake project, e.g.:
cd /path/to/hello_cmake
IMPORTANT: You need to change /path/to/hello_cmake
to the actual directory where you put hello_cmake.
Next, enter the “standard” build commands into the terminal window:
cmake -B build
cd build
cmake --build . --parallel
TIP: You can skip the cd build
command, and use cmake --build build --parallel
instead. I’m showing it in three commands to make it clear that a build directory was created. You may also find switching to the build directory convenient for testing purposes.
CMake should print out some information about what it’s doing. Once it’s done, you should be able to run hello_cmake, by entering hello_cmake
and pushing enter. If that doesn’t work, try Debug/hello_cmake
(or Debug\hello_cmake
on Windows). On Linux, you may need to use ./hello_cmake
instead. The ./
indicates that it should look for hello_cmake in the current directory.
NOTE: Some compilers generate separate Debug and Release builds (e.g., Visual Studio & XCode), while others (such as GNU Make) do not. Hence the difference above. Separate debug and release builds end up in their own subdirectories.
Why did we have to run CMake twice? The first time, we generate the build directory and build script:
cmake -B build
The next line performs the actual build:
cmake --build . --parallel
The single dot (.
) is short-form for the current directory (i.e., the build directory). The --parallel
option enables building in parallel. This speeds up building on multi-core systems, since you can get all cores compiling code instead of just one.
TIP: You do NOT need to execute all lines every time. Once the build directory and scripts have been generated, executing cmake --build . --parallel
is all that’s needed. Changes to the CMakeLists.txt
will be detected automatically, which will trigger a rerun of the configuration process.
Compiling Multiple Source Files Into One Program
As programs become larger, a single source file becomes unwieldy. So much scrolling… And it’s hard to keep things organized. The code becomes a large interconnected mess, a.k.a., “spaghetti code.” So, large programs are broken up into modules stored in separate files; each with its own particular role to play.
How do we turn multiple source files into one program? A simple modification to our CMakeLists.txt
file will do the trick (the CMakeLists.txt
we created in the previous chapter, that is).
First, though, we need to write a multi-source-file program. To keep things simple, let’s break our Hello CMake program into two modules: Main, and Hello. Yes, it’s boring, but we’re learning CMake here, and NOT C++. So let’s keep things simple, eh?
Here are the source files:
Hello.cpp:
/** Hello.cpp
*
* See header file for details.
*/
#include <cstdio>
#include "Hello.h"
void printHello(const char *name) {
printf("Hello %s.\n", name);
}
Hello.h:
/** @file Hello.h
*
* Our module for printing hello.
* Yes, it's pointless. It's also the most basic example of splitting software into
* multiple files. If this program did something useful, then this would be a module
* dedicated to a specific task (e.g., drawing graphics on-screen).
*/
#pragma once
/** Prints hello.
*
* @param name the name of whoever we're saying hello to
*/
void printHello(const char *name);
Main.cpp:
/** Main.cpp
*
* @author Hans de Ruiter
*/
#include "Hello.h"
#include <cstdlib>
int main(int argc, const char **argv) {
printHello("cmake");
return EXIT_SUCCESS;
}
Create three source files named Hello.cpp, Hello.h, and Main.cpp, and add the contents from the program listings above. Feel free to create a copy of your original “Hello CMake” project, and modify it.
Now we need to tell CMake that hello_cmake has two source files instead of one. You could just add Hello.cpp after Main.cpp, but you’ll regret it later (it’s that “spaghetti code” problem again). So, instead we set a variable called SOURCE_FILES
first:
set(SOURCE_FILES
Main.cpp
Hello.cpp)
We then use the SOURCE_FILES
variable in the add_executable()
call below:
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
Here’s the CMakeLists.txt file in full:
cmake_minimum_required(VERSION 3.24)
project(hello_cmake)
# Our Project
set(SOURCE_FILES
Main.cpp
Hello.cpp)
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
That’s it! Click on the run button (). in the blue toolbar. Assuming you got everything right, the program will build, run… and you’ll get the same “Hello CMake!” output again. If something went wrong, look at the error messages in the “Output” sub-window, compare your code with mine above, and try again.
NOTE: You may have noticed an additional line starting with #
. It’s a comment inserted for humans to read. CMake will ignore it. Use such comments sparingly, to give additional context to the code.
Linking to Libraries (a.k.a., Dependencies)
Nobody writes 100% of the code for an app themselves. It’s too much work. Instead, we link our code to libraries of code written by others (or ourselves). Want to draw something to the screen? Then you need to link to one (or more) of the Operating System’s (OS’) graphics libraries. More likely, you’ll link to a “middleware” library that provides extensive drawing features. The middleware then links to the OS’ graphics libraries for you.
Using third-party libraries can get complicated quickly, because it’s where your code interacts with someone else’s. Relationships tend to be complicated…
In this tutorial I’m going to show you how to link to well written third-party libraries that come bundled with their own CMakeLists.txt
script.
Linking to (Open-Source) Third-Party Libraries
Old examples and tutorials on the internet will tell you to use find_package()
to link to dependencies (such as third-party libraries). I say, don’t do it! There’s a better way called FetchContent
.
While find_package()
works, it’ll fail if the “package” isn’t found. You then have to manually install the package on which the code depends before you can continue.
Why does CMake need to “find a package” in the first place? Because there’s no standard place to install third-party dependencies. This inevitably led to files being put all over the place. Find_package()
will search for dependencies in the most common locations, or locations you specify in special script files. It’s messy.
By contrast, if a package is missing then FetchContent
will download and build it for you. No more hunting down and manually installing dependencies. You can understand why I recommend using FetchContent
instead…
Why was it called FetchContent
instead of FetchPackage? No idea, sorry. CMake’s naming of functions & modules can be inconsistent. Try not to get too hung up on the names, and search instead for the right tool to achieve each goal. In this case:
find_package()
searches for a third-party dependency on your computer, and will return an error if the package isn’t foundFetchContent
will search for the dependency first. If it isn’t found, then it’ll download and build the dependency automatically, so you don’t have to
NOTE: This isn’t my preferred way to link to third-party libraries, but it is very convenient. I prefer to include third-party libraries directly in my source-code repository so that nothing extra needs to be downloaded from the internet. This is covered in The CMake Tutorial.
I’m going to link to the Raylib library. Created by Ramon Santamaria (the “Ray” in Raylib]), Raylib is an easy to use game/multimedia library. It’s great for beginners because you can do a lot with a few lines of code. For example, the code below will open a window containing a “Hello World!” message:
/** Main.cpp
*
* A simple Raylib example.
*/
#include "raylib.h"
#include <cstdlib>
int main(int argc, const char **argv) {
// Initialization
const int screenWidth = 1280;
const int screenHeight = 768;
const char *windowTitle = "Hello world!";
const char *message = "Hello world! It's great to be here.";
const int fontSize = 40;
const float msgSpacing = 1.0f;
InitWindow(screenWidth, screenHeight, windowTitle);
// NOTE: The following only works after calling InitWindow() (i.e,. Raylib is initialized)
const Font font = GetFontDefault();
const Vector2 msgSize = MeasureTextEx(font, message, fontSize, msgSpacing);
const Vector2 msgPos = Vector2{(screenWidth - msgSize.x) / 2, (screenHeight - msgSize.y) / 2};
SetTargetFPS(60);
// Main loop
while(!WindowShouldClose()) {
// Update the display
BeginDrawing();
ClearBackground(RAYWHITE);
DrawTextEx(font, message, msgPos, fontSize, msgSpacing, RED);
EndDrawing();
}
// Cleanup
CloseWindow();
return EXIT_SUCCESS;
}
I could make this code even shorter by hard-coding values directly in function calls instead of setting constants in the initialization section (see the top of main()
). However, I want to show good coding style. Hard-coding “magic numbers” is a bad idea, because trying to keep the same number in sync across multiple locations is great way to create a buggy mess.
The code above is part of our next project. So, create a new project directory, and enter the code above into a new file called Main.cpp
.
Now create the CMakeLists.txt
file, and enter the following code:
cmake_minimum_required(VERSION 3.24)
project(hello_raylib)
# Workaround for CLang and Raylib's compound literals
# See: https://github.com/raysan5/raylib/issues/1343
set(CMAKE_CXX_STANDARD 11)
# Dependencies
include(FetchContent)
set(RAYLIB_VERSION 4.5.0)
FetchContent_Declare(
raylib
URL https://github.com/raysan5/raylib/archive/refs/tags/${RAYLIB_VERSION}.tar.gz
FIND_PACKAGE_ARGS ${RAYLIB_VERSION} EXACT
)
set(BUILD_EXAMPLES OFF CACHE INTERNAL "")
FetchContent_MakeAvailable(raylib)
# Our Project
set(SOURCE_FILES
Main.cpp)
add_executable(${PROJECT_NAME} ${SOURCE_FILES})
target_link_libraries(${PROJECT_NAME} raylib)
# Checks if OSX and links appropriate frameworks (Only required on MacOS)
if (APPLE)
target_link_libraries(${PROJECT_NAME} "-framework IOKit")
target_link_libraries(${PROJECT_NAME} "-framework Cocoa")
target_link_libraries(${PROJECT_NAME} "-framework OpenGL")
endif()
You should be able to recognize the basic CMake script skeleton from previous chapters. The script header, SOURCE_FILES
, and add_executable()
parts haven’t changed. Compare the script above with the one you wrote in the previous chapter, and make sure you can identify the common parts.
There are four areas of interest. First, we need to include the FetchContent
module:
# Dependencies
include(FetchContent)
That’s straightforward enough. Here’s where things get complicated. The next blob of code fetches Raylib version 4.5.0:
set(RAYLIB_VERSION 4.5.0)
FetchContent_Declare(
raylib
URL https://github.com/raysan5/raylib/archive/refs/tags/${RAYLIB_VERSION}.tar.gz
FIND_PACKAGE_ARGS ${RAYLIB_VERSION} EXACT
)
set(BUILD_EXAMPLES OFF CACHE INTERNAL "")
FetchContent_MakeAvailable(raylib)
Let’s go through this line by line. First, we set the variable RAYLIB_VERSION
to the version we want to use:
set(RAYLIB_VERSION 4.5.0)
IMPORTANT: Explicitly setting the library version ensures that everybody compiling this code will use the same version. Otherwise, you might be using version 4.5.0, and your colleague has version 4.2.1 or 5.1.0, in which case the program may or may not work properly depending on which of you compiled it. Not good. It’s a recipe for confusing problems and bug reports. I highly recommend explicitly setting the library version.
Next, FetchContent_Declare()
is used to declare the package we want:
FetchContent_Declare(
raylib
URL https://github.com/raysan5/raylib/archive/refs/tags/${RAYLIB_VERSION}.tar.gz
FIND_PACKAGE_ARGS ${RAYLIB_VERSION} EXACT
)
The code above says we want a package we call raylib
. If it isn’t available on this machine, then download it from the specified URL. The FIND_PACKAGE_ARGS
is passed to find_package()
when it’s looking for raylib. We want it to find the EXACT
version we want (i.e., ${RAYLIB_VERSION}
).
Pay close attention to the URL, because it’s the key to downloading packages from GitHub (and other source code repositories). The URL is in the format:
https://{repository_base_url}/archive/refs/tags/{version_number_or_tag}.tar.gz
Most projects will tag their releases by version number, so the URL above is a direct link to download that version. You can confirm this by visiting https://github.com/raysan5/raylib/tree/4.5.0, and looking at the ZIP archive download link (e.g., see below). Compare that download link to the URL above, and you’ll see it matches the pattern exactly, with the exception of the .zip ending instead of .tar.gz. I chose the .tar.gz archive because it’s usually smaller than .zip. Both will work.

NOTE: Some projects prefix the version number with something like “release-“. in that case, {version_number_or_tag}
would be release-{version_number}
.
Let’s move on to the next line, which is a bit of an oddball:
set(BUILD_EXAMPLES OFF CACHE INTERNAL "")
Raylib has a BUILD_EXAMPLES
option. We don’t need the examples, and building them would be a waste of time. So, we disable it using the line above.
IMPORTANT: Library developers: please prefix your configuration variables (e.g., RAYLIB_BUILD_EXAMPLES
) so that they’re unique and don’t clash with other libraries. If BUILD_EXAMPLES
is used by multiple libraries, then disabling it for one would disable it for all, requiring manual intervention. Unique variable names make everyone’s lives easier.
With all of the above done, we can finally make Raylib available:
FetchContent_MakeAvailable(raylib)
Phew! That took a bit of explaining. We’re not done yet because, while Raylib is now available, we haven’t told CMake which target(s) to link to it. That’s done with the line just below add_executable()
:
target_link_libraries(${PROJECT_NAME} raylib)
This line says that the target ${PROJECT_NAME}
(i.e., “hello_raylib” as set in line 2) should be linked to raylib.
The rest of the lines of code are workarounds for issues with Clang and MacOS X. Remember, I said that things got complicated where your code meets someone else’s…
Okay, that’s enough theory; let’s build the code. Click the run button. Assuming all goes well, you should be greeted with a window that looks like this:

If you don’t get a window like screenshot above, then check the compiler error messages, and try to fix them. Compare your code with mine above.
Does the Window Fail to Open?
If you’re using something like a Raspberry Pi, then hello_raylib may spit out the following error message instead of opening a window:
WARNING: GLFW: Error: 65543 Description: GLX: Failed to create context: GLXBadFBConfig
WARNING: GLFW: Failed to initialize Window
FATAL: Failed to initialize Graphic Device
What happened, is that Raylib wasn’t able to detect the correct OpenGL version to use. If you get this error, then try adding the following just above FetchContent_MakeAvailable()
:
set(GRAPHICS GRAPHICS_API_OPENGL_21)
This forces the Raylib build script to use OpenGL 2.1, which is more likely to be supported on older systems.
Common Pitfalls and How to Avoid Them
“The source directory does not appear to contain CMakeLists.txt”
If you get this error but have a CMakeLists.txt
file, then check the filename’s capitalization. Systems such as Linux & MacOS are case-sensitive, so Cmakelists.txt
is NOT the same file as CMakeLists.txt
. Make sure that the C, M and L are capital letters, and everything else is lowercase.
Forgetting to Set the Minimum Required CMake Version
CMake behaves differently depending on the version. If you don’t include cmake_minimum_required(VERSION x.y)
, your project might behave unexpectedly on someone else’s system — or even your own after an update.
Which version should you choose? The current version you’re using is a good start. Don’t overthink this and try to figure out the absolute minimum version that has the features you need. It’s easy to change later if needed (e.g., you need to build on a platform with an older version).
Misunderstanding target_include_directories
A lot of people try to add global include paths with include_directories(...)
, but this opens up the potential for conflicts (i.e., different libraries using the same names for different purposes. The modern recommended way is to use target_include_directories()
for each target.
Not Using Targets Correctly
Beginners often try to set compiler flags, link libraries, or include paths globally. This can get messy fast. The modern approach is to attach everything to targets using target_*
commands (target_compile_options
, target_link_libraries
, etc.).
Trying to Use Variables for Source Files Across Directories
It’s tempting to collect .cpp
files using something like file(GLOB ...)
, but this can create build issues when files change, or you need to exclude files on some builds. It’s usually better to list source files explicitly or use target_sources()
from subdirectories.
Forgetting to Add Subdirectories
You might define a library in a subdirectory’s CMakeLists.txt
, but if you forget to call add_subdirectory()
, that target won’t exist when you try to use it.
Trying to Manually Set CMAKE_CXX_FLAGS
Instead of Using target_compile_options()
Modifying global flags is old-style CMake and can break portability. Use target_compile_options()
instead. It’s safer and more maintainable.
Mixing Old and Modern CMake
Many tutorials and StackOverflow answers use outdated practices (like global variables, include_directories()
, or link_directories()
), which clash with the modern, target-based model. Mixing the two can cause subtle and confusing build issues.
When You’re Ready to Go Deeper
This tutorial gives you a solid start with CMake—but there’s only so much we can cover in a single guide.
If you want to master CMake and learn how to:
- Use modern CMake the right way (targets, interface libraries, and more)
- Integrate third-party libraries cleanly, including non-CMake libraries
- Write your own static and dynamic libraries, with CMake build scripts that “just work”
- Write portable, professional-grade build scripts
- Cross-compile to other platforms (including the web)
- Organize large, multi-platform projects
…then you’ll want to check out The CMake Tutorial.
It’s a complete, step-by-step book/course that takes you from beginner to confident CMake pro — without the frustration. Whether you’re building your own software or contributing to large C++ codebases, you’ll walk away knowing how to write clean, reliable CMakeLists.txt files that work.
And don’t just take my word for it—real developers are already getting results.
👉 Click here to learn more and take your CMake skills to the next level.

FAQs
Can I learn CMake in a day?
You can learn the fundamentals in a day. Mastering it takes months to years, assuming that you use CMake regularly. That’s because it takes time to build up experience.
Is CMake better than Make?
For writing multi-platform software, yes, CMake is generally better. That’s because it’s designed for building multi-platform (a.k.a., cross-platform) software.
That said, some prefer Make, because it gives them finer control over the build process. CMake build scripts work at a higher level of abstraction than Makefiles.
What is the best way to learn CMake?
Hehe, via The CMake Tutorial, of course! 😉
Actually, the best way is via The CMake Tutorial and building projects using CMake. Tutorials, books and videos alone aren’t enough. We learn best by doing.
You can also learn by scouring the internet for tutorials and example code. Just beware that there are a lot of poor and outdated examples out there.
How do I structure a large project with CMake?
In brief: by dividing your CMakeLists.txt script into multiple smaller files. The include()
and add_subdirectory()
functions are provided for this purpose. Include()
enables splitting scripts into multiple files, while add_subdirectory()
allows you to split projects into multiple sub-projects.
Structuring larger projects is covered in detail in The CMake Tutorial.