Skip to content
holzkohlengrill edited this page Dec 15, 2023 · 4 revisions

Modern CMake in 20 minutes

Sample file/folder structure

|-- projectBuild/               # "Out of source build"
\-- project/
    |-- CMakeLists.txt
    |-- include/
    |   \-- MyClass.hpp
    \-- src/
         -- MyClass.cpp
         -- mainapp.cpp

Minimal Modern CMake Example

CMakeLists.txt:

cmake_minimum_required(VERSION 3.8)                 # Set min required CMake version and store it in the variable `VERSION`
# Better: Define ranges with min and max supported version
cmake_minimum_required(VERSION 3.8...3.19)

project(projectName LANGUAGES CXX)                  # Give the project a name (="target") and define the language
# Optional: Get more flexibility by adding a version number
project(projectName VERSION 1.0 LANGUAGES CXX)

target_compile_features(myTarget PUBLIC cxx_std_17)                 # Set cpp standard (cxx_std_11, cxx_std_14, and cxx_std_17, ...)
                                                                    # CMake >=3.8; cleaner than using set(CMAKE_CXX_STANDARD 17)
# set_target_properties(myTarget PROPERTIES CXX_EXTENSIONS OFF)     # Optional


# Add find_packages here:
# ...


# Split your CMake as your project grows in multiple subfolders containing other CMakeLists.txt
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/subfolder/)


# Libraries
add_library(libTargetName STATIC src/someLib.cpp inc/someLib.hpp)   # Add lib as statically binded and add its source and inlcude files
# Used to (automatically by CMake) manage any dependencies (and order) and required build options for each target (otherwise you would have to do it yourself)


# Later it makes often sense to save files in a variable like this:
set(INCLUDES_MYLIB
    ${CMAKE_CURRENT_SOURCE_DIR}/src/                                # The folder suffices
)
set(SOURCES_MYLIB
    ${CMAKE_CURRENT_SOURCE_DIR}/src/someCpp.cpp                     # Each file needs to be stated or use globbing
    ${CMAKE_CURRENT_SOURCE_DIR}/src/anotherCpp.cpp
)
add_library(anotherLibTarget SHARED ${SOURCES_MYLIB} ${INCLUDES_MYLIB})
# ${INCLUDES} is optional but helps IDEs



target_include_directories(libTargetName PUBLIC inc/)               # Make headers visible to the project
target_compile_features(libTargetName PUBLIC cxx_std_11)            # Optional: Specify options for compiling the library

target_include_directories(anotherLibTarget PUBLIC INCLUDES_MYLIB)


add_executable(execName nameOfMainMain.cpp)
target_link_libraries(execName PUBLIC libTargetName)                # Link the library to your project/executable -> Chain the targets
                                                                    # Automatically adds include paths of the library to the target project
target_link_libraries(execName PUBLIC anotherLibTarget)

Execute cmake (command line):

# In the simplest cast
cmake path/to/source

# Generator and compiler specified
cmake -G"my generator" -DCMAKE_C_COMPILER=gcc-4.2 -DCMAKE_CXX_COMPILER=g++-4.2 path/to/source/

About CMake command line parameters:

It is recommended not to put spaces after -D, -G, ... See here why.

Show available generators

Show available generators are shown with:

cmake --help

Specifiying the compiler

From the documentation:

If a non-full path value is supplied then CMake will resolve the full path of the compiler.

Meaning -DCMAKE_C_COMPILER=gcc-4.2 or -DCMAKE_C_COMPILER=/path/to/gcc-4.2 will do.

Detailed explanation

project name. -> The following variables get set with this name:

  • PROJECT_NAME
  • CMAKE_PROJECT_NAME: only reachable in the top level CMakeLists.txt
  • Scope can be PUBLIC, PRIVATE, INTERFACE

Make sources visible to the project:

You can use set() or file(GLOB SOURCES "src/*.cpp"). file allows also recursive search with GLOB_RECURSE (instead of GLOB).

set(SOURCES src/mainapp.cpp src/Student.cpp)
# OR

# Use globbing to add source files additions which are aded to the variable SOURCES
file(GLOB SOURCES "src/*.cpp")

Set a name for the executable (add_executable): This is the name of the binary (executable) being created.

add_executable(execName ${SOURCES})
# TODO: how would it look like without the variable ${SOURCES}? -> add it explicitely

Adding a library

For a library instead it would be:

add_library(libTargetName SHARED ${SOURCES})

Options are:

  • SHARED
  • STATIC
  • MODULE

TODO: explain the differences (maybe also regarding licences) between those options

Add parts of code as library

set(INCLUDES_MYLIB
    # Include directories containing the header files
    ${CMAKE_CURRENT_SOURCE_DIR}/src/
)
set(SOURCES_MYLIB
    # Only source files have to be specified on a file basis
    ${CMAKE_CURRENT_SOURCE_DIR}/src/MyClass.cpp
    ${CMAKE_CURRENT_SOURCE_DIR}/src/AnotherClass.cpp
)

add_library(myLibName STATIC ${SOURCES_MYLIB} ${INCLUDES_MYLIB})
target_include_directories(myLibName PUBLIC ${INCLUDES_MYLIB})
target_link_libraries(myLibName PUBLIC someLib)
target_link_libraries(myLibName PUBLIC mainexec::anotherLocalLib)
# You can set aliases too
add_library(mainexec::mylib ALIAS myLibName)

Add sub-CMakeLists.txt

You can nest CMakeLists.txt. In order to do that add a add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/subfolder/) which contains another CMakeLists.txt etc.

  • Directories (CMakeLists.txt)
  • Scripts (<script>.cmake)
  • Modules (<module>.cmake)

Types

Type Description
BOOL Bool (values: ON (1) / OFF (0)
PATH Path to a directory
FILEPATH Path to a file
STRING String
INTERNAL Hide variable in GUI or with cmake -L; implies STRING
STATIC Value managed by CMake do not change
UNINITIALIZED Type not yet specified

Evaluates to 0 if (all case insensitive):

  • String is empty
  • 0, FALSE, OFF, N, NO, IGNORE, or ends with NOTFOUND

Lists

Lists just strings with semicolon delimiters.

# Some list myVar with elements a, b and c
set(myVar a b c)
# equals
set(myVar "a;b;c")

# but
set(myVar "a b c")
# gives the string: "a b c"

For manipulation of lists use the list() command.

Existing variables

See here.

Expansion

set(var1 OFF)
set(var2 "var1")
if(${var2})         # Evaluates to OFF

CMake offers a variable cache. Those variables are set before the script runs and stored in CMakeCache.txt. This is usally useful to set variables by the user.

# Sets VAR_NAME to `INIT_VALUE` if and only if VAR_NAME is unset so far
set(VAR_NAME "INIT_VALUE" CACHE STRING "Description")

# Shorthand for type BOOL
option(VAR_NAME "Description" [<value>])
# <value> defaults to OFF if not present
# Equivalent to:
set(VAR_NAME "OFF" CACHE BOOL "Description")

Internal

set(VAR_NAME "VALUE" CACHE INTERNAL "DEFAULT VALUE")

Drop-down (STRINGS CACHE property)

# Set default value and description/docstring
set(MY_DROPDOWN_VAR "Default" CACHE STRING "Description")
# Set options
set_property(CACHE MY_DROPDOWN_VAR PROPERTY STRINGS Default LearnCMake GoHome)

Doing so presents a drop-down menu in the CMake GUI.

Environment variables

# Read
$ENV{<env-var-name>}
# Test if defined
if(DEFINED ENV{<env-var-name>})

# Set (only affects the value within CMake!)
set(ENV{<variable>} [<value>])
# If <value> is an empty string or not present the variable is cleared
# Definition
function(<fctn_name> [<arg1> ...])
  some_command()
endfunction()

# Call
a_func()           # Name is case INsensitive
# or
cmake_language(CALL a_func)

Debugging

Watch a variable for change.

variable_watch(myVar)

All predefined variable by CMake: https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html

All predefined properties by CMake: https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html

Print helpers

Prints all, properties/variables... see: https://cmake.org/cmake/help/latest/module/CMakePrintHelpers.html

trace option

Generates a lot of output!!

cmake --trace <...>

Conventions

My take-away from reading some established modern practices:

Casing

Type Convention
VARIABLE UPPER_CASE_SNAKE_CASE
function lower_case_snake_case; prefer functions over macros
CMakeFiles.cmake CamelCase

End commands

Use empty end commands for:

  • endforeach()
  • endif()
  • endfunction()
  • endmacro()
  • endwhile()

Also: Use empty else() commands.

# DO:
if(SOME_VAR)
   some_command()
else()
   some_command()
endif()

# NOT:
if(BAD_EXAMPLE)
   some_command()
endif(BAD_EXAMPLE)

Builds

Out of source builds (recommended)

> tree
\-- project/
    |-- CMakeLists.txt
    |-- include/
    |   \-- MyClass.hpp
    \-- src/
         -- MyClass.cpp
         -- mainapp.cpp

> mkdir projectBuild
> cd projectBuild
> cmake ../project
> make

Misc

  • Always quote string variables ("${MY_STRING_VAR}")
  • Never quote other types (${MY_NON_STRING_VAR})

Further reading

Clone this wiki locally