Modern CMake

Modern CMake

Reference to Teacher Xiao Peng’s Modern CMake course

Modern CMake refers to CMake 3.X while ancient CMake refers to CMake 2.X

Build Command Comparison

# Ancient CMake

The build command for ancient CMake:

# First, create the build directory
mkdir -p build

# Switch to the build directory
cd build

# Run cmake in the build directory to generate Makefile
cmake .. -DCMAKE_BUILD_TYPE=Release

# Build using Make with 4 processes in parallel
make -j4

# Let the local build system execute the install step
make install

# Return to the source directory
cd ..

# Modern CMake

The build command for modern CMake:

# Create the build directory directly in the source directory and generate build/Makefile
cmake -B build -DCMAKE_BUILD_TYPE=Release

# You can also use -G Ninja at the end to compile with ninja
cmake -GNinja -B build

# Automatically call the local build system to build in the build directory, similar to ancient CMake: Make -C build -j4
cmake --build build -j4

# Call the local build system to install this target
cmake --build build --target install

# cmake -B build eliminates the need to create the build directory first, switch into it, and then specify the source directory

# cmake --build build unifies different platforms (on Linux it will call make, on Windows it will call devenv.exe)

# Start using cmake --build build now!!!!

# You can use build/myapp instead of ./myapp
# For example, if the binary file main is generated
build/main

-D Option Detailed Explanation

The -D option is used to specify configuration variables:

The build of a CMake project is divided into two parts: 1. The first stage is the configuration stage, using cmake -B build to generate the build directory and create project files (Makefile or .sln files) that the local build system can recognize in the build directory. 2. The second stage is the build stage, where the compiler is actually called to compile the code.

The -D option is used to cache variables, meaning that the variables after -D will be written into the configuration file (CMakeCache.txt), so the -D option will retain the previously set -D options even if the variables are not reset during a rebuild.

Common -D options include:

Declare the build mode as release mode (Release)

cmake -B build -DCMAKE_BUILD_TYPE=Release

Specify the installation directory as /opt/openvdb-8.0

cmake -B build -DCMAKE_INSTALL_PREFIX=/opt/openvdb-8.0

Differences in CMakeLists.txt Writing:

# Ancient CMake

cmake_minimum_required(VERSION 2.8)
project(MyProject)

list(APPEND CMAKE_MODULE_PATH "")

find_package(TBB COMPONENTS tbb tbbmalloc)
if (NOT TBB_FOUND)
    message(FATAL_ERROR "TBB not found")
endif()

add_executable(myapp myapp)
target_include_directories(myapp ${TBB_INCLUDE_DIRS})
target_compile_definitions(myapp ${TBB_DEFINITIONS})
target_link_libraries(myapp ${TBB_LIBRARIES})

# Modern CMake

cmake_minimum_required(VERSION 3.12)
project(MyProject)

find_package(TBB COMPONENTS tbb tbbmalloc REQUIRED)

add_executable(myapp myapp)
target_link_libraries(myapp TBB::tbb TBB::tbbmalloc)

Simple Comparison of Ninja and Makefile

Performance: Ninja > Makefile > MSBuild Makefile checks each file at startup, wasting a lot of time, especially when there are many files, but only a small portion needs to be built. Therefore, when I/O is bound, Ninja’s speed improvement is significant. However, the CUDA toolkit on Windows only allows building with MSBuild, not Ninja.

Knowledge Point

time cmake --build build

You can see the build time

CMakeLists.txt Configuration Details

Step 1: Specify the Minimum Required CMake Version

cmake_minimum_required(VERSION 3.15)

# You can also set a version range, which will affect cmake_policy
cmake_minimum_required(VERSION 3.15...3.20)

This indicates that this CMakeLists.txt requires at least version 3.15 of CMake to run. Newer CMake versions have new features. If the CMake version is lower than the version specified in the file, a message indicating insufficient CMake version will appear.

For example, if my CMake version is 3.31.6, but the version set in CMakeLists.txt is 3.45, the following error will occur:

CMake Error at CMakeLists.txt:1 (cmake_minimum_required):
CMake 3.45 or higher is required. You are running version 3.31.6

Step 2: Initialize Project Information.

First, set the C++ standard: CMAKE_CXX_STANDARD variable

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

CMAKE_CXX_STANDARD_REQUIRED set to ON means that the version specified by CMAKE_CXX_STANDARD must be used. If CMAKE_CXX_STANDARD_REQUIRED is set to OFF, the version specified by CMAKE_CXX_STANDARD is preferred, and if it is not available, the previous version will be used.

CMAKE_CXX_EXTENSIONS is ON by default, which means that some specific extensions of GCC are enabled. OFF disables GCC’s extension features and only uses standard C++, which improves compatibility. Additionally, using CMAKE_CXX_STANDARD directly instead of -std=c++17 is also to avoid errors when using MSVC in cross-platform scenarios. Furthermore, it will conflict with the default value of CMAKE_CXX_STANDARD, which is automatically set to 11, adding the -std=c++11 option, so do not follow CSDN blindly.

Step 2project( ) function is used to initialize the project

1. Use project(Your_project_name) to set your project name

If the compiler used is MSVC, it will generate a hellocmake.sln solution in the build folder. A .sln solution can contain projects (.csproj), while in non-MSVC projects, it is usually considered a distinction between projects and submodules, where each submodule can belong to a higher-level project.

2. project(Your_project_name LANGUAGES languages-list) specifies which programming languages are used in this project

The currently supported programming languages include:

C: C languageCXX: C++ languageASM: Assembly languageFortran: Fortran languageCUDA: NVIDIA’s CUDA (added in version 3.8)OBJC: Apple’s Objective-C (added in version 3.16)OBJCXX: Apple’s Objective-C++ (added in version 3.16)ISPC: Intel’s automatic SIMD programming language (added in version 3.18)

If LANGUAGES is not specified, the default is C and CXX.

# First way
project(TestCmake LANGUAGES C CXX)

# Second way
project(TestCmake LANGUAGES NONE)
enable_language(CXX)
3. project(Your_project_name VERSION x.y.z) sets the project version

project(project_name VERSION x.y.z) can set the current project’s version number to x.y.z. You can then obtain the current project’s version number through PROJECT_VERSION. PROJECT_VERSION_MAJOR gets x (major version number). PROJECT_VERSION_MINOR gets y (minor version number). PROJECT_VERSION_PATCH gets z (patch version number).

4. project(Your_project_name DESCRIPTION “XXX” HOMEPAGE_URL https://….) gives your project a sense of belonging
project(
hellocmake
DESCRIPTION "My first app"
HOMEPAGE_URL https://...
LANGUAGES CXX C
VERSION 2.3.1
)

message("PROJECT_NAME: ${PROJECT_NAME}")
message("PROJECT_DESCRIPTION: ${PROJECT_DESCRIPTION}")
message("PROJECT_HOMEPAGE_URL: ${PROJECT_HOMEPAGE_URL}")

message("PROJECT_VERSION: ${PROJECT_VERSION}")
message("PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message("PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message("hellocmake_VERSION: ${hellocmake_VERSION}")
message("hellocmake_SOURCE_DIR: ${hellocmake_SOURCE_DIR}")
message("hellocmake_BINARY_DIR: ${hellocmake_BINARY_DIR}")

Meaning of Each Command

Variable Name Meaning
PROJECT_SOURCE_DIR Current project source path (where main.cpp is stored)
PROJECT_BINARY_DIR Current project output path (where main.exe is stored)
CMAKE_SOURCE_DIR Root project source path (where main.cpp is stored)
CMAKE_BINARY_DIR Root project output path (where main.exe is stored)
PROJECT_IS_TOP_LEVEL BOOL type, indicates whether the current project is the (top-level) root project
PROJECT_NAME Current project name
CMAKE_PROJECT_NAME Root project’s name

See: https://cmake.org/cmake/help/latest/command/project.html

cmake_minimum_required(VERSION 3.15)
project(hellocmake)

message("PROJECT_NAME: ${PROJECT_NAME}")
message("PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message("PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message("CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message("CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message("CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
# You can try
message("PROJECT_IS_TOP_LEVEL: ${PROJECT_IS_TOP_LEVEL}")
add_executable(main main.cpp)

add_subdirectory(mylib)

Example

1$ tree

2 CMakeLists.txt

3 main.cpp

4 mylib

5├──  CMakeLists.txt

6├──  mylib2

7│   ├──  CMakeLists.txt

8│   ├──  mylib3

9│   │   ├──  CMakeLists.txt

10│   │   └──  testlib3.cpp

11│   └──  testlib2.cpp

12└──  testlib.cpp

As described above, the top-level CMakeLists.txt is as written above, and the other sub-level directories can be modified accordingly by changing the names of the corresponding subdirectories and files. The configuration in mylib3 is special, as it does not use the project function.

cmake_minimum_required(VERSION 3.15)

message("PROJECT_NAME: ${PROJECT_NAME}")
message("PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message("PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message("CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message("CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
add_executable(testlib3 testlib3.cpp) 

The output of the CMake configuration phase is:

1$ cmake -B build -GNinja

2-- The C compiler identification is GNU 14.2.1

3-- The CXX compiler identification is GNU 14.2.1

4-- Detecting C compiler ABI info

5-- Detecting C compiler ABI info - done

6-- Check for working C compiler: /run/current-system/sw/bin/cc - skipped

7-- Detecting C compile features

8-- Detecting C compile features - done

9-- Detecting CXX compiler ABI info

10-- Detecting CXX compiler ABI info - done

11-- Check for working CXX compiler: /run/current-system/sw/bin/c++ - skipped

12-- Detecting CXX compile features

13-- Detecting CXX compile features - done

14PROJECT_NAME: hellocmake

15PROJECT_SOURCE_DIR: /home/nich/cmakelesson/01

16PROJECT_BINARY_DIR: /home/nich/cmakelesson/01/build

17CMAKE_CURRENT_SOURCE_DIR: /home/nich/cmakelesson/01

18CMAKE_CURRENT_BINARY_DIR: /home/nich/cmakelesson/01/build

19PROJECT_NAME: mylib

20PROJECT_SOURCE_DIR: /home/nich/cmakelesson/01/mylib

21PROJECT_BINARY_DIR: /home/nich/cmakelesson/01/build/mylib

22CMAKE_SOURCE_DIR: /home/nich/cmakelesson/01

23CMAKE_CURRENT_SOURCE_DIR: /home/nich/cmakelesson/01/mylib

24CMAKE_CURRENT_BINARY_DIR: /home/nich/cmakelesson/01/build/mylib

25PROJECT_NAME: mylib2

26PROJECT_SOURCE_DIR: /home/nich/cmakelesson/01/mylib/mylib2

27PROJECT_BINARY_DIR: /home/nich/cmakelesson/01/build/mylib/mylib2

28CMAKE_SOURCE_DIR: /home/nich/cmakelesson/01

29CMAKE_CURRENT_SOURCE_DIR: /home/nich/cmakelesson/01/mylib/mylib2

30CMAKE_CURRENT_BINARY_DIR: /home/nich/cmakelesson/01/build/mylib/mylib2

31-- Configuring done (0.7s)

32-- Generating done (0.0s)

33-- Build files have been written to: /home/nich/cmakelesson/01/build

It can be observed that

Variable Name Meaning Example Path
PROJECT_SOURCE_DIR Indicates the source directory of the last called project CMakeLists.txt. /home/nich/cmakelesson/01/mylib/mylib2
CMAKE_CURRENT_SOURCE_DIR Indicates the source directory of the current CMakeLists.txt. /home/nich/cmakelesson/01/mylib/mylib2/mylib3
CMAKE_SOURCE_DIR Indicates the root source directory of the outermost CMakeLists.txt. /home/nich/cmakelesson/01

Using PROJECT_SOURCE_DIR allows you to directly obtain the path of the outermost directory from submodules. It is not recommended to use CMAKE_SOURCE_DIR, as it will cause your project to be unusable as a submodule (it will be marked as the top-level directory of the user). For example, if someone uses your source files as their own submodule, and your CMakeLists.txt uses this keyword, the meaning of this keyword will change from the top level of your submodule source to the top level of the user’s source, leading to build failures.

Step 3: Specify the Build Type with CMAKE_BUILD_TYPE

CMAKE_BUILD_TYPE is a special variable in CMake used to set the build type. It can be set to:

Settable Variable Types Type Description Corresponding Compiler Flag
Debug (default value) Debug mode: no optimization, generates debug information, mainly for debugging the program ‘-O0 -g’
Release Release mode: highest level of optimization, best program performance, but slower to compile ‘-O3 -DNDEBUG’
MinSizeRel Minimized release mode, releases with the smallest software size, will not fully optimize but will optimize partially ‘-Os -DNDEBUG’
RelWithDebInfo Release with debug information, the generated file will be larger because it contains debug information ‘-O2 -g -DNDEBUG’

Note that defining the NDEBUG macro will remove assert statements.

# You can set
set(CMAKE_BUILD_TYPE Release)

# Since the default value is Debug, you can use this method to override the default Debug and change it to another mode
if (NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif

Step 4: How to Add a Source File

# Directly add source files, where main represents the name of the executable, and main.cpp represents the source file
add_executable(main main.cpp)

# Declare the executable file, then use target_sources to connect to the source file
add_executable(main)
target_sources(main PUBLIC main.cpp)

# When there are multiple source files
add_executable(main)
target_sources(main PUBLIC main.cpp other.cpp)

# You can use a variable to store for easy subsequent calls, it is recommended to add header files so that they appear in the "Header Files" section in VS. For CMAKE, compilation and linking occur simultaneously
add_executable(main)
set(sources main.cpp other.cpp other.h)
target_sources(main PUBLIC ${sources})

# Of course, according to the above writing, if the project is too complex, it will be cumbersome to add them one by one manually, so you can use the following method
add_executable(main)
file(GLOB sources *.cpp *.h)
target_sources(main PUBLIC ${sources})

# However, there is a problem with this method: when the configuration file is created for the first time and written to the cache file, if you do not reset it, it will skip the cache, so that modifications will not be synchronized to the cache variable. Therefore, you need to use CONFIGURE_DEPENDS option when adding new files, which will automatically update the cache variable
add_executable(main)
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
target_sources(main PUBLIC ${sources})

# All the above methods are for source files in the root directory of CMakeLists.txt
# If the source files are in subfolders, what should be done? The most direct way is:
add_executable(main)
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h mylib/*.cpp mylib/*.h)
target_sources(main PUBLIC ${sources})

# Is there a better way? One is to omit the file name suffix, allowing CMake to automatically search for the corresponding source files according to the selected language
add_executable(main)
aux_sources_directory(. sources)
aux_sources_directory(mylib sources)
target_sources(main PUBLIC ${sources})

# The second way is to omit the subfolder directory, using GLOB_RECURSE to recursively include all files under all subfolders, but this method will also include temporary .cpp files generated in the build directory, which will cause errors. Therefore, it is recommended to place the source code in the src directory
add_executable(main)
file(GLOB_RECURSE sources CONFIGURE_DEPENDS src/*.cpp src/*.h)
target_sources(main PUBLIC ${sources})

So far, a standard CMake template

# Set CMake version range
cmake_minimum_required(VERSION 3.15)

# Set C++ standard version
set(CMAKE_CXX_STANDARD 17)
# Must set CMAKE_CXX_STANDARD value, if OFF, the default version will be used. If the highest version is not available in the system, the previous version closest to the default version will be used
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Initialize project
project(myapp LANGUAGES C CXX)

# Output directory and source directory being the same will be messy, so set a warning here
if (PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR)
    message(WARNING "The binary directory of CMake cannot be the same as source directory!")
endif()

# Set the default compile mode to release mode
if (NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

# To prevent the MINMAX macro bug in Windows, set to cancel the MINMAX macro if compiling under Windows
if (WIN32)
    add_definitions(-DNOMINMAX -D_USE_MATH_DEFINES)
endif()

# Since MSVC cannot use CCache, set to use ccache to speed up compilation except for MSVC
if (NOT MSVC)
    find_program(CCACHE_PROGRAM ccache)
    if (CCACHE_PROGRAM)
        message(STATUS "Found CCache: ${CCACHE_PROGRAM}")
        set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE_PROGRAM})
        set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE_PROGRAM})
    endif()
endif()

How to Add a Library

  1. Make mylib a static library (called at compile time, directly packaged in main)
add_library(mylib STATIC mylib.cpp)

add_executable(main main.cpp)

target_link_libraries(main PUBLIC mylib)

Static library issue: GCC compilation will automatically remove object files that have no referenced symbols

#TODO The latest test can output mylib identification, specific verification is still required

add_library(mylib STATIC mylib.cpp)
#include <stdio.h>

static int unused = printf("mylib identification\n");
</stdio.h>
#include <stdio.h>
int main(){
    printf("main function\n");
}
</stdio.h>

Output is

1main function
  1. Make mylib a dynamic library (called at runtime) there are many pitfalls on Windows
add_library(mylib SHARED mylib.cpp)

add_executable(main main.cpp)

target_link_libraries(main PUBLIC mylib)

On Windows, you need to export and import to use dynamic libraries. The dynamic library file (.dll) can only be found in the .exe file folder, or add the .dll to the PATH (because there is no RPATH in Windows like in Linux), or copy the .dll to the directory where the .exe is located.

#mylib.cpp
#include <stdio.h>

#ifdef _MSC_VER
__declspec(dllexport)
#endif
void say_hello(){
    printf("hello, world\n");
}
</stdio.h>
#mylib.h
#ifdef _MSC_VER
__declspec(dllimport)
#endif
void say_hello();

Alternatively, you need to set six variables in CMake

set_property(TARGET mylib PROPERTY RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY RUNTIME_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY LIBRARY_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY RUNTIME_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY LIBRARY_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})
  1. Make mylib an object library (CMake’s own creation, object libraries are similar to static libraries but do not generate .a files, only using CMake as an intermediary to remember which object files are generated) (recommended to use object libraries instead of static libraries when writing code yourself)
add_library(mylib OBJECT mylib.cpp)

add_executable(main main.cpp)

target_link_libraries(main PUBLIC mylib)
  1. When add_library has no parameters, is it a static library or a dynamic library? In add_library, if STATIC or SHARED is not defined, it will be determined based on the BUILD_SHARED_LIBS variable whether it is a static library or a dynamic library. If ON, it is a dynamic library (SHARED), which can be seen from the name.
# Default is OFF
set(BUILD_SHARED_LIBS ON)

add_library(mylib mylib.cpp)

You can also use this method to set the default value

if (NOT DEFINED BUILD_SHARED_LIBS)
    set(BUILD_SHARED_LIBS ON)
endif
  1. Dynamic libraries cannot link static libraries because static libraries are directly linked to the executable file, while dynamic libraries’ memory addresses will change. Dynamic libraries specify a -fPIC option at compile time, but static libraries do not have this option, which will lead to errors such as “cannot relocate x86_ to….”. In this case, we have two solutions: one is to replace the static library with an object library, and the other is to add the PIC option to the static library code.
#set(CMAKE_POSITION_INDEPENDENT_CODE ON) for global
# Only set the otherlib library to be PIC
add_library(otherlib STATIC otherlib.cpp)
set_property(TARGET otherlib PROPERTY POSITION_INDEPENDENT_CODE ON)

add_library(mylib SHARED mylib.cpp)
target_link_libraries(mylib PUBLIC otherlib.cpp)

add_executable(main main.cpp)
target_link_libraries(main PUBLIC mylib)

Setting Object Properties

set(CMAKE_CXX_STANDARD 17)           # Compile using C++17 standard (default 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)  # If the compiler does not support C++17, report an error directly (default OFF)
set(CMAKE_WIN32_EXECUTABLE ON)       # In Windows, do not start the console window at runtime, only the GUI interface (default OFF)
set(CMAKE_LINK_WHAT_YOU_USE ON)      # Tell the compiler not to automatically remove unused linked libraries (default OFF)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)   # Set the output path for dynamic libraries (default ${CMAKE_BINARY_DIR})
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)   # Set the output path for static libraries (default ${CMAKE_BINARY_DIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)   # Set the output path for executable files (default ${CMAKE_BINARY_DIR})

add_executable(main main.cpp)

Can set a bunch of properties at once

set_target_properties(main PROPERTIES
    CXX_STANDARD 17           # Compile using C++17 standard (default 11)
    CXX_STANDARD_REQUIRED ON  # If the compiler does not support C++17, report an error directly (default OFF)
    WIN32_EXECUTABLE ON       # In Windows, do not start the console window at runtime, only the GUI interface (default OFF)
    LINK_WHAT_YOU_USE ON      # Tell the compiler not to automatically remove unused linked libraries (default OFF)
    LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib   # Set the output path for dynamic libraries (default ${CMAKE_BINARY_DIR})
    ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib   # Set the output path for static libraries (default ${CMAKE_BINARY_DIR})
    RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin   # Set the output path for executable files (default ${CMAKE_BINARY_DIR})
    )

add_executable(main main.cpp)

Note add_executable needs to be after set, meaning property settings need to be before creating the executable file

Also, common mistakes in settings found on Baidu

set_property(TARGET main PROPERTY CXX_STANDARD 17)        # Correct
target_compile_options(main PUBLIC "-std=c++17")          # Incorrect
set_property(TARGET main PROPERTY CUDA_ARCHITECTURES 75)  # Correct
target_compile_options(main PUBLIC "-arch=sm_75")         # Incorrect

Linking Third-Party Libraries

Because the Linux folder system is very standard, with fixed library directories (/usr/lib) (except for NixOS), you can use

add_executable(main main.cpp)
target_link_libraries(main PUBLIC tbb)

Windows does not have standard library directories, so the above method cannot be used

Method 1: Cannot cross devices (not even cross-platform)

add_executable(main main.cpp)
target_link_libraries(main PUBLIC C:/.....)

Method 2: A common approach for both Linux and Windows

add_executable(main main.cpp)

find_package(TBB REQUIRED)
target_link_libraries(main PUBLIC TBB::tbb)

Note If you, like the author, are a little greedy and use NixOS, congratulations, you need to take an extra step

1 Cppdev

2├──  flake.lock

3└──  flake.nix
{
  description = "A CMake usage with third-party libraries that can use find_package";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in {
        devShells.default = pkgs.mkShell {
          # Declare build tools and dependencies
          nativeBuildInputs = with pkgs; [
            cmake
            boost.dev  # Boost headers and libraries, make sure to find the right package
            tbb_2022_0
            gcc # or clang
            # clang
            gdb
            ninja
            lldb
            gnumake
            meson
            clang-tools
            valgrind
          ];

          # Ensure CMake can find Boost's Config mode files
          BOOST_ROOT = "${pkgs.boost.dev}";
          TBB_ROOT = "${pkgs.tbb_2022_0}";
          CMAKE_PREFIX_PATH = "${pkgs.boost.dev}:${pkgs.tbb_2022_0}";

          # Optional: Set environment variables for debugging
          shellHook = ''
            echo "Boost path: $BOOST_ROOT"
            echo "TBB path: $TBB_ROOT"
            echo "CMake prefix path: $CMAKE_PREFIX_PATH"
          '';
        };
      }
    );
}
1cd Cppdev

2nix develop

3...

cmake_minimum_required(VERSION 3.15)
project(hellocmake)

message("PROJECT_NAME: ${PROJECT_NAME}")
message("PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message("PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message("CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message("CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
add_executable(main main.cpp)

add_subdirectory(mylib)

find_package(TBB COMPONENTS tbb tbbmalloc tbbmalloc_proxy REQUIRED)
if(NOT TBB_FOUND)
    message(FATAL_ERROR "TBB not found")
endif()
target_link_libraries(main PUBLIC TBB::tbb TBB::tbbmalloc TBB::tbbmalloc_proxy)

Some libraries have many components, and if you do not set at least one component, an error will occur

set(Qt5_DIR C:/Qt/Qt5.14.2/msvc2019_64/lib/cmake)

find_package(Qt5 REQUIRED COMPONENTS Widgets Gui REQUIRED)
target_link_libraries(main PUBLIC Qt5::Widgets Qt5::Gui)

find_package(TBB COMPONENTS tbb tbbmalloc tbbmalloc_proxy REQUIRED)
target_link_libraries(main PUBLIC TBB::tbb TBB::tbbmalloc TBB::tbbmalloc_proxy)

If you do not want this package to be exposed globally

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} C:/Qt/Qt5.14.2/msvc2019_64/lib/cmake)

find_package(Qt5 REQUIRED COMPONENTS Widgets Gui REQUIRED)
target_link_libraries(main PUBLIC Qt5::Widgets Qt5::Gui)

Second method: set a separate variable to store the path

set(Qt5_DIR C:/Qt/Qt5.14.2/msvc2019_64/lib/cmake)

# REQUIRED will report an error if not found
find_package(Qt5 REQUIRED COMPONENTS Widgets Gui REQUIRED)
target_link_libraries(main PUBLIC Qt5::Widgets Qt5::Gui)

Third method (recommended): set directly in the command line

1cmake -B build -DQt5_DIR="C:/Qt/Qt5.14.2/msvc2019_64/lib/cmake"

Fourth method: setting environment variables is more difficult in Windows

export Qt5_DIR="/..."
cmake -B build

cpp can also use code to avoid errors when finding packages. In .cpp, you can check the WITH_TBB macro, and if TBB is not found, it can degrade to serial mode to execute the for loop

#include <stdio.h>
#ifdef WITH_TBB
#include <tbb tbb.h="">
#endif

int main() {
#ifdef WITH_TBB
    tbb::parallel_for(0, 4, [&] (int i) {
#else
    for (int i = 0; i < 4; i++) {
#endif
        printf("hello, %d!\n", i);
#ifdef WITH_TBB
    });
#else
    }
#endif
}
</tbb></stdio.h>

You can check for the presence of a package, and if it is not found, report an error in time. You can also use conditional statements to use alternative libraries, such as finding Eigen if TBB is not found.

add_executable(main main.cpp)

find_package(TBB)
if (TBB_FOUND)
    message(STATUS "TBB found at: ${TBB_DIR}")
    target_link_libraries(main PUBLIC TBB::tbb)
    target_compile_definitions(main PUBLIC WITH_TBB)
else()
    message(WARNING "TBB not found! using serial for")
endif()

Printing and Output

message("Hello, world!")
message(STATUS "Hello, world!")
message(WARNING "This is a warning sign!")
message(FATAL_ERROR "This is an error message!")

set(myvar "hello world")
message("myvar is: ${myvar}")

set(myvar hello world)
message("myvar is: ${myvar}")

set(myvar FATAL_ERROR hello)
message(${myvar})

Corresponding shell output

1Hello, world!

2

3-- Hello, world!

4

5CMake Warning at CMakeLists.txt:2 (message):

6  This is a warning sign!

7

8CMake Error at CMakeLists.txt:2 (message):

9  This is an error message!

10

11myvar is: hello world

12

13myvar is: hello;world

14

15CMake Error at CMakeLists.txt:2 (message):

16  hello

AUTHOR_WARNING can be turned off with -Wno-dev.

message(AUTHOR_WARNING "This is a warning sign!")

You can turn off warnings with -Wno-dev

1cmake -B build -Wno-dev

CMake Cache

How to clean the cache, some intermediate files can be skipped from recompilation. Note that find_package utilizes the caching mechanism, so be careful. Variables will also be cached, so changes to default values require deleting the cache.

rm -f ./build/CMakeCache.txt

In fact, default values should not be changed by users, so the official recommended way is to use the -D parameter to change them.

1cmake -B build -Dmyvar=world

You can use TUI commands to select variable changes

1ccmake -B build

You can also use FORCE to force update the cache

set(myvar "world" CACHE STRING "this is the docstring." FORCE)
message("myvar is ${myvar}")

You can also create your own cache variable like this, creating a boolean variable WITH_TBB, with a default value of ON

cmake_minimum_required(VERSION 3.15)
project(hellocmake LANGUAGES CXX)

add_executable(main main.cpp)

set(WITH_TBB ON CACHE BOOL "set to ON to enable TBB, OFF to disable TBB.")
if (WITH_TBB)
    target_compile_definitions(main PUBLIC WITH_TBB)
    find_package(TBB REQUIRED)
    target_link_libraries(main PUBLIC TBB::tbb)
endif()

The essence of option is set(..CACHE..), which also utilizes the caching mechanism and can be seen in ccmake.

cmake_minimum_required(VERSION 3.15)
project(hellocmake LANGUAGES CXX)

add_executable(main main.cpp)

option(WITH_TBB "set to ON to enable TBB, OFF to disable TBB." ON)
if (WITH_TBB)
    target_compile_definitions(main PUBLIC WITH_TBB)
    find_package(TBB REQUIRED)
    target_link_libraries(main PUBLIC TBB::tbb)
endif()

Cross-Platform and Compiler

CMake defines a macro for .cpp, allowing you to use CMake to define macros in .cpp files

add_executable(main)
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
target_sources(main PUBLIC ${sources})
target_compile_definitions(main PUBLIC MY_MACRO=233)
#include <stdio.h>

int main() {
#ifdef MY_MACRO
    printf("MY_MACRO defined! value: %d\n", MY_MACRO);
#else
    printf("MY_MACRO not defined!\n");
#endif
}
</stdio.h>

Platform Check

add_executable(main)
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
target_sources(main PUBLIC ${sources})

if (CMAKE_SYSTEM_NAME MATCHES "Windows")
    target_compile_definitions(main PUBLIC MY_NAME="Bill Gates")
elseif (CMAKE_SYSTEM_NAME MATCHES "Linux")
    target_compile_definitions(main PUBLIC MY_NAME="Linus Torvalds")
elseif (CMAKE_SYSTEM_NAME MATCHES "Darwin")
    target_compile_definitions(main PUBLIC MY_NAME="Steve Jobs")
endif()
add_executable(main)
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
target_sources(main PUBLIC ${sources})

if (WIN32)
    target_compile_definitions(main PUBLIC MY_NAME="Bill Gates")
elseif (UNIX AND NOT APPLE)
    target_compile_definitions(main PUBLIC MY_NAME="Linus Torvalds")
elseif (APPLE)
    target_compile_definitions(main PUBLIC MY_NAME="Steve Jobs")
endif()
add_executable(main)
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
target_sources(main PUBLIC ${sources})

# Equivalent to the three statements in the above if and automatically performs the check
target_compile_definitions(main PUBLIC
    $<:MY_NAME="Bill Gates">
    $<:MY_NAME="Linus Torvalds">
    $<:MY_NAME="Steve Jobs">
)

When multiple conditions share a branch, you can separate them with “,”

add_executable(main)
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
target_sources(main PUBLIC ${sources})

target_compile_definitions(main PUBLIC
    $<:MY_NAME="DOS-like">
    $<:MY_NAME="Unix-like">
)

You can also use it to check which compiler it is

add_executable(main)
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
target_sources(main PUBLIC ${sources})

if (CMAKE_CXX_COMPILER_ID MATCHES "GNU")
    target_compile_definitions(main PUBLIC MY_NAME="gcc")
elseif (CMAKE_CXX_COMPILER_ID MATCHES "NVIDIA")
    target_compile_definitions(main PUBLIC MY_NAME="nvcc")
elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    target_compile_definitions(main PUBLIC MY_NAME="clang")
elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
    target_compile_definitions(main PUBLIC MY_NAME="msvc")
endif()

If the if statement itself has an assignment operation

set(MYVAR Hello)
if (MYVAR MATCHES "Hello")
    message("MYVAR is Hello")
else()
    message("MYVAR is not Hello")
endif()

add_executable(main main.cpp)

MYVAR = world

set(Hello world)
set(MYVAR Hello)
if (${MYVAR} MATCHES "Hello")
    message("MYVAR is Hello")
else()
    message("MYVAR is not Hello")
endif()

add_executable(main main.cpp)

MYVAR = hello, adding “” will not be evaluated

set(Hello world)
set(MYVAR Hello)
if ("${MYVAR}" MATCHES "Hello")
    message("MYVAR is Hello")
else()
    message("MYVAR is not Hello")
endif()

add_executable(main main.cpp)
1MYVAR is Hello

2MYVAR is not Hello

3MYVAR is Hello

CMake Case Sensitivity

CMake does not distinguish between commands, but variable file paths are case-sensitive.

Passing Variables from Submodules to Parent Modules

1 04

2├──  CMakeLists.txt

3├──  main.cpp

4└──  mylib

5    └──  CMakeLists.txt
#04/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)

set(MYVAR OFF)
add_subdirectory(mylib)
message("MYVAR: ${MYVAR}")

add_executable(main main.cpp)
#04/mylib/CMakeLists.txt
set(MYVAR ON PARENT_SCOPE)
1MYVAR is ON

Video link:

https://www.bilibili.com/video/BV16P4y1g7MH/?spm_id_from=333.1391.0.0&vd_source=4e582285f531be6868d482c5bffda858

Leave a Comment