CMake Turorial 官方教程个人学习笔记,附带 vscode 配置

CMake Turorial 个人笔记

本文是我学习 cmake 官方教程中记录的一些笔记,cmake目前的官方教程还是不错的,给出了12个Step,每个Step有若干Exercise做练习,这些内容都是可以运行的,在官方的GitHub仓库给出了这些Step的源代码,位置在 Help/guide/tutorial,或者你也可以使用下面我给出的百度网盘链接下载。

官方教程链接 CMake Tutorial — CMake 4.1.0 Documentation

官方 Github 仓库 Kitware/CMake: Mirror of CMake upstream repository

Turorial 源代码 https://pan.baidu.com/s/172iKEf52NNbd_pneBgvUrA?pwd=9i3n 提取码: 9i3n

一个典型的 cmake 项目配置过程

~/package $ mkdir build
~/package $ cd build
~/package/build $ cmake ..
~/package/build $ cmake --build .

cmake 工作流程大致如下,第一步 cmake .. 会根据 .. 目录下的 CMakeLists.txt 配置文件生成构建系统配置文件,常见构建系统有 Unix MakeFiles Ninja Visual Studio 17 2022 等,你可以通过 cmake -G 来查看并指定构建系统。第二步使用生成的构建系统的配置文件进行构建,例如 Unix Makefiles 会使用 make 命令,Ninja 使用 ninja 等,而 cmake --build . 是一种通用的命令,cmake会自行根据 . 目录自动构建。

# 使用 Visual Studio 2022 生成器
cmake -G "Visual Studio 17 2022" ..

# 使用 Ninja 生成器
cmake -G "Ninja" ..

# 使用 MinGW Makefiles 生成器
cmake -G "MinGW Makefiles" ..

一些资料中把第一步叫配置 (Configuration),第二步叫构建 (Build)

Step1: A Basic Starting Point

Exercise1: Building a Basic Project

  1. 添加最低版本要求
cmake_minimum_required(VERSION <min>[...<policy_max>] [FATAL_ERROR])
  • <min>:指定最低版本号(如 3.10),低于此版本会报错并停止配置。
  • <policy_max>(可选):指定 CMake 的策略兼容性最大版本,通常不用。
  • FATAL_ERROR(可选):如果版本不满足,立即终止并报错。

例如

cmake_minimum_required(VERSION 3.10...4.0.1 FATAL_ERROR)
  1. 添加项目名称和版本号
project(Tutorial VERSION 1.0)

调用 projectcmakelist.txt 是顶层配置文件,其路径是 ${PROJECT_SOURCE_DIR}

  1. 添加可执行目标 (executable target)
add_executable(Tutorial tutorial.cxx)

Exercise2: Specifying the C++ Standard

  1. 设置C++标准并启用,需要在 add_executable() 前设置
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

这种设置是全局的,在 Step3 我们会使用一种更精确可控的方法添加C++标准

Exercise3: Configured Header File

这部分我们定义了一个 .h.in 文件,其中所有形如的 @VAR@ 的内容都会被 cmake 使用 configure_file 替换,并输出 .h 文件

  1. 替换文件中的 cmake 变量

使用 configure_fileTutorialConfig.h.in 中的cmake变量替换并生成新文件 TutorialConfig.h

# CMakeLists.txt
configure_file(TutorialConfig.h.in TutorialConfig.h)
// TutorialConfig.h.in
// cmake 变量 Tutorial_VERSION_MAJOR 是主版本号,第二个是次版本号
// 在执行 project(<name> VERSION 1.0) 时就已经设置了 
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
// TutorialConfig.h
#define Tutorial_VERSION_MAJOR 1
#define Tutorial_VERSION_MINOR 0
  1. 添加 include 目录
target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           )

这里使用了cmake内置变量 ${PROJECT_BINARY_DIR} 代表 build 构建目录

Step2: Adding a Library

Exercise1: Creating a Library

  1. 添加一个库目标 (library target)
## add_library(<target> <source file>)
add_library(MathFunctions MathFunctions.cxx)
  1. 添加子目录
## add_subdirectory(dir)
add_subdirectory(MathFunctions)
  1. 将库目标 (library target) 链接到可执行目标 (executable target)
target_link_libraries(Tutorial PUBLIC MathFunctions)

链接的可见性

public private interface 分别对于以下目标可见

  • PUBLIC 目标 + 依赖目标的目标
  • PRIVATE 目标
  • INTERFACE 依赖目标的目标

什么是可见

  • 编译选项的可见性:某些编译选项(如 -D 宏定义、-I 头文件路径)在目标编译时是否使用 (例如 target_compile_optionstarget_compile_definitions 等可以设置)

  • 头文件路径的可见性:目标能否引入这些路径下的头文件,即在代码中 include (target_include_directories 可以设置)

  • 链接库的可见性:目标在链接时是否使用该库 (target_link_libraries 可以设置)

  1. 添加include路径
target_include_directories(Tutorial PUBLIC
                          "${PROJECT_BINARY_DIR}"
                          "${PROJECT_SOURCE_DIR}/MathFunctions"
                          )

内置变量

  • ${PROJECT_BINARY_DIR}:项目的构建目录,也就是运行 cmake .. 时所在的目录,例如 build 目录
  • ${PROJECT_SOURCE_DIR}: 项目顶层CMakeLists.txt 文件的目录

在这里添加 ${PROJECT_BINARY_DIR} 目的是引入被替换的 include.in.h 文件
添加 ${PROJECT_SOURCE_DIR} 目的引入 MathFunctions 库中的头文件

Exercise2: Adding an Option

例如,C/C++ 可以通过命令行传递参数,cmake 也可以在cmake配置时传递参数,这些参数变量会被缓存在本地,无需在每次构建时重新配置cmake并设置该值

cmake .. -DUSE_MYMATH=OFF ## DUSE_MYMATH 是自定义选项
cmake --build .
  1. 设置选项 (option)

option 创建一个布尔缓存变量

option(USE_MYMATH "Use tutorial provided math implementation" ON)
  1. 传递编译定义

编译器会接收到一个编译参数 -DUSE_MYMATH ,即添加一个 USE_MYMATH 的宏定义,效果相当于 #define USE_MYMATH

target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")

还可以添加带值的编译定义,效果相当于 #define VALUE 1

target_compile_definitions(MyTarget PUBLIC "VALUE=1")

这样定义后,MathFunctions 所属的源文件就可以使用这个宏定义了,例如

#ifdef USE_MYMATH
  return detail::mysqrt(x);
#else
  return std::sqrt(x);
#endif
  1. cmake 中的 if 语句
if (USE_MYMATH)
  target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
endif(USE_MYMATH) ## endif 括号的内容仅作注释,无实际效果
  1. 利用编译选项 USE_MYMATH

例如,我们可以在 cmakelist.txt 中创建一个 if 代码块,在代码块中编译对应的项目,实现一个库的可选编译

add_library(MathFunctions MathFunctions.cxx)

option(USE_MYMATH "Use tutorial provided math implementation" ON)

if(USE_MYMATH)
    # 添加编译器宏定义 等价于给编译器传递-D选项
    target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")

    # 仅在 USE_MYMATH=ON 时链接 SqrtLibrary
    add_library(SqrtLibrary STATIC
        mysqrt.cxx
    )
    target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif(USE_MYMATH) # 括号内容无所谓

上面代码实现的功能是,当使用 USE_MYMATHON 时,加入我们自己实现的链接库,否则就不使用自定义的链接库

相应的 target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH") 语句给编译器传递了信息,在源代码中要使用这个信息同时适配两种情况

#ifdef USE_MYMATH
  return detail::mysqrt(x);
#else
  return std::sqrt(x);
#endif
  1. 查看 cmake 选项
cmake -L
CMAKE_BUILD_TYPE:STRING=Debug
CMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE
CMAKE_INSTALL_PREFIX:PATH=C:/Program Files (x86)/Tutorial
USE_MYMATH:BOOL=OFF

Step3: Adding Usage Requirements for a Library

Exercise 1 - Adding Usage Requirements for a Library

我们可以给一个库目标添加使用需求,这些需求可以被传递给其他目标

例如头文件搜索目录就是一个使用需求,对于链接到这个库目标的可执行目标,不需要手动引入库目录作为include文件搜索路径,而是由库目标本身定义的使用需求 (usage requirements) 指出 include 路径,并传递给可执行目标

  1. 对库目标的使用需求进行修改,使其符合 Modern CMake

库目标对 current source directory 是不需要的,也就是使用 INTERFACE 可见性

target_include_directories(MathFunctions
                           INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
                           )

让我们这样声明的动机是,一个库中会有接口头文件,库的源代码文件不会使用这些头文件,而任何链接到这个库的目标都需要使用这些头文件。cmake 官方教程中把 INTERFACE 比喻为消费者 (consumers) 需要但生产者 (producer) 不需要

使用这种技术,可以使得可执行目标使用一个库时,唯一要做的就是 target_link_libraries() 而不需要添加头文件搜索路径,在大型项目中很有用

Exercise 2 - Setting the C++ Standard with Interface Libraries

  1. 使用接口库而不是 set() 变量来设置我们的 C++ 标准
add_library(tutorial_compiler_flags INTERFACE)
target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)
类型生成文件作用
STATIC静态库(.lib嵌入到目标中,独立于运行时。
SHARED动态库(.dll在运行时动态加载,多个目标共享。
INTERFACE无文件仅传递编译选项、包含路径等。
OBJECT目标文件(.o生成目标文件供其他目标使用,不生成库文件。
ALIAS无文件为现有目标创建别名,不能添加源文件或属性。

target_compile_features 的主要作用是为指定的目标启用某些编译器特性,例如 C++ 标准(如 cxx_std_11)或其他语言功能。CMake 会根据这些特性自动设置适当的编译器标志(如 -std=c++11/std:c++11

  • PRIVATE: 仅对目标自身生效。
  • PUBLIC: 对目标自身和依赖它的目标生效。
  • INTERFACE: 仅对依赖该目标的目标生效。

采用上述方法设置 C++ 标准的好处是,相比起全局设置的 set(),这种方法可以精确控制每个目标的 C++ 标准特性,更加灵活。

Step4: Adding Generator Expressions (生成器表达式)

Exercise 1: Adding Compiler Warning Flags with Generator Expressions

生成器表达式是一段模式字符串,cmake会在运行时动态替换这段字符串

  1. 使用生成器表达式确定编译器
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")

COMPILE_LANG_AND_ID 是 CMake 中的一个生成器表达式,用于检查当前编译器的语言和编译器 ID,如果当前编译器的语言是 lang,并且编译器的 ID 是 id1id2 等之一,则返回 1(真),否则为 0

$<COMPILE_LANG_AND_ID:lang,id1,id2,...>
  1. 使用 $<condition:value> 添加编译选项
target_compile_options(tutorial_compiler_flags INTERFACE
  "$<${gcc_like_cxx}:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>"
  "$<${msvc_cxx}:-W3>"
)
  • condition:一个布尔条件。如果条件为真,则返回 value;否则返回空字符串。
  • value:当条件为真时使用的值。
  1. 设置仅在构建时提示警告信息,而不是安装时
target_compile_options(tutorial_compiler_flags INTERFACE
  "$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
  "$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)

BUILD_INTERFACE 是 CMake 的一个生成器表达式,用于区分构建时安装后使用时的行为

$<BUILD_INTERFACE:content>

Step5: Installing and Testing

在执行 cmake --build . 构建成功后,install 实际做的就是拷贝 build 目录里的目标文件

cmake --install .

对于多配置,使用 --config 参数指定

cmake --install . --config Release

使用 --prefix 指定安装路径,这会影响 cmake 变量 CMAKE_INSTALL_PREFIX

cmake --install . --prefix "/home/myuser/installdir"

Exercise 1: Install Rules

  1. 安装库目标

对于一个库,我们希望将库和头文件分别安装到 libinclude 目录中

# 将要安装的库目标打包到一个变量
set(installable_libs MathFunctions tutorial_compiler_flags)
if(TARGET SqrtLibrary)
  list(APPEND installable_libs SqrtLibrary)
endif()

install(TARGETS ${installable_libs} DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

DESTINATION 指定安装目标的路径,lib 表示将这些目标安装到安装目录下的 lib 子目录中,安装目录的根路径由 CMAKE_INSTALL_PREFIX 决定,默认是 /usr/local(Linux/macOS)或 C:/Program Files

  1. 安装可执行目标

对于可执行文件,我们希望将可执行文件和配置的头文件分别安装到 bininclude 目录中

install(TARGETS Tutorial DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
  DESTINATION include
  )

Exercise 2: Testing Support

ctest -N # 列出所有测试
ctest -VV # 运行所有测试

如果使用多配置生成器(例如 Visual Studio),还需要指定类型 -C <Debug | Release>

VV 表示 “Very Verbose”

  1. 测试可执行文件

启动测试

enable_testing()

使用 add_test() 添加测试项,这里我们没有检查输出是否正确

add_test(NAME Runs COMMAND Tutorial 25)
  • NAME:指定测试的名称。
  • Runs:测试的名称,用户可以通过这个名称识别和运行测试。例如,运行 ctest -R Runs 会只执行这个测试。
  • COMMAND:指定测试运行时要执行的命令。
  • Tutorial:要运行的可执行文件的名称
  • 25:传递给 Tutorial 可执行文件的参数

ctest -R Runs 并不会让程序输出结果,仅仅显示是否运行通过

使用 PASS_REGULAR_EXPRESSION 验证输出字符串是否正确

add_test(NAME Usage COMMAND Tutorial)
set_tests_properties(Usage
  PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number"
  )
  1. 使用函数来快速添加测试项
function(do_test target arg result)
  add_test(NAME Comp${arg} COMMAND ${target} ${arg})
  set_tests_properties(Comp${arg}
    PROPERTIES PASS_REGULAR_EXPRESSION ${result}
    )
endfunction()

# do a bunch of result based tests
do_test(Tutorial 4 "4 is 2")
do_test(Tutorial 9 "9 is 3")
do_test(Tutorial 5 "5 is 2.236")
do_test(Tutorial 7 "7 is 2.645")
do_test(Tutorial 25 "25 is 5")
do_test(Tutorial -25 "-25 is (-nan|nan|0)")
do_test(Tutorial 0.0001 "0.0001 is 0.01")

Step 6: Adding Support for a Testing Dashboard

ctest [-VV] -D Experimental

-D Experimental:以实验性模式运行测试,并将结果提交到 CDash

cmake 其他测试模式

  • Nightly:用于夜间构建和测试。
  • Continuous:用于持续集成测试。

夜间构建(Nightly Build) 是一种软件开发中的自动化构建和测试策略,通常在每天的固定时间(例如午夜)触发。它的主要目的是在开发者工作时间之外,自动构建项目、运行测试并生成报告,以便在第二天开发者开始工作时可以查看构建和测试的结果。

CTestConfig.cmake 中配置了 CDash

# ./CTestConfig.cmake
set(CTEST_NIGHTLY_START_TIME "00:00:00 EST") # 夜间开始时间

set(CTEST_SUBMIT_URL "https://my.cdash.org/submit.php?project=CMakeTutorial") # 要发送到的url

Exercise1: Send Results to a Testing Dashboard

  1. 添加仪表盘支持

唯一要做的就是修改 enable_testing()include(CTest),后者的作用如下

  • 启用测试功能,使得后续的 add_test()do_test() 函数可以正常工作。
  • 配合 CTestConfig.cmake 文件,支持将测试结果提交到 CDash。
# Replace enable_testing() with include(CTest)
# enable_testing()
include(CTest)

Step 7: Adding System Introspection (系统探查)

System Introspection(系统探查) 是指通过 CMake 脚本检测和查询目标系统的特性、工具链、库、头文件、编译器功能等信息的过程。这种探查通常用于确保项目能够在不同的平台和环境中正确构建。

有这样一个问题,我们想知道当前头文件是否提供了某些函数,例如 logexp ,使用cmake脚本可以完成这件事

  1. System Introspection

要解决这个问题,需要引入 CheckCXXSourceCompiles 模块

# ./CMakeLists.txt
include(CheckCXXSourceCompiles)

这个模块提供了 check_cxx_source_compiles

check_cxx_source_compiles(SOURCE_CODE VARIABLE)
  • SOURCE_CODE: 一段 C++ 源代码(以字符串形式提供)。
  • VARIABLE: 一个变量名,用于存储编译结果。如果代码能够成功编译,变量会被设置为 1,否则为 0

使用如下代码验证是否存在 logexp 函数,并将结果保存在 HAVE_LOGHAVE_EXP 变量中

    check_cxx_source_compiles("
    #include <cmath>
    int main() {
      std::log(1.0);
      return 0;
    }
  " HAVE_LOG)
    check_cxx_source_compiles("
    #include <cmath>
    int main() {
      std::exp(1.0);
      return 0;
    }
  " HAVE_EXP)

传递变量到目标

if(HAVE_LOG AND HAVE_EXP)
        target_compile_definitions(SqrtLibrary
            PRIVATE "HAVE_LOG" "HAVE_EXP"
        )
endif()

这样在我们的源代码中,就可以使用这两个宏 HAVE_LOG HAVE_EXP 来分情况编写代码了

#if defined(HAVE_LOG) && defined(HAVE_EXP)
  double result = std::exp(std::log(x) * 0.5);
  std::cout << "Computing sqrt of " << x << " to be " << result
            << " using log and exp" << std::endl;
#else
  double result = x;

  // do ten iterations
  for (int i = 0; i < 10; ++i) {
    if (result <= 0) {
      result = 0.1;
    }
    double delta = x - (result * result);
    result = result + 0.5 * delta / result;
    std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;
  }
#endif

Step 8: Adding a Custom Command and Generated File

下面的示例演示的是,在构建过程中执行步骤:运行自定义命令来生成文件,并将文件添加到我们的源代码列表中

具体来说,我们编写了 MakeTable.cxx ,其生成可执行文件 MakeTable ,运行 MakeTable <filepath> 生成一个 Table.h 文件,之后我们 MathFunction 库的源代码要使用 Table.h

  1. 编写第一部分,运行自定义命令生成文件

声明:以下的 ./ 目录指的都是 build 目录

我们在 ./MathFuctions 下新建 MakeTable.cmake 文件,这个文件将来会在 ./MathFuctions/CMakeLists.txt 中被 include

# ./MathFuctions/MakeTable.cmake
add_executable(MakeTable MakeTable.cxx)

target_link_libraries(MakeTable PRIVATE tutorial_compiler_flags)

message("CMAKE_CURRENT_BINARY_DIR: " ${CMAKE_CURRENT_BINARY_DIR})

add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  DEPENDS MakeTable
  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
  )
# ./MathFuctions/CMakeLists.txt
include(MakeTable.cmake)

add_custom_command 用于定义一个自定义命令

add_custom_command(
  OUTPUT <output-file>         # 指定生成的文件
  COMMAND <command> [args...]  # 要执行的命令及其参数
  DEPENDS <dependencies>       # 依赖的文件或目标
  WORKING_DIRECTORY <dir>      # (可选)指定命令执行的工作目录
  COMMENT <comment>            # (可选)构建时显示的注释
  VERBATIM                    # (可选)确保命令参数被正确转义
)

tips
这里 ${CMAKE_CURRENT_BINARY_DIR} 的值是 ./MathFuctions,因为我们是在 ./MathFuctions/CMakeLists.txt 中运行这些命令的 ( 见 include(MakeTable.cmake) ),同时可执行文件 MakeTable 也生成在这个目录,因此不要忘了添加 WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
顶层cmake配置中 ${CMAKE_CURRENT_BINARY_DIR} 值是根目录也就是 build 目录,此外这里我们为了简便用的是相对路径,实际上是绝对路径

接下来修改 MathFunctions/CMakeLists.txt,为 SqrtLibrary 库添加新的文件

add_library(SqrtLibrary STATIC
            mysqrt.cxx
            ${CMAKE_CURRENT_BINARY_DIR}/Table.h
            )
# 添加 include 搜索路径
target_include_directories(SqrtLibrary PRIVATE
                            ${CMAKE_CURRENT_BINARY_DIR}
                            )

注意!!

这里 ${CMAKE_CURRENT_BINARY_DIR}/Table.h 被添加到了源文件列表,但是头文件一般不是以 target_include_directories 方式添加搜索路径就可以了吗?

实际上,添加搜索路径只是让编译器知道,头文件可以在这些路径搜索,但是在这个示例中,Table.h 是依赖构建系统动态生成的,而 CMake 需要显式知道哪些文件是构建目标的一部分,这就需要显示地将 ${CMAKE_CURRENT_BINARY_DIR}/Table.h 添加到 SqrtLibrary 的源文件列表中来实现

CMake 会将 Table.h 的生成规则(由 add_custom_command 定义)与 SqrtLibrary 的构建关联起来,从而确保在构建 SqrtLibrary 之前,Table.h 已经被正确生成

Step 9: Packaging an Installer

我们想将项目分发给他人,这个需求具体来说是,我们希望在各种平台上提供源代码二进制发行版, CPack可以实现这个功能

  1. 向顶层 CMakeLists.txt 添加 CPack 相关配置
# ./CMakeLists.txt
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set(CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set(CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
set(CPACK_GENERATOR "TGZ")
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)
  • include(InstallRequiredSystemLibraries) 该模块会自动检测并包含当前系统所需的运行时库(例如,Windows 上的 Visual C++ 运行时库),确保生成的二进制程序在目标平台上可以正常运行

  • set() 设置 CPACK 变量,主要有许可证路径、版本号、二进制安装包格式、源代码包格式

  • include(CPack) 启用 CPack 模块

设置源代码打包忽略文件

set(CPACK_SOURCE_IGNORE_FILES
    "/build/[^/];/.git/;~$;${CPACK_SOURCE_IGNORE_FILES};/.cache"
)
  1. CPack 相关命令
# 根据 CPACK_GENERATOR 设置生成二进制程序包
# 或全写为 cpack --config CPackConfig.cmake
cpack
# 根据 CPACK_SOURCE_GENERATOR 打包源代码
cpack --config CPackSourceConfig.cmake

Step 10: Selecting Static or Shared Libraries

使用变量来控制 add_libraries 的行为,即控制如何构建没有显式指明类型 (STATIC SHARED MODULE OBJECT) 的库

使用 option 创建一个变量允许用户自行选择 ( 关于 optionStep2 :: Exercies2 )

option(BUILD_SHARED_LIBS "Build using shared libraries" ON)

设置静态库和共享库的输出目录,注意 MathFunciotns 目录的 ${PROJECT_BINARY_DIR} 值是 ./build/MathFunctions 而非 ./build

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")

修改 MathFunctions/MathFunctions.h 以使用 dll 导出

// MathFunctions/MathFunctions.h
#if defined(_WIN32)
#  if defined(EXPORTING_MYMATH)
#    define DECLSPEC __declspec(dllexport)
#  else
#    define DECLSPEC __declspec(dllimport)
#  endif
#else // non windows
#  define DECLSPEC
#endif

namespace mathfunctions {
double DECLSPEC sqrt(double x); // 使用 DECLSPEC 导出符号
}

并且向其传递宏 EXPORTING_MYMATH

## MathFunctions/CMakeLists.txt
# define the symbol stating we are using the declspec(dllexport) when
# building on windows
target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")

MathFunctions/CMakeLists.txt 中添加,通过变量控制未显示指定库类型的 add_libraries 的行为,这里实际做的是设置库目标生成位置无关代码,因为默认的非位置无关代码只能生成静态库

## MathFunctions/CMakeLists.txt
# state that SqrtLibrary need PIC when the default is shared libraries
set_target_properties(SqrtLibrary PROPERTIES
    POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS}
    )

运行并编译

cmake .. -G "Unix Makefiles" -DBUILD_SHARED_LIBS=ON
cmake --build .

注意动态库和可执行文件没有在一个目录,直接运行会找不到 .dll 文件

Step 11: Adding Export Configuration

Step5: Installing and Testing 实现了安装库和头文件到本地目录的功能,在 Step 9: Packaging an Installer 实现了打包项目以发送给别人的功能。

现在有一个问题,他人拿到我们的项目包,解压并安装后,这些库是存放在他人自己的系统路径下的,而每个人的系统路径可能不一样,这样无法实现他人直接使用我们的项目。

  1. 修改 install 命令,使其导出库的配置文件

原先的 install 包含了要安装的目标 TARGET 和位置 DESTINATION,现在还需要添加 EXPORT MathFunctionsTargets

# MathFunctions/CMakeLists.txt
install(TARGETS ${installable_libs}
        EXPORT MathFunctionsTargets
        DESTINATION lib)

这样会导出一个配置文件 MathFunctionsTargets.cmake

  1. 安装导出库的配置文件

在顶层添加

install(EXPORT MathFunctionsTargets
  FILE MathFunctionsTargets.cmake
  DESTINATION lib/cmake/MathFunctions
)
  1. 修改源目录路径

按照上述设置完后运行 cmake 会出现一个错误

PS D:\Codes\CMake_Tutorial\Step11\build> cmake ..
-- Configuring done (0.2s)
CMake Error in MathFunctions/CMakeLists.txt:
  Target "MathFunctions" INTERFACE_INCLUDE_DIRECTORIES property contains
  path:

    "D:/Codes/CMake_Tutorial/Step11/MathFunctions"

  which is prefixed in the source directory.


-- Generating done (0.0s)
CMake Generate step failed.  Build files cannot be regenerated correctly.

这个错误是因为 MathFunctions 的 INTERFACE_INCLUDE_DIRECTORIES 属性包含了一个源目录路径(MathFunctions),而 CMake 不允许在安装时将源目录路径暴露给其他项目。这是因为安装后的项目可能无法访问源目录路径。

查找 MathFunctions/CMakeLists.txt 关于 MathFunctions 的相关信息可以看到这里使用了源路径

target_include_directories(MathFunctions
                           INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
                           )

将其修改为如下,这里使用了生成器表达式语法,BUILD_INTERFACEINSTALL_INTERFACE 用于区分构建时和安装时

target_include_directories(MathFunctions
                           PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
                                  $<INSTALL_INTERFACE:include>
                           )
  1. 生成一个MathFunctionsConfig.cmake文件,以便CMake的 find_package() 命令可以找到我们的项目

引入CMakePackageConfigHelpers 模块 ,其提供了 configure_package_config_file,使用上和 configure_file() 有细微不同

include(CMakePackageConfigHelpers)
# generate the config file that includes the exports
configure_package_config_file(
  ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
  "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake"
  INSTALL_DESTINATION "lib/cmake/MathFunctions"
  NO_SET_AND_CHECK_MACRO
  NO_CHECK_REQUIRED_COMPONENTS_MACRO
  )

前两行是输入和输出文件路径,和 configure_file() 用法类似,${CMAKE_CURRENT_SOURCE_DIR} 指的是源代码根目录${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake" 是指定配置文件的安装路径。

剩下两个我也不知道有什么用,这是ai的解释:
NO_SET_AND_CHECK_MACRO禁用生成 set_and_check 宏。
NO_CHECK_REQUIRED_COMPONENTS_MACRO 禁用生成的宏中对组件依赖的检查。

要输入的 Config.cmake.in 模板文件如下。
当 CMake 处理一个 .cmake 文件时,CMAKE_CURRENT_LIST_DIR 会自动设置为该文件所在的目录路径。这在需要引用与当前脚本文件相关的资源(如其他 .cmake 文件或目标文件)时非常有用。当其他项目通过 find_package(MathFunctions) 加载 MathFunctionsConfig.cmake 时,CMAKE_CURRENT_LIST_DIR 会指向安装目录中的 lib/cmake/MathFunctions
@PACKAGE_INIT@ 是 CMake 的一个占位符,它会在 configure_package_config_file 命令执行时被替换为一段初始化代码。这段代码通常用于设置一些与包相关的变量和环境。这段初始化代码确保生成的配置文件(如 MathFunctionsConfig.cmake)能够正确设置包的基本信息。它为后续的 find_package 和依赖管理提供必要的上下文。

// Config.cmake.in
@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/MathFunctionsTargets.cmake") // 这是刚刚导出库的配置

使用 write_basic_package_version_file 写入一个文件,该文件由find_package()使用,记录所需包的版本和兼容性。

write_basic_package_version_file(
  "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
  VERSION "${Tutorial_VERSION_MAJOR}.${Tutorial_VERSION_MINOR}"
  COMPATIBILITY AnyNewerVersion
)

安装上述两个文件

install(FILES
  ${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake
  ${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake
  DESTINATION lib/cmake/MathFunctions
  )

此时,我们已经为我们的项目生成了一个可重定位的CMake配置,可以在项目安装或打包后使用。如果我们也希望我们的项目可以从构建目录中使用,我们只需在顶级CMakeLists.txt的底部添加以下内容

export(EXPORT MathFunctionsTargets
  FILE "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsTargets.cmake"
)

其中 MathFunctionsTargets 是我们在 MathFunctions\CMakeLists.txt 中的导出库的配置

之后,其他项目要使用我们的项目时,就可以通过如下方法

find_package(MathFunctions REQUIRED)

add_executable(MyExecutable main.cpp)
target_link_libraries(MyExecutable PRIVATE MathFunctions) # 链接我们的库

Step 12: Packaging Debug and Release

通过 CPack 打包一个同时包含 debugrelease 版本的二进制文件包

设置 debug 版本的二进制文件后缀

# CMakeLists.txt
set(CMAKE_DEBUG_POSTFIX -debug)

添加 DEBUG_POSTFIX 属性

# CMakeLists.txt
add_executable(Tutorial tutorial.cxx)
set_target_properties(Tutorial PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX})

MathFunctions 库添加版本号,在 MathFunctions/CMakeLists.txt 中,设置 VERSIONSOVERSION 属性

# MathFunctions/CMakeLists.txt
set_property(TARGET MathFunctions PROPERTY VERSION "1.0.0")
set_property(TARGET MathFunctions PROPERTY SOVERSION "1")

创建如下所示目录

- Step12
   - debug
   - release

使用 CMAKE_BUILD_TYPE来设置配置类型

cd debug
cmake -DCMAKE_BUILD_TYPE=Debug .. -G "Unix Makefiles"
cmake --build .
cd ../release
cmake -DCMAKE_BUILD_TYPE=Release .. -G "Unix Makefiles"
cmake --build .

自定义配置文件将这两个构建打包成一个单一的发布。在 Step12 目录中,创建一个名为 MultiCPackConfig.cmake 的文件。在此文件中,首先包含由 cmake 可执行文件创建的默认配置文件。

接下来,使用 CPACK_INSTALL_CMAKE_PROJECTS 变量来指定要安装的项目。在这种情况下,我们想同时安装调试版和发布版。

# MultiCPackConfig.cmake
include("release/CPackConfig.cmake")

set(CPACK_INSTALL_CMAKE_PROJECTS
    "debug;Tutorial;ALL;/"
    "release;Tutorial;ALL;/"
    )

Step12 目录中,运行 cpack,并使用 config 选项指定我们的自定义配置文件

cpack --config MultiCPackConfig.cmake

Vscode 中的 cmake 配置

基础

使用的工具链:cmake + clangd + clang llvm

工作流:cmake 配置 => 在 build 目录下生成 compile_commands.json => clangd 语言服务器读取 compile_commands.json实现语法提示 => cmake 生成

完整配置

	/* clangd */
    "clangd.path": "clangd",
    "clangd.enableHover": true,
    "clangd.onConfigChanged": "restart",
    "clangd.arguments": [
        // 在后台自动分析文件(基于complie_commands)
        "--background-index",
        // 全局补全(会自动补充头文件)
        "--all-scopes-completion",
        // Clang-Tidy 以提供「静态检查」
        "--clang-tidy",
        // compelie_commands.json 文件的目录位置(相对于工作区,由于 CMake 生成的该文件默认在 build 文件夹中,故设置为 build)
        "--compile-commands-dir=build",
        // 建议风格:打包(重载函数只会给出一个建议);反可以设置为detailed
        "--completion-style=bundled",
        // 启用 .clangd 配置文件
        "--enable-config",
        // 默认格式化风格: 谷歌开源项目代码指南(可用的有 LLVM, Google, Chromium, Mozilla, Webkit, Microsoft, GNU 等)
        "--fallback-style=Google",
        // 启用这项时,补全函数时,将会给参数提供占位符,键入后按 Tab 可以切换到下一占位符,乃至函数末
        // 我选择禁用
        "--function-arg-placeholders=false",
        // 输入建议中,已包含头文件的项与还未包含头文件的项会以圆点加以区分
        "--header-insertion-decorators",
        // 允许自动补充头文件
        "--header-insertion=iwyu",
        // 让 Clangd 生成更详细的日志
        "--log=verbose",
        // pch优化的位置(memory 或 disk,选择memory会增加内存开销,但会提升性能)
        "--pch-storage=memory",
        // 输出的 JSON 文件更美观
        "--pretty",
        // 建议排序模型
        "--ranking-model=heuristics",
        // 同时开启的任务数量
        "-j=12"
    ],
    // 编译失败备选标志,例如添加-Ipath
    "clangd.fallbackFlags": [],
    "clangd.enableCodeCompletion": true,
    "clangd.enable": true,
    /** cmake **/
    // 保存 cmake.sourceDirectory 或 CMakeLists.txt 内容时,不自动配置 CMake 项目目录
    "cmake.configureOnEdit": false,
    // 在 CMake 项目目录打开时自动对其进行配置
    "cmake.configureOnOpen": false,
    // 成功配置后,将 compile_commands.json 复制到此位置
    "cmake.copyCompileCommands": "", // clangd 已设置 --compile-commands-dir=build,所以无需导出 compile_commands.json
    "cmake.debugConfig": {
        "type": "lldb"
    },
    "cmake.cmakePath": "cmake",
    "cmake.outputLogEncoding": "65001", // utf-8 编码,如果你使用 GBK 则需要修改
    "cmake.generator": "Unix Makefiles", // 可以换成 "Ninja",实际这里就是 -G 参数后的内容,你可以用 cmake -G 查看可选的生成器

关键设置

"clangd.path": "clangd",
"clangd.arguments": [
    // compelie_commands.json 文件的目录位置(相对于工作区,由于 CMake 生成的该文件默认在 build 文件夹中,故设置为 build)
    "--compile-commands-dir=build",
]

// 保存 cmake.sourceDirectory 或 CMakeLists.txt 内容时,不自动配置 CMake 项目目录
"cmake.configureOnEdit": false,
"cmake.cmakePath": "cmake",
// 在 CMake 项目目录打开时自动对其进行配置
"cmake.configureOnOpen": false,
"cmake.generator": "Unix Makefiles", // 可以换成 "Ninja",实际这里就是 -G 参数后的内容,你可以用 cmake -G 查看可选的生成器

进阶 - 配置 CMakePresets.json 和 CMakeUserPresets.json

CMakePresets.jsonCMakeUserPresets.json 是 CMake 3.19 引入的一种配置文件,用于简化和标准化 CMake 构建配置的管理。它们允许开发者定义和共享构建配置、工具链、变量等信息。

CMakePresets.json 定义了整个项目的配置,放置在根 CMakeLists.txt 文件同级的目录下,一般作为团队共享配置上传到版本控制软件,下面以一个例子来说明如何配置

{
    "version": 2,
    "configurePresets": [
        {
            "name": "default",
            "hidden": false,
            "generator": "Ninja",
            "binaryDir": "${sourceDir}/build",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug",
                "CMAKE_C_COMPILER": "clang",
                "CMAKE_CXX_COMPILER": "clang++",
                "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
            }
        },
        {
            "name": "vcpkg",
            "generator": "Unix Makefiles",
            "binaryDir": "${sourceDir}/build",
            "cacheVariables": {
                "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
            }
        }
    ]
}
  • configurePresets : 定义配置预设。
    • name: 预设名称。
    • generator: 指定生成器(如 Ninja、Unix Makefiles)。
    • binaryDir: 构建目录。
    • cacheVariables: 设置 CMake 缓存变量。

也就是说定义了一些配置的元信息,以及一组 configurePresets,在启动cmake时可以选择一个预设配置,使用以下命令进行配置:cmake .. --preset <preset_name> ( 假设当前在 build 目录,CMakeLists.txt.. 目录 )

cmake .. --preset default
cmake .. --preset vcpkg

需要注意的是,vscode 提供的 cmake 插件在我们没有预设配置时,会使用很多有用的配置项,如选择编译器(工具包),导出 compile_command.json 文件等,但是如果你使用自己的 configurePresets.json 预设配置,就需要设置好这些,例如defaultcacheVariables 项中设置了这些,否则如前所述,没有 compile_command.json 会导致 clangd 无法引用的某些解析头文件,此外编译器的不同可能会带来一些编译错误,尤其是对 MSVC 来说

CMakeUserPresets.json 继承自一个 CMakePresets.json 中的配置,并可以添加新的配置内容,例如下面

{
  "version": 3,
  "configurePresets": [
    {
      "name": "custom-debug",
      "inherits": "default",
      "hidden": false,
      "cacheVariables": {
        "MY_CUSTOM_OPTION": "ON"
      }
    }
  ]
}
  • inherits: 继承 CMakePresets.json 中的预设,这个例子中继承的是 default
  • cacheVariables: 添加或覆盖变量
  • hidden: false: 该预设会显示在 cmake --list-presets 的输出中,用户可以轻松查看和选择

附录:推荐阅读

Introduction · Modern CMake

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值