CMAKE学习笔记【上】

(零) 变量


一、基本概念

CMake 中的变量是 字符串类型(或列表),没有内置的数据结构如数组、字典等。变量的作用类似于 shell 脚本中的环境变量,但也有自己的作用域机制。

常见预定义变量(举例)

  • PROJECT_NAME:当前项目的名称。
  • CMAKE_CURRENT_SOURCE_DIR:当前处理的源码目录。
  • CMAKE_CURRENT_BINARY_DIR:当前构建的二进制目录。
  • CMAKE_SOURCE_DIR:项目根目录。
  • CMAKE_BINARY_DIR:构建输出根目录。
  • CMAKE_CXX_COMPILER:C++ 编译器路径。
  • CMAKE_BUILD_TYPE:构建类型(Debug/Release)。

二、变量的设置

1. 使用 set() 设置变量

cmake 复制代码
set(VAR_NAME value [CACHE TYPE DOCSTRING [FORCE]])

示例:

cmake 复制代码
set(MY_VAR "Hello CMake")
message(STATUS "MY_VAR = ${MY_VAR}")

输出:

复制代码
-- MY_VAR = Hello CMake

设置多个值(列表)

cmake 复制代码
set(MY_LIST a b c)
message(STATUS "MY_LIST = ${MY_LIST}")

输出:

复制代码
-- MY_LIST = a;b;c

如果你希望用空格分隔显示为字符串:

cmake 复制代码
message(STATUS "MY_LIST = ${MY_LIST}")
# 或者转换为空格分隔
message(STATUS "MY_LIST = ${MY_LIST}")

2. 使用 option() 设置布尔变量

用于用户可配置的开关选项,默认值为 ONOFF

cmake 复制代码
option(USE_MYMATH "Use custom math library" ON)

你可以像普通变量一样使用它:

cmake 复制代码
if(USE_MYMATH)
    message(STATUS "Using custom math")
else()
    message(STATUS "Not using custom math")
endif()

3. 使用 unset() 删除变量

cmake 复制代码
unset(MY_VAR)

可用于清除某些变量的影响。


三、变量的引用

使用 ${VAR_NAME} 来引用变量值。

cmake 复制代码
set(NAME "Alice")
message(STATUS "Hello, ${NAME}!")

输出:

复制代码
-- Hello, Alice!

如果变量未定义,则会被替换为空字符串。


四、变量的作用域(Scope)

CMake 的变量作用域不像 C/C++ 那样严格,但它有以下几种行为:

1. 局部作用域(Local Scope)

默认情况下,set() 设置的变量只在当前 CMakeLists.txt 文件中有效,不会传递到父级或子级目录。

cmake 复制代码
# CMakeLists.txt
set(MY_VAR "local")

add_subdirectory(subdir)  # 进入 subdir 目录
cmake 复制代码
# subdir/CMakeLists.txt
message(STATUS "MY_VAR = ${MY_VAR}")  # 输出空,因为不在同一个作用域

2. PARENT_SCOPE

如果你想把一个变量设置回上一级作用域(比如你在 add_subdirectory() 内部设置变量返回给上级),可以使用:

cmake 复制代码
# subdir/CMakeLists.txt
set(MY_VAR "from child" PARENT_SCOPE)

这样上级就能看到这个值了。

3. CACHE 变量(全局可见)

使用 set(... CACHE ...) 定义的变量是全局的,保存在 CMake 缓存中,可以在任何地方访问。

cmake 复制代码
set(MY_CACHE_VAR "global value" CACHE STRING "Description of this variable")

这种变量通常用于跨目录通信或配置持久化(例如用户通过 -DMY_VAR=value 传参)。

你也可以使用 cache 类型来覆盖缓存值:

bash 复制代码
cmake .. -DMY_CACHE_VAR="new value"

五、常见问题与技巧

1. 如何查看所有变量?

你可以运行:

bash 复制代码
cmake --build . --target help

或者更直接地:

bash 复制代码
cmake -LAH ..

这会列出所有的缓存变量及其值。


2. 判断变量是否存在或是否为空

cmake 复制代码
if(DEFINED VAR_NAME)
    message(STATUS "${VAR_NAME} is defined.")
endif()

if(NOT VAR_NAME)
    message(STATUS "${VAR_NAME} is empty or not set.")
endif()

3. 拼接字符串或路径

cmake 复制代码
set(PATH1 "/usr/local")
set(PATH2 "bin")
set(FULL_PATH "${PATH1}/${PATH2}")
message(STATUS "Full path: ${FULL_PATH}")

输出:

复制代码
-- Full path: /usr/local/bin

推荐使用 CMAKE_CURRENT_SOURCE_DIR 等宏拼接路径,避免硬编码。


4. 多个值如何处理?

cmake 复制代码
set(SOURCES main.cpp utils.cpp helper.cpp)

add_executable(myapp ${SOURCES})

这是最常见的做法,适用于源文件列表、库名列表等。


六、最佳实践建议

场景 推荐做法
设置本地变量 set(VAR value)
设置全局变量 set(VAR value CACHE INTERNAL "")
用户配置选项 option(VAR "desc" ON/OFF)
子目录返回变量 set(VAR value PARENT_SCOPE)
拼接路径 使用 ${CMAKE_CURRENT_SOURCE_DIR}/xxx
判断变量是否存在 if(DEFINED VAR)
清理变量 unset(VAR)

实例总结

cmake 复制代码
# 设置变量
set(MY_LIB mylib)
set(SOURCES main.cpp utils.cpp)
option(BUILD_TESTS "Build unit tests" ON)

# 引用变量
message(STATUS "Building with lib: ${MY_LIB}")
add_executable(app ${SOURCES})

# 控制逻辑
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

(一)设置项目名 代码目录和构建目录

cmake 复制代码
cmake_minimum_required(VERSION 3.1)

project(MyProject
    VERSION 1.2.3
    DESCRIPTION "A library for managing non-volatile memory"
    HOMEPAGE_URL "https://example.com/libnvm"
    LANGUAGES CUDA C CXX
)

# 打印 PROJECT_SOURCE_DIR 和 PROJECT_BINARY_DIR
message(STATUS "Source directory: ${PROJECT_SOURCE_DIR}")
message(STATUS "Binary directory: ${PROJECT_BINARY_DIR}")

一、LANGUAGES

  1. C: CMake 默认会启用的语言之一,适用于C源代码项目。
  2. CXXC++: 另一个默认启用的语言,适用于C++源代码项目。
  3. CUDA : 从 CMake 3.8 版本开始正式支持 CUDA 作为一等公民,允许直接编写 CUDA 源文件(.cu)并将其集成到构建系统中。
  4. ASM: 支持汇编语言。需要注意的是,使用 ASM 需要特定的编译器支持,并且可能需要额外配置。
  5. 其他语言

二、代码目录和构建目录

变量名 含义 是否变化
PROJECT_SOURCE_DIR 顶级 CMakeLists.txt 所在目录(即源码目录) 固定
PROJECT_BINARY_DIR 你运行 cmake 的目录(即构建目录) 根据你运行命令的位置而定

假设你的项目结构如下:

复制代码
/home/user/my_project/
├── CMakeLists.txt
└── src/
    └── main.cpp

情况一:就地构建(不推荐)

bash 复制代码
cd /home/user/my_project
cmake .
  • 此时:
    • PROJECT_SOURCE_DIR = /home/user/my_project
    • PROJECT_BINARY_DIR = /home/user/my_project

⚠️ 这样会导致构建文件和源码混在一起,不利于维护。


情况二:外出构建out-of-source build( 推荐做法)

bash 复制代码
cd /home/user/my_project/
mkdir build
cd build
cmake ..
  • 此时:
    • PROJECT_SOURCE_DIR = /home/user/my_project
    • PROJECT_BINARY_DIR = /home/user/my_project/build

✅ 所有构建产物都会被放在这个目录中,保持源码目录干净整洁。

三、添加子目录到构建系统中

add_subdirectory() 是 CMake 中用于组织大型项目的非常重要的命令,它允许你将一个子目录添加到构建系统中,并在该子目录中执行其 CMakeLists.txt 文件。告诉 CMake 进入 src/ 子目录,读取并处理其中的 CMakeLists.txt 文件。 子目录中的目标(如库或可执行文件)会被构建,并被主项目所使用。如果你在子目录中使用了 add_subdirectory(),每个子目录也有自己的 CMAKE_CURRENT_BINARY_DIR,但 PROJECT_BINARY_DIR 始终指向顶层构建目录。 子目录中也可以继续调用 add_subdirectory() 添加更深层的模块。

cmake 复制代码
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
  • source_dir: 必须参数,相对于当前 CMakeLists.txt 的源代码子目录路径。
  • binary_dir: 可选参数,指定子目录构建输出的位置(默认是与源码同路径构建)。
  • EXCLUDE_FROM_ALL: 可选参数,表示子目录中的目标不会被默认构建(除非被其他目标依赖)。

使用示例

目录结构

复制代码
my_project/
├── CMakeLists.txt           # 主 CMakeLists.txt
├── main.cpp
└── src/
    ├── CMakeLists.txt       # 子模块
    └── mylib.cpp

src/CMakeLists.txt 示例

cmake 复制代码
# 构建一个静态库 libmylib.a
add_library(mylib STATIC mylib.cpp)

# 导出头文件目录
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

CMakeLists.txt 示例

cmake 复制代码
cmake_minimum_required(VERSION 3.14)
project(MyProject LANGUAGES CXX)

# 添加子目录 src
add_subdirectory(src)

# 添加主程序
add_executable(my_app main.cpp)

# 链接子目录中生成的库
target_link_libraries(my_app PRIVATE mylib)

关键变量说明

在子目录中可以使用的变量:

变量 含义
CMAKE_CURRENT_SOURCE_DIR 当前 CMakeLists.txt 所在的源码目录
CMAKE_CURRENT_BINARY_DIR 对应的构建目录(即生成中间文件的地方)
PROJECT_SOURCE_DIR 顶层项目的源码目录(不变)
PROJECT_BINARY_DIR 顶层项目的构建目录(不变)

指定独立构建目录

你可以为子目录指定一个单独的构建路径:

cmake 复制代码
add_subdirectory(src build_src)

这会把 src/ 目录下的构建产物放到 build_src/ 路径下,而不是直接放在 src/ 中。

⚠️ 注意:这个功能通常用于外部项目集成,比如结合 ExternalProject_Add 使用。

(二)定义可执行文件

手动添加源文件

add_executable() 是 CMake 中用于定义一个可执行文件目标的命令。它告诉 CMake 你想要从一组源代码文件构建出一个可执行程序,并允许你在后续步骤中为这个目标添加属性,比如链接库、编译选项等。

  • 定义目标:指定要生成的可执行文件名称以及由哪些源文件构成。
  • 组织构建:将相关的源文件组合成一个可执行文件,便于管理和构建。
cmake 复制代码
add_executable(target_name source1 [source2 ...])
  • target_name: 可执行文件的目标名称(即输出的可执行文件名)。
  • source1, source2, ...: 构建该可执行文件所需的源代码文件列表。

非递归自动查找源文件

cmake 复制代码
# 作用:查找当前目录(. 表示当前目录)下所有的源文件(默认识别 .c, .cxx, .cpp, .cc 等),并将这些文件路径保存到变量 DIR_NAME 中。
# 不会递归子目录:只查找当前目录,不进入子目录。
# 常用于简单项目:适合小型项目或演示项目,快速将当前目录下所有源文件加入构建系统。
aux_source_directory(. DIR_NAME)
add_executable(target_name ${DIR_NAME})

递归查找源文件

file(GLOB_RECURSE ...) 不会自动检测新增的文件,在 IDE 中(如 CLion、Qt Creator),可能需要重新运行 CMake 才能识别新加入的文件。

cmake 复制代码
# 递归查找 src 目录下所有 .cpp 文件(包括子目录)这里的 GLOB_RECURSE 表示递归搜索。
file(GLOB_RECURSE SRC_LIST
    "${PROJECT_SOURCE_DIR}/*.cpp"
    "${PROJECT_SOURCE_DIR}/*.cc"
    "${PROJECT_SOURCE_DIR}/*.cxx"
)

# 添加可执行程序
add_executable(myapp ${SRC_LIST})

示例

假设有一个简单的项目结构如下:

复制代码
my_project/
├── CMakeLists.txt
└── main.cpp

其中 main.cpp 包含了项目的入口代码。

CMakeLists.txt 示例

cmake 复制代码
cmake_minimum_required(VERSION 3.10)
project(MyExecutable LANGUAGES CXX)

# 添加可执行文件目标
add_executable(my_app main.cpp)

在这个例子中,my_app 将是最终生成的可执行文件的名字,而 main.cpp 则是它的源文件。

使用场景与扩展

1. 多个源文件

如果你的项目包含多个源文件,可以像下面这样列出所有源文件:

cmake 复制代码
add_executable(my_app main.cpp utils.cpp network.cpp)

或者,使用通配符简化输入(虽然不推荐,因为这可能会导致不必要的重新构建):

cmake 复制代码
file(GLOB SOURCES "src/*.cpp")
add_executable(my_app ${SOURCES})

2. 链接外部库

你可以通过 target_link_libraries() 来链接你的可执行文件到其他库:

cmake 复制代码
find_package(SomeLibrary REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE Some::Library)

3. 设置编译选项

你可以通过 target_compile_options() 或者 target_compile_definitions() 等命令来设置特定于目标的编译器选项或宏定义:

cmake 复制代码
add_executable(my_app main.cpp)
target_compile_options(my_app PRIVATE -Wall -Wextra)
target_compile_definitions(my_app PRIVATE DEBUG=1)

4. 设置包含目录

为了包含头文件路径,可以使用 target_include_directories()

cmake 复制代码
add_executable(my_app main.cpp)
target_include_directories(my_app PRIVATE ${PROJECT_SOURCE_DIR}/include)

注意事项

  • 目标名称唯一性:在一个 CMake 项目中,所有的目标名称必须是唯一的。
  • 避免重复定义:不要在同一个 CMakeLists.txt 文件或不同的子目录中重复定义相同的目标名称。
  • 现代 CMake 实践 :尽量使用目标级别的命令(如 target_link_libraries, target_include_directories)而不是全局命令(如 include_directories, link_libraries),以提高封装性和减少冲突的可能性。

(三)头文件搜索路径和指定链接库

一、目标级命令 vs 全局作用域命令

目标级命令 全局作用域命令
target_link_libraries(...) link_libraries(...), link_directories(...)
target_include_directories(...) include_directories(...)
target_compile_options(...) add_compile_options(...)
target_compile_definitions(...) add_definitions(...)

二、逐个讲解每个命令的作用和用法

1. target_include_directories(...)

作用 & 示例

为某个目标添加头文件搜索路径。在哪里搜索头文件。

cmake 复制代码
target_include_directories(myapp PRIVATE ${PROJECT_SOURCE_DIR}/include)
  • PRIVATE: 表示这些头文件路径只对 myapp 生效。
  • 支持 PUBLICINTERFACE 模式,用于控制是否将路径暴露给依赖者。

旧式写法(不推荐):

cmake 复制代码
include_directories(${PROJECT_SOURCE_DIR}/include)
add_executable(myapp main.cpp)

⚠️ 缺点:include_directories(...) 是全局生效的,会影响所有后续定义的目标,即使它们并不需要这些路径。

作用 & 示例

指定某个目标(如可执行文件或库)在链接阶段需要使用的其他库。

cmake 复制代码
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE Some::Library)
  • PRIVATE: 表示这个依赖只用于 myapp,不会传递给使用 myapp 的目标。
  • Some::Library: 是一个"导入目标"(imported target),通常通过 find_package() 获得。

旧式写法(不推荐):

cmake 复制代码
link_directories(/usr/local/lib)
link_libraries(Some::Library)
add_executable(myapp main.cpp)

⚠️ 缺点:link_directorieslink_libraries 是全局生效的,影响所有后续目标,容易造成混乱。

3. target_compile_options(...)

作用 & 示例

为某个目标设置编译器选项(如 -Wall, -O3, /W4 等)。

cmake 复制代码
target_compile_options(myapp PRIVATE -Wall -Wextra -O3)
  • 只对 myapp 这个目标生效。
  • 支持跨平台编译器选项判断:
cmake 复制代码
target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:--pedantic>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)

旧式写法(不推荐):

cmake 复制代码
add_compile_options(-Wall -Wextra)
add_executable(myapp main.cpp)

⚠️ 缺点:add_compile_options(...) 是全局生效的,影响所有目标。

4. target_compile_definitions(...)

作用 & 示例

为某个目标定义宏定义(预处理器宏),相当于 -DNAME=VALUE

cmake 复制代码
target_compile_definitions(myapp PRIVATE DEBUG=1 USE_FEATURE_X)
  • 仅对 myapp 生效。
  • 不会污染其他目标。

旧式写法(不推荐):

cmake 复制代码
add_definitions(-DDEBUG=1 -DUSE_FEATURE_X)
add_executable(myapp main.cpp)

⚠️ 缺点:add_definitions(...) 是全局生效的,影响所有目标。

三、进阶技巧:使用 INTERFACE 库封装接口

你可以创建一个纯接口的库,把公共配置集中起来:

cmake 复制代码
add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE ${PROJECT_SOURCE_DIR}/include)
target_compile_definitions(mylib INTERFACE DEBUG=1)
target_link_libraries(mylib INTERFACE Some::Library)

# 使用该接口库
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)

(四)动态库和静态库

在 CMake 中,你可以通过 add_library() 命令来创建静态库或动态库(共享库)。这两个类型的库在编译、链接和最终部署时有不同的行为和用途。

  • 静态库(Static Library)

    • 文件扩展名通常为 .a(Unix/Linux)或 .lib(Windows)。
    • 在编译时,静态库的代码会被直接复制到生成的可执行文件中。
    • 优点:部署简单,因为所有依赖都已包含在可执行文件内;启动速度快,因为没有额外的加载过程。
    • 缺点:可能导致可执行文件体积较大;如果多个程序使用同一个静态库,则每个程序都会携带一份副本,浪费磁盘空间和内存。
  • 动态库(Shared Library)

    • 文件扩展名为 .so(Unix/Linux)、.dylib(macOS)或 .dll(Windows)。
    • 在编译时仅记录对库的引用,在运行时才真正加载这些库。
    • 优点:多个程序可以共享同一份库的副本,节省磁盘空间和内存;便于更新,无需重新编译依赖它的程序。
    • 缺点:需要确保在运行时能够找到并加载相应的库文件;可能会导致启动时间增加,因为需要加载外部库。

创建静态库和动态库

在 CMake 中,使用 add_library() 来定义库,并通过指定库类型来决定是创建静态库还是动态库。如果没有指定 STATICSHARED,CMake 将根据平台默认设置选择一种类型。但是为了明确性和控制力,建议总是显式地指定库类型。

创建静态库STATIC

cmake 复制代码
add_library(my_static_lib STATIC source1.cpp source2.cpp)
  • STATIC: 表示创建一个静态库。

创建动态库SHARED

cmake 复制代码
add_library(my_shared_lib SHARED source1.cpp source2.cpp)
  • SHARED: 表示创建一个动态库。

使用库

无论是静态库还是动态库,都可以通过 target_link_libraries() 来将它们链接到你的可执行文件或其他库中。

示例

假设你有一个项目结构如下:

复制代码
my_project/
├── CMakeLists.txt
└── src/
    ├── main.cpp
    └── mylib.cpp
    └── mylib.h

CMakeLists.txt

cmake 复制代码
cmake_minimum_required(VERSION 3.10)
project(MyProject LANGUAGES CXX)

# 添加子目录 src
add_subdirectory(src)

# 添加主程序
add_executable(main_app main.cpp)

# 链接库
target_link_libraries(main_app PRIVATE mylib) # 假设 mylib 是上面添加的库

src/CMakeLists.txt

cmake 复制代码
# 创建静态库
add_library(my_static_lib STATIC mylib.cpp)

# 创建动态库
add_library(my_shared_lib SHARED mylib.cpp)

# 导出静态库头文件目录
target_include_directories(my_static_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# 导出动态库头文件目录
target_include_directories(my_shared_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

# 在 Windows 上构建 .dll 动态库时,默认情况下,CMake 不会自动导出任何符号(与 Linux/macOS 上的 .so/.dylib 行为不同)。你需要明确告诉编译器哪些符号应该被导出,供外部程序调用。Linux、macOS 上不需要加这一行代码
set_target_properties(my_shared_lib PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE)

# 安装库文件
install(TARGETS my_static_lib my_shared_lib
        ARCHIVE DESTINATION lib
        LIBRARY DESTINATION lib
        RUNTIME DESTINATION bin
        INCLUDES DESTINATION include)

# 安装头文件(可选,如果 PUBLIC 没有包含全部头文件)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/mylib.h
        DESTINATION include)

# 可选:导出配置文件以支持 find_package()
install(EXPORT MyLibTargets
        FILE MyLibTargets.cmake
        NAMESPACE MyLib::
        DESTINATION lib/cmake/MyLib)

特别注意事项

  • Windows 上的 DLLs:在 Windows 平台上,当创建动态库时,通常还需要提供一个"导入库"(.lib 文件),这个文件包含了对外部函数的引用信息。CMake 会自动生成这个导入库。

  • 版本号与 ABI 兼容性 :对于共享库,特别是在 Linux 系统上,考虑给库加上版本号以管理 ABI(应用程序二进制接口)兼容性问题。可以通过 set_target_properties() 设置属性来实现这一点。

  • 安装规则:如果你计划分发你的库,可能需要定义安装规则,以便于正确地安装头文件和库文件到系统目录。

例如:

cmake 复制代码
install(TARGETS my_shared_lib DESTINATION lib)
install(FILES mylib.h DESTINATION include)

这将把你的共享库安装到系统的 lib 目录下,头文件安装到 include 目录下。

(五)查找和加载外部依赖库

find_package() 是 CMake 中用于 查找和加载外部依赖库(第三方库或系统库) 的核心命令之一。它帮助你自动检测系统中是否安装了某个库,并设置好相应的变量(如头文件路径、库路径、版本号等),以便后续使用这些库进行编译和链接。

一、find_package 的作用

简单来说,它的主要功能是:

  • 查找指定的库是否存在于系统中
  • 设置相关变量:
    • <PackageName>_FOUND: 是否找到该库
    • <PackageName>_INCLUDE_DIRS<PackageName>_INCLUDE_DIR: 头文件路径
    • <PackageName>_LIBRARIES<PackageName>_LIBRARY: 库文件路径
    • <PackageName>_VERSION: 版本信息(如果支持)
  • 自动导入目标(现代用法):可以直接 target_link_libraries(my_target PRIVATE Some::Library)

二、基本语法

cmake 复制代码
find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
             [REQUIRED] [[COMPONENTS] [components...]]
             [OPTIONAL_COMPONENTS components...]
             [NO_POLICY_SCOPE])

参数说明:

参数 含义
<PackageName> 要查找的包名,比如 Threads, OpenCV, CUDA, Protobuf
[version] 可选,指定最低版本要求
EXACT 可选,要求精确匹配版本
QUIET 可选,禁止输出错误信息
MODULE 可选,强制使用模块模式(Module mode)而不是配置模式(Config mode)
REQUIRED 可选,如果找不到包则报错并停止构建
COMPONENTS 可选,指定需要查找的子组件(适用于有多个组件的库,如 Qt5Core, Qt5Gui 等)

三、典型使用示例

示例 1:查找线程库(Threads)

cmake 复制代码
find_package(Threads REQUIRED)
target_link_libraries(my_target PRIVATE Threads::Threads)
  • find_package(Threads REQUIRED):查找线程库
  • Threads::Threads:现代 CMake 推荐的导入方式,表示找到的线程库目标

示例 2:查找 CUDA

cmake 复制代码
find_package(CUDA 11.7 REQUIRED)
if (CUDA_FOUND)
    message(STATUS "Found CUDA ${CUDA_VERSION}")
endif()

示例 3:查找 OpenCV 并链接

cmake 复制代码
find_package(OpenCV REQUIRED COMPONENTS core imgproc highgui)
include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE ${OpenCV_LIBS})

示例 4:查找可选库(非必须)

cmake 复制代码
find_package(ZLIB QUIET)
if (ZLIB_FOUND)
    target_compile_definitions(my_target PRIVATE HAVE_ZLIB)
    target_link_libraries(my_target PRIVATE ZLIB::ZLIB)
else()
    message(WARNING "ZLIB not found, some features will be disabled.")
endif()

四、两种查找模式

CMake 支持两种查找机制:

1. Module Mode(模块模式)

  • 使用内置的 Find<PackageName>.cmake 文件来查找库
  • 通常用于标准库或常见第三方库(如 Threads、OpenGL、SDL 等)
  • 示例:FindOpenCV.cmake

2. Config Mode(配置模式)

  • 使用库自带的 <PackageName>Config.cmake 文件
  • 更现代、更灵活的方式,推荐优先使用
  • 一般在你安装了某个库之后会自动生成这些文件(如通过 make installvcpkg, conan 安装)

你可以通过环境变量 CMAKE_PREFIX_PATH 来告诉 CMake 去哪些路径下查找这些配置文件。

五、如何查看 find_package 找到了什么?

你可以在 CMakeLists.txt 中加入以下代码来打印相关信息:

cmake 复制代码
message(STATUS "<PackageName>_FOUND: ${PackageName}_FOUND")
message(STATUS "<PackageName>_INCLUDE_DIRS: ${PackageName}_INCLUDE_DIRS")
message(STATUS "<PackageName>_LIBRARIES: ${PackageName}_LIBRARIES")
message(STATUS "<PackageName>_VERSION: ${PackageName}_VERSION")

或者在运行 cmake 时加上 --debug-find(某些版本支持)查看详细查找过程。

六、一些常见的可以 find_package 的库

包名 用途
Threads 多线程支持
CUDA NVIDIA GPU 编程支持
OpenMP OpenMP 并行编程支持
OpenCV 计算机视觉库
Eigen3 C++ 矩阵运算库
Boost C++ 扩展库
Python3 Python 支持
PkgConfig 使用 pkg-config 查找其他库
Doxygen 文档生成工具
Git Git 工具支持

(六)配置文件configure_file

configure_file 是 CMake 中的一个命令,用于在构建过程的配置阶段生成新的文件或更新现有文件。这个命令通常用来处理模板文件,比如将一些项目配置或者版本信息嵌入到源代码中,或者根据构建环境的不同生成不同的配置文件。*.in 模板文件一般是用于在 cmake 执行阶段(准确来说,应该是配置阶段)将其嵌入的 CMake 变量、列表展开,并生成目标文件的过程中的。这一步通常是由 configure_file() 完成,会在【06】变量参与 C++ 的编译 中进行介绍。*.in 的后缀是约定俗成的,当然也可以使用其他形式的后缀。

基本用法

cmake 复制代码
configure_file(<input> <output>
               [NO_SOURCE_PERMISSIONS | FILE_PERMISSIONS <permissions>...]
               [COPYONLY] [ESCAPE_QUOTES] [@ONLY])
  • <input>: 模板文件的位置。可以使用相对路径或绝对路径。
  • <input> 文件中可以包含 CMake 变量,它们会在执行 configure_file 时被实际值替换。
  • <output>: 目标文件的位置和名称。如果目标文件已经存在,则会被覆盖。

示例

假设你有一个名为 config.h.in 的输入文件,内容如下:

cpp 复制代码
#define PROJECT_SOURCE_DIR "@CMAKE_SOURCE_DIR@"
#define PROJECT_VERSION "@PROJECT_VERSION@"

然后,在你的 CMakeLists.txt 文件中,你可以这样使用 configure_file

cmake 复制代码
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0)

configure_file(
  ${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
  ${CMAKE_CURRENT_BINARY_DIR}/config.h
)

在这个例子中,@CMAKE_SOURCE_DIR@@PROJECT_VERSION@ 这样的标记会被对应的 CMake 变量值所替换,并生成一个新的 config.h 文件到构建目录下

参数解释

  • NO_SOURCE_PERMISSIONS: 不复制原始文件的权限设置给输出文件,默认情况下会复制。
  • FILE_PERMISSIONS: 设置输出文件的权限。
  • COPYONLY: 如果指定了此选项,configure_file 将不会替换任何变量,只会简单地复制文件。
  • ESCAPE_QUOTES: 转义输出中的引号(对于某些特定情况可能需要)。
  • @ONLY: 仅替换以 @ 开头和结尾的变量,忽略 ${} 格式的变量引用。

configure_file 是一个非常有用的工具,尤其是在你需要基于构建环境动态生成配置文件的时候。通过它,你可以轻松实现从简单的文本替换到复杂的配置文件生成等多种任务。

举例说明1:在代码中使用

背景

假设你正在开发一个C++项目,并希望在编译时根据项目的设置动态生成配置头文件(如 config.h),以便于在代码中访问一些构建时确定的信息,比如项目版本号、源码目录等。

步骤 1: 创建模板文件

首先,在你的项目目录下创建一个名为 config.h.in 的模板文件。这个文件将包含一些占位符,这些占位符会在构建时被实际值替换。

例如,config.h.in 文件内容如下:

cpp 复制代码
#pragma once

#define PROJECT_SOURCE_DIR "@CMAKE_SOURCE_DIR@"
#define PROJECT_VERSION "@PROJECT_VERSION@"

这里的 @CMAKE_SOURCE_DIR@@PROJECT_VERSION@ 是占位符,它们会被 CMake 中定义的实际值替换。

步骤 2: 修改 CMakeLists.txt

接下来,你需要修改或者创建 CMakeLists.txt 文件,告诉 CMake 在构建过程中如何处理这个模板文件。

cmake 复制代码
cmake_minimum_required(VERSION 3.10)

# 定义项目名称和版本
project(MyProject VERSION 1.0)

# 使用 configure_file 命令处理模板文件
configure_file(
  ${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
  ${CMAKE_CURRENT_BINARY_DIR}/config.h
)

这里我们使用了 configure_file 命令来指定输入文件(config.h.in)和输出文件(config.h)。${CMAKE_CURRENT_SOURCE_DIR}${CMAKE_CURRENT_BINARY_DIR} 分别表示源代码目录和构建目录。

步骤 3: 构建项目

现在,当你运行 CMake 来构建项目时,它会自动处理 config.h.in 文件,并生成一个新的 config.h 文件到构建目录下。

例如,如果你的源代码目录结构如下:

复制代码
MyProject/
├── CMakeLists.txt
└── config.h.in

并且你在一个单独的构建目录中执行 CMake:

bash 复制代码
mkdir build
cd build
cmake ..

CMake 将会在 build/ 目录下生成 config.h 文件,其内容类似于:

cpp 复制代码
#pragma once

#define PROJECT_SOURCE_DIR "/path/to/your/source/directory"
#define PROJECT_VERSION "1.0"

这样,你就可以在代码中包含这个自动生成的 config.h 文件,并访问其中定义的宏了:

cpp 复制代码
#include "config.h"

void printConfig() {
    std::cout << "Source directory: " << PROJECT_SOURCE_DIR << "\n";
    std::cout << "Version: " << PROJECT_VERSION << "\n";
}

通过这种方式,你可以轻松地在构建时生成包含特定信息的配置文件,这在大型项目中特别有用。

举例说明2:控制子目录编译和链接库设置

config.h.in 文件中使用 #cmakedefine USE_MYMATH,然后根据 CMake 中是否定义了某个变量(例如 USE_MYMATH),来决定生成的 config.h 文件中是否启用这个宏定义。


场景说明

你想控制是否启用某个功能模块(比如自定义数学库 mymath),通过 CMake 配置时决定是否定义 USE_MYMATH 宏,并在源代码中通过预处理指令判断:

1. 创建 config.h.in 文件

内容如下:

cpp 复制代码
#pragma once

#cmakedefine USE_MYMATH

⚠️ 注意:

  • #cmakedefine 是 CMake 的特殊语法。
  • 如果 CMake 中设置了 USE_MYMATH 变量(为 ONTRUE 或非空值),则会在生成的 config.h 中变成 #define USE_MYMATH
  • 否则,这行会被注释掉或直接省略。

2. 修改 CMakeLists.txt

假设你有一个选项让用户选择是否启用 USE_MYMATH

cmake 复制代码
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0)

# 定义 USE_MYMATH 开关,默认为 ON
option(USE_MYMATH "Use the custom math library" ON)

# 配置 config.h 头文件
configure_file(
  ${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
  ${CMAKE_CURRENT_BINARY_DIR}/config.h
)

# 设置包含路径(如果需要让源码中 #include "config.h")
include_directories(${CMAKE_CURRENT_BINARY_DIR})

# 如果启用了 USE_MYMATH,则处理 math 子模块
if(USE_MYMATH)
    include_directories("${PROJECT_SOURCE_DIR}/math")
    add_subdirectory(math)
    set(EXTRA_LIBS mathfun)
else()
    unset(EXTRA_LIBS) # 可选:避免变量污染
endif()

# 添加主程序
add_executable(myapp main.cpp)

# 链接额外库(如果有)
if(DEFINED EXTRA_LIBS)
    target_link_libraries(myapp PRIVATE ${EXTRA_LIBS})
endif()
  • option() 创建了一个可配置的开关,默认是 ON
  • configure_file() 根据这个变量是否存在,来决定是否在 config.h 中写入 #define USE_MYMATH

3. 在代码中使用这个宏

比如在 main.cpp 中:

cpp 复制代码
#include "config.h"

#include <iostream>

int main() {
#ifdef USE_MYMATH
    std::cout << "Using my custom math library." << std::endl;
#else
    std::cout << "Using standard math functions." << std::endl;
#endif
    return 0;
}

4. 构建项目并测试

默认构建(USE_MYMATH=ON)

bash 复制代码
mkdir build
cd build
cmake ..
make
./myapp

输出:

复制代码
Using my custom math library.

关闭 USE_MYMATH

bash 复制代码
cmake .. -DUSE_MYMATH=OFF
make
./myapp

输出:

复制代码
Using standard math functions.

查看生成的 config.h 内容

你可以查看构建目录下的 config.h 文件内容:

  • 如果启用了 USE_MYMATH,内容是:
cpp 复制代码
#pragma once

#define USE_MYMATH
  • 如果没有启用,则是:
cpp 复制代码
#pragma once

/* #undef USE_MYMATH */

或者干脆不出现这一行(取决于 CMake 版本)。