Skip to content

Latest commit

 

History

History
456 lines (418 loc) · 14.5 KB

cmake.md

File metadata and controls

456 lines (418 loc) · 14.5 KB
marp math theme paginate footer
true
katex
custom-theme
true

CMake introduction

Today:

  • Build process with CMake
  • CMake Variables
  • Targets and their properties
  • Example CMake project

📺 Watch the related YouTube video!


Special symbols used in slides

  • 🎨 - Style recommendation
  • 🎓 - Software design recommendation
  • 😱 - Not a good practice! Avoid in real life!
  • ✅ - Good practice!
  • ❌ - Whatever is marked with this is wrong
  • 🚨 - Alert! Important information!
  • 💡 - Hint or a useful exercise
  • 🔼1️⃣7️⃣ - Holds for this version of C++(here, 17) and above
  • 🔽1️⃣1️⃣ - Holds for versions until this one C++(here, 11)

Style (🎨) and software design (🎓) recommendations mostly come from Google Style Sheet and the CppCoreGuidelines


Use CMake to simplify the build

center width:500

  • A very popular build configuration and generation tool
  • Designed to solve most of the make shortcomings
  • Very powerful, while the build script is readable
  • Does not build the code, generates files for a build system
  • By default generates a Makefile with build commands
  • 🚨 We still need to call make to build the code!
  • 💡 Have a look at the Makefile generated by CMake
  • The generated build file is complex but does many things
  • Can generate files for other build systems too (e.g. ninja)

Intuition to what CMake does

  • Provides access to your host system
  • Configures variables for your build
  • Generates the correct commands for the compiler:
    • Defines library targets and executable targets
    • Specifies their dependency graph
    • Specifies correct build and linking flags for them
    • Stores these commands in a single generated file

That's it!


CMake is just a scripting language

  • The build process is fully defined in CMakeLists.txt files
  • 🚨 CMake reads each CMakeLists.txt sequentially
  • Has all the features of a scripting language, i.e., functions, control structures, variables, etc.
  • Has some CMake-specific quirks 🤷‍♀️
    • We will learn most by example
    • Variables and CMake cache are especially confusing

Minimal CMake project

Consists of a single file in some project_folder: CMakeLists.txt

cmake_minimum_required(VERSION 3.16..3.24)
project(our_project VERSION 0.0.1
                    DESCRIPTION "Our first project"
                    LANGUAGES CXX)

Set information about our project

  • Which version of CMake do we support (3.16---3.24)
  • Name of our project: our_project
  • Version of the project: 0.0.1
  • Description of what this project is about
  • Languages used in this project (Options: C, CXX, CUDA, OBJC, OBJCXX, Fortran, etc.)

Build generation with CMake

✅ Configure and generate using a command line:

cd <project_folder>  # One that has CMakeLists.txt in it
cmake -S . -B build  # Configure build and generate a Makefile
cmake --build build -j 8  # Run make with 8 threads

✅ Configure and generate using TUI (text user interface)

cd <project_folder>  # One that has CMakeLists.txt in it
ccmake -S . -B build  # Press "c" to configure and "g" to generate
cmake --build build -j 8  # Run make with 8 threads

😱 If you only have an older CMake version (🔽3.13)

cd <project_folder>
mkdir build
cd build
cmake ..      # Configure build and generate a Makefile
make -j8      # Run make with 8 threads
  • 🚨 Always build using the build sub-folder!

Variables and cache 👇

  • CMake maintains a cache of variables (look for CMakeCache.txt file after running CMake)
  • Two types of CMake variables: cached and scope-local
    # Local variable hides any cached variable with the same name
    set(VAR "value")  # Does not get copied to cache!
    # Cached variable only set if not found already in cache
    set(VAR "value" CACHE STRING "Description")
    # Ignore the value in the cache and always set variable value
    set(VAR "value" CACHE STRING "" FORCE)
    set(VAR "value" CACHE INTERNAL "")
  • Use option (ON or OFF) for a boolean cached variable
    option(CUSTOM_OPTION "Description" OFF)
  • Use the value of a variable as ${VAR}
  • We can add a variable to cache from command line:
    cmake -D FOO="foo" -D BAR="bar" -S . -B build

Print a message in CMake

  • Show a simple message:
    message(STATUS "My awesome message")
  • Use variables in messages:
    set(FOO 42)  # A local variable
    message(STATUS "Value of FOO variable is: ${FOO}")
    # The PROJECT_NAME variable is defined by project(...) command
    message(STATUS "Project name is: ${PROJECT_NAME}")
  • Show warnings and errors with WARNING, FATAL_ERROR
    if(NOT CMAKE_BUILD_TYPE)
      message(WARNING "CMAKE_BUILD_TYPE is not defined!")
    endif()
    if(NOT FOO)
      message(FATAL_ERROR "FOO is not defined!")
    endif()
  • Omit the message type to always print the message:
    message("Always print this")

Create libraries: add_library

  • To add a shared library do:
    add_library(my_library SHARED
    ${PROJECT_SOURCE_DIR}/folder/source_1.cpp source_2.cpp)
  • To add a static library do:
    add_library(my_library STATIC
    ${PROJECT_SOURCE_DIR}/folder/source_1.cpp source_2.cpp)
  • Use INTERFACE for header-only libraries
    add_library(my_library INTERFACE) # 🚨 No source files needed!
  • 💡 Note that source file paths are relative to current folder
  • 💡 For absolute paths use ${PROJECT_SOURCE_DIR}
  • 💡 If no STATIC or SHARED are provided the value of the BUILD_SHARED_LIBS variable defines the library type

Create executables with add_executable command

  • Very similar to the add_library command
  • To add an executable do:
    add_executable(my_executable source_with_main.cpp)

    🚨 Note that there must be a main function in the source file

Setting various target properties

  • Modern CMake records properties per-target
  • Property visibilities: PRIVATE, PUBLIC or INTERFACE
  • Targets created as INTERFACE (e.g. header-only libraries) can only have properties with INTERFACE visibility
  • 🚨 For a target my_target and a command that sets its properties
    command(my_target <VISIBILITY> properties)
<VISIBILITY> properties seen in my_target properties seen in targets depending on my_target
PUBLIC
PRIVATE
INTERFACE

Dependencies between targets

  • Use target_link_libraries to express dependencies between library and binary targets:
    target_link_libraries(my_target PUBLIC|PRIVATE|INTERFACE lib)
  • 💡 my_target can be either a library or an executable
  • 🚨 Confusion point: target_link_libraries doesn't just "link" libraries! It also propagates all visible (PUBLIC and INTERFACE) properties of lib to my_target

What properties are there?

  • They map to what we care about when compiling
  • Set compilation flags for using #include: -I, etc.
    ✅ Use target_include_directories command
  • Set linker flags: -L, -l:
    ✅ Use target_link_libraries command
  • Set flags to choose the C++ standard: -std=c++17
    ✅ Use target_compile_features
  • Other compilation flags: -Wall, -Wextra etc.
    ✅ Use target_compile_options
  • All of these use the same interface and visibility

When to use which visibility 🎓

  • Use PRIVATE properties with add_executable targets
  • Mostly use PUBLIC properties on library targets
  • Use INTERFACE properties on header-only library targets
  • 🚨 Always specify visibility with commands:
    • target_include_directories
    • target_compile_options
    • target_compile_features
    • target_link_libraries
    • (generally any command that starts with target_)

bg width:100%


Typical project structure

our_project/
├── CMakeLists.txt
├── executables   # Can also be "examples" or alike
│   ├── CMakeLists.txt
│   └── print_hello.cpp
├── our_project/  # Name of this project (again)
│   ├── CMakeLists.txt
│   └── tools/    # A library (typically more than one)
│       ├── CMakeLists.txt
│       ├── tools.cpp
│       ├── tools.h
│       └── tools_test.cpp  # 💡 Can be in a separate folder too
├── build/  # All generated build files. Don't modify manually!
├── .git    # Generated by git. Don't modify manually!
├── .clang-format
├── .gitignore
└── readme.md  # Description of your project. Very important!

Let's fill in all (well, most) of these files!

Stay tuned for the *_test.cpp files in a later video 😉

🎨 It's a good style if namespaces follow the folder structure


Let's start with the C++ code

  • our_project/our_project/tools/tools.h
    #pragma once
    namespace our_project { namespace tools {
    void PrintHello();
    }} // Saving place on slides here, use clang-format!
  • our_project/our_project/tools/tools.cpp
    #include <our_project/tools/tools.h>
    #include <iostream>
    namespace our_project { namespace tools {
    void PrintHello() { std::cout << "Hello!" << std::endl; }
    }} // Saving place on slides here, use clang-format!
  • our_project/executables/print_hello.cpp
    #include <our_project/tools/tools.h>
    int main() {
      our_project::tools::PrintHello();
      return 0;
    }

The main CMakeLists.txt file

our_project/CMakeLists.txt

cmake_minimum_required(VERSION 3.16..3.24)
project(our_project VERSION 0.0.1
                    DESCRIPTION "Our first project"
                    LANGUAGES CXX)
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()
message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}")

add_library(cxx_setup INTERFACE)
target_compile_options(cxx_setup
  INTERFACE -Wall -Wpedantic -Wextra)
target_compile_features(cxx_setup INTERFACE cxx_std_17)
target_include_directories(cxx_setup
  INTERFACE ${PROJECT_SOURCE_DIR})

# 🚨 Make sure CMakeLists.txt file exists in the subdirectories!
add_subdirectory(${PROJECT_NAME})
add_subdirectory(executables)

# 💡 Some things (e.g. tests or installation) are missing here.
# 💡 Stay tuned for these in future lectures!

CMake files in sub-folders

  • our_project/our_project/CMakeLists.txt
    add_subdirectory(tools)
  • our_project/our_project/tools/CMakeLists.txt
    add_library(tools tools.cpp)
    target_link_libraries(tools PUBLIC cxx_setup)
  • our_project/executables/CMakeLists.txt
    add_executable(print_hello print_hello.cpp)
    target_link_libraries(print_hello PRIVATE tools cxx_setup)

That's it! Let's build it!

We have to execute the following commands: (with approximately the same output)

cd path/to/our/project/our_project
λ › cmake -S . -B build
-- The CXX compiler identification is AppleClang 14.0.0.14000029
-- 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
-- Generating done
-- Build files have been written to: .../our_project/build
λ › cmake --build build
[ 25%] Building CXX object .../tools.cpp.o
[ 50%] Linking CXX static library libtools.a
[ 50%] Built target tools
[ 75%] Building CXX object .../print_hello.cpp.o
[100%] Linking CXX executable print_hello
[100%] Built target print_hello

How to clean the build

  • To "clean" means to remove all generated build artifacts
  • That includes all libraries and binaries as well as all the temporary files generated by the build system
  • It's very easy to do with CMake

Just remove the build folder!


bg