Skip to content

Instantly share code, notes, and snippets.

@dk949
Last active January 10, 2024 17:24
Show Gist options
  • Save dk949/e158b94009e0b8b92e5b89d1c9ac7634 to your computer and use it in GitHub Desktop.
Save dk949/e158b94009e0b8b92e5b89d1c9ac7634 to your computer and use it in GitHub Desktop.

How to cmake

NOTE: You can also take a look at this cmake c++ started project.

Starting

The bare minimum cmake project contains one CMakeLists.txt file at the root of the project with the following code:

cmake_minimum_required(VERSION 3.15)
project(MyAwesomeProject CXX)

This states that this project requires at least cmake 3.15 to compile, it is called MyAwesomeProject and the language of the project is c++. At this point you could specify multiple languages or project version if you want, though it is not required.

cmake_minimum_required(VERSION 3.15)
project(MyAwesomeProject VERSION 0.1.0 LANGUAGES C CXX)

Doing something useful

Follow these steps to compile a binary:

  • Add the binary.
    • You could think of this step as creating an object within cmake to represent the binary.
    • These objects are called "target"s.
    • You will refer to the target by the name you gave it.
  • Set various properties for the target.
add_executable("MyAwesomeExe" "src/main.cpp" "src/lib.cpp" "src/lib.hpp")
target_link_libraries("MyAwesomeExe" PRIVATE pthread)

add_executable creates a target called "MyAwesomeExe". This target is compiled using the sources listed after target name. Notice I am listing the header file too. It will be ignored and can be omitted.

target_link_libraries adds libraries to the target. The word PRIVATE means that if this target was added as a library (e.g. if it was an add_library instead) to another target, the libraries linked to this target will not be linked to the parent target. (Use PUBLIC to make these libraries available to the parent).

Building it

The old way of doing this was to have a different command on each system. So on unix you would run:

mkdir BUILD_DIR
cd BUILD_DIR
cmake ..
make

More recent versions of cmake (early version 3 I think) allow you to have a cross-platform command for building.

cmake -B BUILD_DIR # equivalent to `mkdir BUILD_DIR && cd BUILD_DIR && cmake ..; cd ..`
cmake --build BUILD_DIR # equivalent to `cd BUILD_DIR && make; cd ..`

The most common name of BUILD_DIR is build

That's it. Your executable should be in ./BUILD_DIR/MyAwesomeExe

Bits and pieces

A (very) quick look at the cmake programming language:

Data types:

In short, there is just 1: String. In certain contexts strings that look like numbers will be interpreted as numbers but by and large everything is a string.

Quotes are only required if you want to preserve spaces. Any text passed as a function argument is a string.

Functions:

In cmake a function is called with round brackets after function name, with arguments separated by spaces. Function names are case insensitive. Old cmake required all functions to be in caps, so you will see some people still doing that.

SomeFunction(1 2 3 4)

Variables:

A variable in cmake is set with the set command

set(myVar someValue) # someValue is the literal string "someValue" (quotes are not required)

 # message is `print`. STATUS means prepend "-- " to the printout to make it look uniform (it is not required)
message(STATUS "The value of myVar is ${myVar}") # variables are accessed by enclosing it in `${}`
#-- The value of myVar is someValue

You can create a list of items and pass it to a function that takes multiple arguments like so:

set(SOURCE_FILES src/lib.cpp src/main.cpp)

add_executable("MyExe" ${SOURCE_FILES})

Any undefined variable is set to empty string

message(STATUS ${UNSET_VARIABLE})
# --

set(UNSET_VARIABLE 42)
message(STATUS ${UNSET_VARIABLE})
# -- 42

set(UNSET_VARIABLE)
message(STATUS ${UNSET_VARIABLE})
# --

If statements:

Try to avoid them, it's where all the complexity starts. The one you will probably use the most is to check whether you are compiling with MSVC. This is done by:

if (MSVC)

else()

endif()

In old cmake it was required to put the condition in the argument of else and endif, so sometimes you will see:

if (MSVC)

else(MSVC)

endif(MSVC)

Else if is called elseif.

Boolean literals are

TRUE, ON, YES # true
FALSE, OFF, NO # false

They all mean the same thing and are interchangeable

Subprojects

As mentioned above, the root of any cmake project is the CMakeLists.txt file, but a project can (and often does) contain subprojects denoted by more CMakeLists.txts further down in the hierarchy.

  ├── CMakeLists.txt      # Root project
  ├── src
  │   └── CMakeLists.txt  # Subporject for managing application source
  └── tests
      └── CMakeLists.txt  # Subporject for managing tests

These subprojects are included like so:

# ./CMakeLists.txt
add_subdirectory(src)

In ./src/CMakeLists.txt if project(...) or cmake_minimum_required(...) are not specified they will be inherited.

A complete example can look like this:

# ./CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyAwesomeProject CXX)
add_subdirectory(src)
# ./src/CMakeLists.txt

set(SOURCE_FILES src/lib.cpp src/main.cpp)
add_executable("MyAwesomeExe" ${SOURCE_FILES})
target_compile_features("MyAwesomeExe" PRIVATE cxx_std_17) # Set language standard to 17
target_link_libraries("MyAwesomeExe" PRIVATE pthread)

Flags

Cmake has too many flags. Probably the most used one is CMAKE_BUILD_TYPE which sets the build type. You can specify this when you run cmake -B like so:

cmake -B -DCMAKE_BUILD_TYPE=Debug # replace Debug with Release for release mode

Generally when setting cmake variables from the command line use syntax -DVARNAME=value where VARNAME is the variable you want to set and value is it's value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment