CMake by Example
CMake is an open source cross-platform meta build system that works on Windows, macOS, Linux, and more. It is designed to bridge the gap between the platform-specific build tools such as Ninja, GNU Make, BSD Make, Visual Studio, and Xcode. You can read more about CMake on the official CMake.org website. CMake is best known for its unmatched popularity as the primary C/C++ build tool.
CMake by Example is a hands-on introduction to how to use CMake to build C/C++ projects using annotated example projects. Make sure you have installed the latest version of CMake on your system. Check out the first Hello world example to get started.
- Hello world
- Logging
- Values
- Variables
- Executables
- Libraries
- Headers
- If/else
- For
- Arrays
- Functions
- Macros
- Set C/C++ standard
- Toolchain files
- Custom targets
- Post-build commands
- FetchContent dependency
- FindPackage dependency
- Conan dependency
- Vcpkg dependency
- Xrepo dependency
- Testing-only dependencies
- cosmocc toolchain
- Zig toolchain
- Spawning processes
- HTTP requests
- String functions
- JSON
- Environment variables
- Feature detection
- clang-format
- cmake-format
- Vendoring
- Build & test GitHub Actions
- Workflows
- Testing
- GTest testing
- Unity testing
- Packaging
- Release GitHub Actions
- Install target
- Scripts
Hello world
main.c
// You can compile and run this program directly without CMake using a C
// compiler manually. Here's how you might do so with GCC:
//
// gcc -o hello-world main.c
// ./hello-world
//
// ...but that can quickly become unweildly when you add more .c files,
// dependencies, include directories, etc. That's where CMake really shines.
#include <stdio.h>
int main() {
puts("Hello, World!");
return 0;
}
CMakeLists.txt
# First we require a minimum version of CMake. It's a good idea to choose
# something recent. You can find the latest version on the CMake website
# https://cmake.org/download/. This function is HIGHLY RECOMMENDED. It should be
# before even the project() function.
cmake_minimum_required(VERSION 3.30)
# Next we define a project. Think of a project as similar to a Python package, a
# JavaScript package, a Rust package, etc. This project() function call will
# define all subsequent targets (executables, libraries, etc.) in the scope of
# this project. You may specify more than just a project name:
#
# project(<PROJECT-NAME>
# [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
# [DESCRIPTION <project-description-string>]
# [HOMEPAGE_URL <url-string>]
# [LANGUAGES <language-name>...])
project(hello-world)
# Finally, we add an executable target. This will create an executable named
# hello-world from the source file main.c. You are able to specify more than one
# source file if you wish:
#
# add_executable(<name> <options>... <sources>...)
#
# For example: add_executable(hello-world main.c other.c). For now we only need
# one main.c file.
add_executable(hello-world main.c)
# CMake isn't actually a build system itself. It's a meta build system. CMake
# takes your imperative build instructions from the CMakeLists.txt script and
# generates the complicated structure that each platform-specific build tool
# requires. For example, GNU Makefiles on Linux, Xcode projects on macOS, and
# Visual Studio solutions on Windows. This is called "configuring" or
# "generation". You can perform the configure step by running `cmake`. **But**
# by default that creates a lot of files in the current working directory. That
# gets messy with complicated `.gitignore` patterns and such. It's much easier
# to perform an *out of source build* where we tell CMake to put those files in
# another folder like `./build/` instead of `./`. That's where the `-B build`
# flag comes in.
cmake -B ./build/
-- The C compiler identification is GNU 13.3.0
-- The CXX compiler identification is GNU 13.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - 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: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.4s)
-- Generating done (0.0s)
-- Build files have been written to: /home/runner/work/cmakebyexample.jcbhmr.com/cmakebyexample.jcbhmr.com/src/hello-world/build
# Now that we've generated the platform-specific build system files in
# `./build/`, we can tell CMake to invoke `make`, `msbuild`, `ninja`, or another
# build command in that directory to finally build our project. This stage is
# aptly called "building". You can perform the build step by running `cmake
# --build <configure_output_folder>` where `<configure_output_folder>` is the
# folder you specified with the `-B` flag. In our case, that's `./build/`.
cmake --build ./build/
[ 50%] Building C object CMakeFiles/hello-world.dir/main.c.o
[100%] Linking C executable hello-world
[100%] Built target hello-world
# And finally we can run the executable that was built in `./build/`. The
# executable is named `hello-world` because that's the target name we specified
# in the `add_executable()` function in `CMakeLists.txt`. (You _can_ override
# this with the `OUTPUT_NAME` target property.)
./build/hello-world
Hello, World!
Logging
CMakeLists.txt
cmake_minimum_required(VERSION 3.30)
project(logging)
# message() can be placed anywhere! Remember, CMake is procedural: it executes
# commands like a programming language. It's not JSON. That means we can place
# message() commands anywhere in the CMakeLists.txt file to print messages to
# the console. This is very useful for debugging and understanding what's
# happening in the GENERATION phase of CMake.
message("Hello, World!")
add_executable(logging main.c)
# message() takes an optional log level as the first argument. Here's all of
# them in one big list. Uncomment the FATAL_ERROR and SEND_ERROR lines to see
# how they cause the build process to fail.
# message(FARAL_ERROR "CMake Error, stop processing and generation. The cmake(1) executable will return a non-zero exit code.")
# message(SEND_ERROR "CMake Error, continue processing, but skip generation.")
message(WARNING "CMake Warning, continue processing.")
message(AUTHOR_WARNING "CMake Warning (dev), continue processing.")
message(DEPRECATION "CMake Deprecation Error or Warning if variable CMAKE_ERROR_DEPRECATED or CMAKE_WARN_DEPRECATED is enabled, respectively, else no message.")
message("Important message printed to stderr to attract user's attention.")
message(NOTICE "Important message printed to stderr to attract user's attention.")
message(STATUS "The main interesting messages that project users might be interested in. Ideally these should be concise, no more than a single line, but still informative.")
message(VERBOSE "Detailed informational messages intended for project users. These messages should provide additional details that won't be of interest in most cases, but which may be useful to those building the project when they want deeper insight into what's happening.")
message(DEBUG "Detailed informational messages intended for developers working on the project itself as opposed to users who just want to build it. These messages will not typically be of interest to other users building the project and will often be closely related to internal implementation details.")
message(TRACE "Fine-grained messages with very low-level implementation details. Messages using this log level would normally only be temporary and would expect to be removed before releasing the project, packaging up the files, etc.")
# But what if you want to print a message during the build process? You can't. I
# mean, you *can* but you really shouldn't. If you're looking to debug what
# CMake's "cmake --build" is doing under the hood then use
# set(CMAKE_EXPORT_COMPILE_COMMANDS ON) or -DCMAKE_EXPORT_COMPILE_COMMANDS=ON to
# generate a compile_commands.json file. Then you can inspect compile_commands.json
# and see what's going on. Tools like Visual Studio Code can also utilize this
# file to provide code completion and other features.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# In fact, the CMake Visual Studio Code extension will add
# "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" to its CMake configuration by default.
main.c
// This is just a no-op target. See CMakeLists.txt for how message() works.
int main() {
return 0;
}
# During the CONFIGURE stage is then the message() command will print things.
cmake -B ./build/
-- The C compiler identification is GNU 13.3.0
-- The CXX compiler identification is GNU 13.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - 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: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- The main interesting messages that project users might be interested in. Ideally these should be concise, no more than a single line, but still informative.
-- Configuring done (0.3s)
-- Generating done (0.0s)
-- Build files have been written to: /home/runner/work/cmakebyexample.jcbhmr.com/cmakebyexample.jcbhmr.com/src/logging/build
# Notice that none of the message() calls show up here.
cmake --build ./build/
[ 50%] Building C object CMakeFiles/logging.dir/main.c.o
[100%] Linking C executable logging
[100%] Built target logging
Values
cosmocc toolchain
cmake/cosmocc.cmake
set(CMAKE_SYSTEM_NAME Generic)
unset(CMAKE_SYSTEM_PROCESSOR)
set(CMAKE_ASM_COMPILER cosmocc)
set(CMAKE_C_COMPILER cosmocc)
set(CMAKE_CXX_COMPILER cosmoc++)
set(CMAKE_USER_MAKE_RULES_OVERRIDE
"${CMAKE_CURRENT_LIST_DIR}/cosmocc-override.cmake")
find_program(CMAKE_AR cosmoar REQUIRED)
find_program(CMAKE_RANLIB cosmoranlib REQUIRED)
cmake/cosmocc-override.cmake
set(CMAKE_ASM_OUTPUT_EXTENSION ".o")
set(CMAKE_C_OUTPUT_EXTENSION ".o")
set(CMAKE_CXX_OUTPUT_EXTENSION ".o")
main.c
#include <stdio.h>
// cosmocc adds a cosmo.h header.
#include <cosmo.h>
int main() {
printf("IsLinux()=%d\n", IsLinux());
printf("IsMetal()=%d\n", IsMetal());
printf("IsWindows()=%d\n", IsWindows());
printf("IsBsd()=%d\n", IsBsd());
printf("IsXnu()=%d\n", IsXnu());
printf("IsFreebsd()=%d\n", IsFreebsd());
printf("IsOpenbsd()=%d\n", IsOpenbsd());
printf("IsNetbsd()=%d\n", IsNetbsd());
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.30)
project(cosmocc-toolchain)
add_executable(cosmocc-toolchain main.c)
cmake --toolchain ./cmake/cosmocc.cmake -B ./build/
-- Configuring incomplete, errors occurred!
cmake --build ./build/
./build/cosmocc-toolchain
Zig toolchain
cmake/zig-x86_64-linux-musl.cmake
{{#include cmake/cosmocc.cmake}}
main.c
#include <stdio.h>
int main() {
#if defined(__x86_64__) || defined(_M_X64)
puts("This is x86-64 also known as x64");
#elif defined(__aarch64__) || defined(_M_ARM64)
puts("This is AArch64 also known as ARM64");
#else
puts("This is some other architecture");
#endif
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.30)
project(zig-toolchain)
add_executable(zig-toolchain main.c)
cmake --toolchain ./cmake/zig-x86_64-linux-musl.cmake -B ./build/
-- Configuring incomplete, errors occurred!
cmake --build ./build/
./build/zig-toolchain
Scripts
cat.cmake
#!/usr/bin/env -S cmake -P
# ^^ This hashbang uses the popular "env -S" extension which lets us supply more
# than a single argument to the interpreter command. On Linux/macOS you would
# "chmod +x <this_script>" and then "./<this_script>" to run it. On Windows you
# must manually "cmake -P <this_script>" to run it.
cmake_minimum_required(VERSION 3.30)
# We can use the "cmake -P" mode to invoke CMake in **script mode** which does
# NOT use a project() or define targets. Instead, script mode just runs
# commands. That's it. CMake scripts are useful when you want to do ad-hoc
# things like move build files around, precompute some tables, download some
# things and extract them, etc. while still staying within the C/C++/CMake
# ecosystem. You can invoke a script like:
#
# cmake -P <script_file>
#
# For example, when called like "./cat.cmake ./raven.txt" on Linux/macOS or
# "cmake -P ./cat.cmake ./ravent.txt" on Linux/macOS/Windows...
#
# message("CMAKE_ARGC=${CMAKE_ARGC}")
# message("CMAKE_ARGV0=${CMAKE_ARGV0}")
# message("CMAKE_ARGV1=${CMAKE_ARGV1}")
# message("CMAKE_ARGV2=${CMAKE_ARGV2}")
# message("CMAKE_ARGV3=${CMAKE_ARGV3}")
# message("CMAKE_ARGV4=${CMAKE_ARGV4}")
#
# ...would print:
#
# CMAKE_ARGC=4
# CMAKE_ARGV0=cmake
# CMAKE_ARGV1=-P
# CMAKE_ARGV2=./cat.cmake
# CMAKE_ARGV3=./raven.txt
#
# The CMAKE_ARGV${N} arguments keep going if you need want arguments.
# For this "cat.cmake" example we want to grab the first non-hashbang related
# argument which is CMAKE_ARGV3.
set(input_file "${CMAKE_ARGV3}")
# Then we read the file:
file(READ "${input_file}" file_contents)
# And finally we print it. We use "echo_append" because a) it prints to stdout
# instead of stderr (which is where message() goes) and b) it doesn't append an
# extra newline (which the other "cmake -E echo" does).
execute_process(COMMAND ${CMAKE_COMMAND} -E echo_append "${file_contents}")
raven.txt
The Raven
By Edgar Allan Poe
Once upon a midnight dreary, while I pondered, weak and weary,
Over many a quaint and curious volume of forgotten lore,
While I nodded, nearly napping, suddenly there came a tapping,
As of some one gently rapping, rapping at my chamber door.
"'Tis some visitor," I muttered, "tapping at my chamber door,
Only this, and nothing more."
... Read more https://www.public-domain-poetry.com/edgar-allan-poe/raven-1717
Linux/macOS
# You only need to do this once.
chmod +x ./cat.cmake
# Now you can directly invoke the CMake script!
./cat.cmake ./raven.txt
Windows
REM On Windows you need to manually invoke it.
cmake -P ./cat.cmake ./raven.txt
The Raven
By Edgar Allan Poe
Once upon a midnight dreary, while I pondered, weak and weary,
Over many a quaint and curious volume of forgotten lore,
While I nodded, nearly napping, suddenly there came a tapping,
As of some one gently rapping, rapping at my chamber door.
"'Tis some visitor," I muttered, "tapping at my chamber door,
Only this, and nothing more."
... Read more https://www.public-domain-poetry.com/edgar-allan-poe/raven-1717