at the request of a friend i’ve decided to dump some of my basic cmake knowledge. this is information i’ve picked up from supercollider, personal projects, and tutorial videos, but it doesn’t seem to be immediately accessible unless you know what you’re looking for.

for each bit i’ll be using the most recent notation available, as far as i know it. i’m also intentionally keeping this post short so it can serve as a cheat sheet - that’s why, for instance, i’m not listing every possible compiler id or command variation.

contents:

config and build steps

most usage of cmake will consist of two types of commands: configuration and building. i don’t think these are exactly the official names, but what i mean is:

> cmake -G"XCode" -DFOO_FLAG=OFF .. # config step
> cmake --build . --config Release  # build step

compiler ids

CMAKE_<LANG>_COMPILER_ID

to figure out what compiler CMake is using, check CMAKE_CXX_COMPILER_ID or use a <CXX_COMPILER_ID:Foo> generator expression (see below)

compiler id
gcc GNU
clang (LLVM) Clang
clang (Apple) - this is what comes with XCode AppleClang
msvc MSVC
icc Intel

build types

CMAKE_BUILD_TYPE

with CMake you can have several build types. the most important thing to know is that if you use an IDE project generator (like Visual Studio or XCode) you specify this in the build step, but for Makefiles you specify it in the config step.

each build type passes different flags to your compiler. for some ungodly reason it’s hard to find out what those are, so i’m listing them here. the only other place i can find this is on a stack overflow answer. these are the flags for AppleClang, others should be similar.

you can find out what they are for your compiler by creating a minimum CMake project, running cmake ., and reading CMakeCache.txt.

build type what it means flags (AppleClang, GCC) flags (MSVC 2017)
None (empty) you didn’t pass a build type to a makefile generator   /DWIN32 /D_WINDOWS /W3 /GR /EHsc
Debug debug build, no optimization -g /MDd /Zi /Ob0 /Od /RTC1
Release release build, full optimization -O3 -DNDEBUG /MD /O2 /Ob2 /DNDEBUG
RelWithDebInfo “release with debug info”. keeps debugging symbols and does optimization -O2 -g -DNDEBUG /MD /Zi /O2 /Ob1 /DNDEBUG
MinSizeRel “minimum size release”. optimized for small binary size -Os -DNDEBUG /MD /O1 /Ob1 /DNDEBUG

the ‘none’ flags will be passed for all build types.

don’t develop on Windows and don’t know what those flags mean? i looked them up for you:

  • /DFOO - define FOO in the preprocessor
  • /EHsc - catch C++ exceptions, assume extern "C" functions never throw C++ exceptions
  • /GR - enable RTTI
  • /MD - make a multithreaded DLL
  • /MDd - make a debug multithreaded DLL
  • /O1 - optimize for size
  • /O2 - optimize for speed
  • /Ob0 - no auto-inlining
  • /Ob1 - only inline functions that are marked inline, and C++ member functions defined in a class declaration
  • /Ob2 - let compiler inline freely
  • /Od - no optimization
  • /RTC1 - run-time checking: report when a variable is used without being initialized, and stack frame run-time error checking. See their site for more details.
  • /W3 - use warning level 3 (out of 4), “production quality”
  • /Zi - generate “complete debugging information”, like -g for clang/gcc

passing flags to the compiler or linker

sometimes you want to force a flag to be passed to the compiler or linker. while some compilers also let you specify these using environment variables, other times you just want to test out whether adding a flag does what you want it to do. the following configuration-step flags are good for that:

  • CMAKE_C_FLAGS/CMAKE_CXX_FLAGS - used by the compiler
  • CMAKE_EXE_LINKER_FLAGS - used by the linker when linking executables
  • CMAKE_SHARED_LINKER_FLAGS - used by linker when linking shared object libraries
  • CMAKE_MODULE_LINKER_FLAGS - used by linker when linking modules (i don’t know what this means)
  • CMAKE_STATIC_LINKER_FLAGS - used by linker when linking static object libraries

you can also edit the cache directly to set these if you don’t want to go through an entire configuration step again.

you can also use the environment variables CFLAGS, CXXFLAGS, and LDFLAGS, but those only work on the first configuration and are therefore useless in my opinion.

using boost

FindBoost

# "REQUIRED" not required, but will error early if not found
find_package(Boost 1.66 REQUIRED COMPONENTS program_options)
# for finding headers only
find_package(Boost 1.66 REQUIRED)

# then, later
target_link_libraries(MyProgramTarget Boost::program_options)

find_package will take care of everything else for you: include directories, finding the libraries, etc.

you can set some variables pre-command to configure what libs are linked. some of interest may be:

  • Boost_USE_MULTITHREADED - ON by default, set to OFF to use non-multithreaded libs
  • Boost_STATIC_LIBS - OFF by default, set to ON to force-use static libraries
  • Boost_DEBUG - get debug output from the find_package command

adding tests

enable_testing | add_test | CTest manual

# without this, you won't get a test target
enable_testing()

# need to create a test target separately from actually registering the test
add_executable(some_test tests/test_foo.cpp)

# the test can be the same name as the target
add_test(some_test some_test)

after building, you can now you can execute ctest in the build directory to run the tests.

for a more sophisticated project you will want to hide this behind a flag so that end users don’t have to build your tests if they don’t want to.

setting compiler options

target_compile_definitions | target_compile_options

use target_compile_definitions for preprocessor definitions, target_compile_options for actual compiler options.

these are close to real commands i’ve used:

# preprocessor definition; now the value of KRE_LOG_LEVEL in my C++ code can be
# controlled on the command line in the config step: `cmake -DKRE_LOG_LEVEL=3 ..`.
# for the meaning of PUBLIC here, refer to the documentation.
target_compile_definitions(kre PUBLIC KRE_LOG_LEVEL=${KRE_LOG_LEVEL})

# add some of my favorite flags
target_compile_options(kre -Wall -Wextra -Wpedantic -Werror)

# use a generator expression to silence a spurious warning from gcc
target_compile_options(kre $<$<CXX_COMPILER_ID:GNU>:-Wno-psabi>)

using generator expressions

cmake-generator-expressions(7)

i think the docs here are quite good actually, and the list is too long to go over here.

generator expressions are fantastic; they let you succinctly write informational messages and conditional expressions. take the example i gave in the previous section:

# inner generator expression only
$<CXX_COMPILER_ID:GNU>
# full expression
$<$<CXX_COMPILER_ID:GNU>:-Wno-psabi>

the inner expression evaluates to either 1 or 0, and a generic expression like $<X:STR> will only evaulate to STR if X is 1. overall, this evaluates to the flag -Wno-psabi iff the compiler is gcc; otherwise it evaluates to nothing.

this is a lot simpler and clearer than writing:

# avoid this!
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    target_compile_options(kre -Wno-psabi)
endif()

note: unlike most of CMake, you can’t just coerce things on the left hand side of one of these things to true or false; it has to be exactly 0 or 1. there’s a special expression, $<BOOL:foo>, for just that purpose.

setting the c++ standard

CMAKE_CXX_STANDARD

note: added in cmake 3.1.

set(CMAKE_CXX_STANDARD 17)

putting it all together

a sample cmake file for a project using the boost unit testing lib:

# CMakeLists.txt
project(barnowl)
cmake_minimum_required(VERSION 3.7) # optimistic
set(CMAKE_CXX_STANDARD 17)

# this is how I prefer to use Boost.Test; usually, you don't need to set this
set(Boost_USE_STATIC_LIBS ON)
find_package(Boost 1.66 REQUIRED COMPONENTS unit_test_framework)

# create a static library that can be used for both the final program and tests.
# the static lib should have most of the sources except for main()
add_library(libbarnowl STATIC src/barnowl.cpp)
# these options will propagate to all targets linked with libbarnowl, thanks to PUBLIC
target_compile_options(libbarnowl PUBLIC -Wall -Wextra -Werror)

# create the real program and link it with our static library.
add_executable(barnowl src/main.cpp)
target_link_libraries(barnowl libbarnowl)

# tests
enable_testing()

add_executable(barnowl_test test/main.cpp)
target_link_libraries(barnowl_test libbarnowl Boost::unit_test_framework)
add_test(barnowl_test barnowl_test)