Author: methylDragon
Contains a syntax reference for CMake. We'll be going through some concepts, the CLI, and scripting with CMake!
Assumed knowledge
- Have a rudimentary understanding of C/C++
- Since CMake is used to build C/C++ projects!
- Understood the linkage and build concepts from this tutorial
- The tutorial is written with Linux users in mind, specifically Ubuntu
- But the scripting section should apply in general
- Recommended: Check out this 15 minute tutorial to familiarise yourself with CMake!
- Introduction
- CMake Concepts and Command Line
2.1 Installation
2.2 CMakeLists.txt
2.3 CMake Build Order
2.4 Project Structure
2.5 Invoking CMake
2.6 Configuring CMake
2.7 Ordering - CMake Scripting
3.1 Code Style
3.2 Minimal CMakeLists.txt
3.3 Comments
3.4 Printing
3.5 Variables
3.6 Variable Scope and Directories
3.7 User Input: Cache Variables
3.8 User Input: Options
3.9 Prefixes
3.10 Conditionals
3.11 Loops
3.12 Functions
3.13 Macros
CMake is Cross-platform Make, where Make
is the utility for building programs from source code.
CMake is an extensible, open-source system that manages the build process in an operating system and compiler-independent manner. Unlike many cross-platform systems, CMake is designed to be used in conjunction with the native build environment. Simple configuration files placed in each source directory (called CMakeLists.txt files) are used to generate standard build files (e.g., makefiles on Unix and projects/workspaces in Windows MSVC) which are used in the usual way.
In other words, CMake is used to generate files that are used with Make to build a project. It's like a meta-build system. (Though, actually, it can be used with a lot more build pipelines like Visual Studio, XCode, and more!)
Additionally, CMake is more generally used nowadays due to its convenience and simplicity (relative to pure Make
), and so, it is far more useful to learn as a build tool. This is especially true since anything you can do in make
, you can do in CMake.
When comparing CMake with Make, there are several advantages of using CMake:
- Cross platform discovery of system libraries.
- Automatic discovery and configuration of the toolchain.
- Easier to compile your files into a shared library in a platform agnostic way, and in general easier to use than make.
Also note, this tutorial will not serve to be an exhaustive guide on everything CMake, but is designed to be just enough to get you up to speed and able to work somewhat effectively on projects.
Additionally, since CMake is mostly used with C or C++, this tutorial will focus on use of CMake with those languages.
In this section, we'll go through some concepts, the command line interface, and scripting basics.
Before we can even begin trying to code with CMake, we need to know what it actually does, and how to use it from the command line. So let's take a look!
sudo apt install cmake
Wow! So easy! If this doesn't work, check your software repositories, or seek out a PPA repository.
CMake is invoked by using a CMakeLists.txt
file placed in the root directory of a project (though you can nest projects too!) This file tells CMake what to do and configures the final makefile
that is produced, which will then be subsequently used with Make
to build your project.
CMake first configures itself by checking the CMakeLists.txt
file. The configuration should specify the targets for build, their linkages, as well as any build properties or configurations, as well as run any configuration time logic.
Then, it runs that configuration through a generation step to produce a Makefile or some other build pipeline that can be used to actually build your targets.
There's no standardised way to structure a C++ project, but here is a generally acceptable project structure.
This is especially important since CMake will generate a whole bunch of files where the cmake
command is invoked. And no one wants generated files clogging up their source directories.
my_project
- src/
- include/
- build/
- CMakeLists.txt
There are also a couple of additional folder directories that you can put in, and a couple of other files that you can place in your project directory. Here's a full description of them!
Also, do note that you can nest projects!
Folders
Name | Description |
---|---|
src/ |
Stores source files and private headers |
include/ |
Stores public headers for using the project's public API |
build/ |
Invoke CMake from within this folder for neatness' sake, so all CMake-generated files go in here |
extern/ |
External projects go here. Cool thing is that these projects can have their own CMakeLists.txt and you can invoke those files in the parent project's CMakeLists.txt |
doc/ |
Store documentation |
test/ |
Stores test scripts |
data/ |
Stores files that are used by the executables |
Files
Name | |
---|---|
CMakeLists.txt |
Input to CMake build system for building software packages |
README.md |
README for the project. Check out how to write a good one! |
LICENSE |
License for your code |
We'll assume your project follows the structure detailed in the previous section, and that it already has a preconfigured CMakeLists.txt
. As such, we'll build our project in the build/
directory.
# So, starting in the project directory...
cd build
cmake ..
make
So we:
- Move into the
build/
directory - Invoke CMake on the
CMakeLists.txt
in the project directory to generate the makefile - And call Make to build the project based off of the generated
Makefile
You can also configure CMake further with command line arguments though! I can't go through all of them, but here are a couple of handy ones.
The default pipeline that is created when CMake is invoked are makefiles. But you can specify a custom one to use!
Just use the -G
flag!
cmake --help # See all available generators
cmake -G "GENERATOR_NAME_HERE"
cmake -G "Ninja" # Example
Use the -D
flag!
So for example, to change the build type,
cmake -DCMAKE_BUILD_TYPE=Debug # Build unoptimised code with debug flags
cmake -DCMAKE_BUILD_TYPE=MinSizeRel # Build optimised code, but at minimal size
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo # Build optimised code with debug flags
cmake -DCMAKE_BUILD_TYPE=Release # Build optimised code with no debug flags
cmake -D CMAKE_BUILD_TYPE=Release # This also works!
Additionally, do note that this is actually the best practice for invoking CMake
So you can see what variables were used during the latest invocation of CMake
cmake -L -N .
So, actually, you don't have to call CMake from inside the build folder because you can actually just specify the build folder to build into using a couple of flags.
So your usual invocation of
cd build
cmake ..
make
Becomes
cmake .. -B build
make
You should think of CMake as a kind of scripting language that eventually produces files that are used to command the build of a project. (Because it actually IS a scripting language.)
As such, it is important to keep in mind the fact that the order of calls in the CMakeLists file matters.
Now that we have a rough understanding of how to use CMake. Let's look at some basic programming constructs within CMake!
We'll cover this before the actual build configuration part of CMake because it helps give a good background understanding moving forward, especially once variables and conditions start getting thrown all around.
So do be patient!
If you've seen CMakeLists.txt
files from projects around the web, you'll notice that there's a whole mix of styles being used, some with lowercase function names, some with uppercase, and a lot more.
Here's a very rough code style guide that I like to use, and that is used in this tutorial:
-
Keywords and function names are all lowercase
- Eg.
if
,for
,message
, etc.
- Eg.
-
CMake function and macro arguments are all UPPERCASE
- Eg.
message(STATUS "message body here...)
- Eg.
-
Indentation is in spaces, and each level of indent is 2 spaces
-
You may split function arguments into multiple lines, but do so indented
-
Eg.
# Split message(STATUS 1 2 3 4 5 ) # Not split message(STATUS 1 2 3 4 5)
-
-
And finally, string variables should be wrapped in double-quotes
-
Eg.
# Do this message("Rawr") # Don't do this message(Rawr)
-
But before we can even begin, if you'd like to test your code, you need to write CMakeLists.txt
files. So let's just start with an example of how a minimal CMakeLists file that builds code.
The bare minimum CMakeLists that does meaningful work involves three things
- CMake requirements
- Project name
- Build targets
But actually, if you just want to test the basic programming stuff in CMake, you can have an empty
CMakeLists.txt
file with just the statements to test (in most cases!) It won't build anything, but it's good enough.But to be safe, at least include the
cmake_minimum_required()
statement. Lots of statements require it.
So suppose our file structure was as such
minimal_example
- build/
- src/
- example.cpp
- CMakeLists.txt
Then the corresponding CMakeLists.txt will look like this
cmake_minimum_required(VERSION 2.6)
project(minimal_example)
add_executable(Example src/example.cpp)
So we target src/example.cpp for compilation into the Example executable. Very simple!
(In this care the executable is only made of one translation unit. You can add more after the first to link them together)
You can also compile libraries, and link them! So for example, in some other directory...
cmake_minimum_required(VERSION 2.6)
project(another_example)
add_library(WOW somelib.cpp)
target_include_directories(WOW include) # Specifying the header locations
add_executable(Example src/example.cpp)
target_link_libraries(Example WOW)
As we move on, you'll notice that more and more stuff will get added, but usually only after the project()
call. But just keep this basic structure in mind.
Also, it is important to notice that in this case, we had to build the
WOW
library before linking it to the executable.This is another example of the importance of the order of statements inside a
CMakeLists.txt
file! So in this case it is necessary to runadd_library()
beforeadd_executable()
.
# This is a comment
# It's nothing special
General syntax
message([<mode>] "message to display" ...)
Where the modes are:
Name | Description |
---|---|
(None) | Important Info |
STATUS |
Status Info |
WARNING |
Warning Info |
AUTHOR_WARNING |
Warning for Devs |
SEND_ERROR |
Error, skip generation, but continue processing |
FATAL_ERROR |
Error, stop processing and generation |
DEPRECIATION |
Warning only if CMAKE_ERROR_DEPRECIATED or CMAKE_WARN_DEPRECIATED is enabled |
There are also a couple others in the more recent versions of CMake, but at the time of writing the tutorial those versions don't come shipped in an easily installable distribution of CMake.
Name | Description |
---|---|
VERBOSE |
Detailed messages enabled by setting CMAKE_VERBOSE_MAKEFILE to ON |
DEBUG |
Debug messages enabled by calling CMake with --debug-output |
Example usage
message("HI!")
message(STATUS "This is a status message!")
message(WARNING "RAWR. BIG WARNS.")
message(FATAL_ERROR "Time to skedaddle out of this compilation script.")
message("This message shouldn't be printing")
Nice! Multi-line works within the quotation marks too!
(And of course, the backslash escapes characters.)
message(" ______
< Wow! >
------
\\
\\ . .
. |\\-^-/| .
/| } O.=.O { |\\
/' \\ \\_ ~ _/ / '\\
/' | \\-/ ~ \\-/ | '\\
| | /\\\\ //\\ | |
\\|\\|\\/-\"\"-\"\"-\\/|/|/
______/ /
'------
")
Furthermore, subsequent arguments are just concatenated together
message(1 2 3 4 5)
# Set variables
set(VARIABLE_NAME "value")
set(WOW_NUM 10)
# Or even as a list!
# Lists are just semi-colon delimited strings!
set(SOME_LIST "a" "b")
set(EQUIVALENT_LIST "a;b") # These are the same!
set(NUM_LIST 1 2)
set(EQUIVALENT_NUM_LIST 1;2)
# You can even append to the lists!
set(NUM_LIST ${NUM_LIST} 3 4) # So-so way
list(APPEND NUM_LIST 5 6) # Better way
# Use variables
message("WOW_NUM: ${WOW_NUM}")
message("EQUIVALENT_NUM_LIST: ${EQUIVALENT_NUM_LIST}")
# And yes, variables can be called even within a string declaration!
# Escape the characters if you don't want this to happen
# Like so: message("EQUIVALENT_NUM_LIST: \${EQUIVALENT_NUM_LIST}")
Output
Note: Variable names are case sensitive!
Additionally, there are some pre-defined environment or CMake variables that are set automatically! You can access them by using them in much the same way. The full list of environment variables will be put below.
But here's an example call.
# Example CMake variable call
message(${CMAKE_MAJOR_VERSION})
# Environment variable set and get
set(ENV{variable_name} 10)
message($ENV{variable_name})
Variables are created in the scope they are set in. And scopes are defined by directories and functions.
Directories!?
Yes! Directories! Since each
CMakeLists.txt
file governs its directory, so treat them modularly like that
So just think of C++ scopes, and you should be fine.
Luckily, there is a way to state that a variable is set in its parent scope, instead of its current scope.
# Just use the PARENT_SCOPE argument!
set(PARENT_SCOPED_VAR 10 PARENT_SCOPE)
CMake uses a cache, which is really just a text file called CMakeCache.txt
to remember settings so you don't have to restate them when running CMake.
Normal variables aren't placed in the cache unless you explicitly tell CMake to though! Adding a variable as a cache variable also exposes them in the command line. If you don't set them in the command line, they'll use their default value, so it's a good way to create settable variables with default values!
You can list out all the available settable cache variables from the command line!
Just use these commands:
cmake -L # List all non-advanced cache variables
cmake -LA # List all cache varialbes (including advanced ones)
cmake -LAH # List all cache variables, and also display help for them
Same as setting variables in the command line.
cmake -DCACHE_VAR_NAME=rawr
# Or with type hints
cmake -DTYPED_CACHE_VAR_NAME:STRING=raa
# General call
set(<variable> <value>... CACHE <type> <docstring> [FORCE])
The available types are
Name | Description |
---|---|
BOOL |
ON or OFF |
FILEPATH |
Path to a file on disk |
PATH |
Path to a directory on disk |
STRING |
Line of text |
INTERNAL |
Line of text (does not show up inside cmake-gui ) |
Important: Variables will only go into the cache if it doesn't exist yet in the cache. So command line cache variable setting will override these
set()
calls. Unless, of course, you use theFORCE
argument.Additionally: Normal variables bound to the same name as a cache variable will be deleted when the cache variable is set.
Example usage
# Example call, no force
# In this case, 10 will be CACHE_VAR's default value, if it isn't set in the command line
set(CACHE_VAR 10 CACHE STRING "Here's some description of the var...")
# Example call, force
# In this case, FORCED_VAR will overwrite whatever was stated in the command line
set(FORCED_VAR 10 CACHE STRING FORCE)
Recall:
You can set variables into the cache like so
# In this case we are setting the variable: VAR_NAME cmake . -DVAR_NAME=10 # You can also do it with an explicit typing! cmake. -DVAR_NAME:STRING=10
These are just boolean cache variables that you can set ON or OFF in the command line.
If you want to use options, you can use the same cache variable interface.
You can list out all the available settable cache variables from the command line!
Just use these commands:
cmake -L # List all non-advanced cache variables cmake -LA # List all cache varialbes (including advanced ones) cmake -LAH # List all cache variables, and also display help for them
Same as setting variables in the command line.
cmake -DOPTION_NAME=ON
option(OPTION_NAME "SOME_RANDOM_DOCSTRINGS" ON) # OPTION_NAME is default ON
# You can also set a dependent option!
# This one defaults to ON
# If and only if ON_ME_BEING_ON is ON and AND_ON_ME_BEING_OFF is OFF
CMAKE_DEPENDENT_OPTION(DEP_OPTION "I'm dependent!!" ON
"ON_ME_BEING_ON;NOT AND_ON_ME_BEING_OFF" OFF)
You can simulate data types or structures by using prefixes. Like: METHYL_A
, METHYL_B
, METHYL_C
The cool thing is you can resolve variables when within the declaration of the name of a variable! So it's one very easy way to state prefixes!
set(PREFIX "methyl")
set(${PREFIX}_A 1)
set(${PREFIX}_B 2)
set(${PREFIX}_C 3)
# These generate: methyl_A, methyl_B, and methyl_C
# Which contain: 1, 2, and 3 respectively
if(CONDITION)
# Do stuff
elseif(CONDITION)
# Otherwise, do stuff
else()
# Or, do this if everything else fails
endif()
Cool! You can control program flow depending on whether stuff evaluates TRUE
or FALSE
.
Named constants are case-insensitive.
True | False |
---|---|
1 | 0 |
ON |
OFF |
YES |
NO |
TRUE |
FALSE |
Y |
N |
Additionally, the following are evaluated to false:
IGNORE
, NOTFOUND
, an empty string, or strings that end with the suffix "-NOTFOUND"
set(VAR TRUE)
if(VAR)
message("Wow")
else()
message("Oh no")
endif()
# This should message "Wow"
Wait a sec. Why wasn't VAR encapsulated by ${}?
The
if()
statement is a little weird in this regard, because it takes in both variables and constants! In this case, if we wrapped VAR and passed it in asif(${VAR})
, the if statement evaluates it as the string "TRUE" as opposed to the boolean constantTRUE
.
You can do logic with the expressions that go into if()
!
Here's a handy list. But for the full list please check the reference
# Logical Operations
if(NOT <expression>) # Negation
if(<expression_1> AND <expression_2>) # AND
if(<expression_1> OR <expression_2) # OR
if( (<expression_1> AND <expression_2>) OR <expression_3> ) # Parentheses
# Comparisons
if(<expression_1> LESS <expression_2>) # Less than
if(<expression_1> GREATER <expression_2>) # Greater than
if(<expression_1> EQUAL <expression_2>) # Equals to
# Existence Checking
if(DEFINED <variable>) # Check if variable is defined
if(EXISTS <path>) # Check if path exists
# Misc
if(<string> MATCHES regex) # Regex matching
Notice that in this case, loop_var is each successive element in the list passed into the for loop.
cmake_minimum_required(VERSION 3.10)
set(LIST_VAR 1 2 3)
foreach(loop_var ${LIST_VAR})
message("CURRENT_LIST_ELEMENT: ${loop_var}")
endforeach()
You can also pass in individual elements, of course! So, these two code snippets have equivalent outputs
cmake_minimum_required(VERSION 3.10)
foreach(loop_var 1 2 3)
message("CURRENT_LIST_ELEMENT: ${loop_var}")
endforeach()
There's also alternate syntax, like so
cmake_minimum_required(VERSION 3.10)
set(LIST_VAR 1 2 3)
foreach(loop_var IN LISTS LIST_VAR)
message("CURRENT_LIST_ELEMENT: ${loop_var}")
endforeach()
cmake_minimum_required(VERSION 3.10)
foreach(loop_var RANGE 10)
message("CURRENT_LIST_ELEMENT: ${loop_var}")
endforeach()
Or to have a specified start top and a step:
cmake_minimum_required(VERSION 3.10)
foreach(loop_var RANGE 10 20 2) # Start, stop, step
message("CURRENT_LIST_ELEMENT: ${loop_var}")
endforeach()
The while loop conditions are resolved in the same way the if()
command resolves conditions.
while(<condition>)
# do stuff
endwhile()
Define your own functions for convenience!
The signature is function(<NEW_FUNCTION_NAME> [ARG_1 ARG_2 ARG_3...])
cmake_minimum_required(VERSION 3.10)
function(echo_stuff num_argument)
foreach(i RANGE ${num_argument})
message("ECHOING: ${i}")
endforeach()
endfunction()
echo_stuff(10)
Also note that any variables set()
within a function is local to its function's scope, and cannot be accessed outside of the function.
Macros are just like functions, except instead of being called normally, when macros are called, they inline the contents of the macro.
As such, while functions run in their own scope, macros don't! (Since the variables are inlined directly.)
cmake_minimum_required(VERSION 3.10)
macro(echo_stuff num_argument)
set(n ${num_argument})
foreach(i RANGE ${num_argument})
message("ECHOING: ${i}")
endforeach()
endmacro()
echo_stuff(10)
message(${n}) # This is legal! Since macros constrain no scopes
. .
. |\-^-/| .
/| } O.=.O { |\