CMake指令: include、include_guard、include_directories、target_include_directories

发布于:2025-06-13 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

1.include

1.1.简介

1.2.核心作用

1.3.工作流程

1.4.常见使用场景

1.5.与 add_subdirectory 的区别

1.6.注意事项

2.include_guard

2.1.简介

2.2.核心作用

2.3.工作原理

2.4.常见使用场景

2.5.include_guard() 优势

2.6.注意事项

3.include_directories

3.1.简介

3.2.常见使用场景

3.3.注意事项

4.target_include_directories

4.1.简介

4.2.核心作用

4.3.常见使用场景

4.4.与其他命令的对比

4.5.注意事项

5.总结

相关链接


1.include

1.1.简介

        在 CMake 中,include() 是一个用于导入外部 CMake 脚本或模块的命令,类似于编程语言中的 import 或 #include

        基本语法:

include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>] [NO_POLICY_SCOPE])
  • <file|module>:要导入的文件路径或模块名。
    • 若为文件路径(如 cmake/Utils.cmake),直接加载该文件。
    • 若为模块名(如 FetchContent),在 CMake 模块路径中查找并加载。
  • OPTIONAL:可选参数,若文件不存在则忽略错误。
  • RESULT_VARIABLE <var>:将加载结果存储到变量中(成功为 TRUE,失败为 FALSE)。
  • NO_POLICY_SCOPE:不创建新的策略作用域(CMake 3.20+)。

        如果include的是模块名,该命令会依次将 CAMAKE_MODULE_PATH 列表变量中的路径作为父目录来搜索<CMake>模块,若任未能搜索到,则从CMake预置模块目录搜索指定的<CMake模块>。

        引用的外部程序与该命令所在的调用上下文共享相同的作用域,也就是说,引用的程序内创建的变量,在include命令之后仍然存在。

1.2.核心作用

  1. 模块化 CMake 代码:将通用逻辑(如工具链配置、依赖管理)封装到独立文件中,避免重复代码。
  2. 导入标准模块:加载 CMake 内置模块(如 ExternalProjectCPack),扩展功能。
  3. 跨平台工具链:通过导入工具链文件(如 arm-gcc-toolchain.cmake),支持交叉编译。

1.3.工作流程

1.路径搜索:

  • 若参数是绝对路径(如 /path/to/script.cmake),直接加载该文件。
  • 若为相对路径(如 cmake/Utils.cmake),在当前目录及其父目录中查找。
  • 若为模块名(如 FindOpenCV),在 CMAKE_MODULE_PATH 中查找(通常位于 $CMAKE_ROOT/Modules)。

2.脚本执行

  • 被导入的脚本在当前 CMake 作用域中执行,定义的变量、函数、宏等直接生效。
  • 若使用 NO_POLICY_SCOPE,则不创建新的策略作用域(避免策略版本冲突)。

3.变量继承

  • 导入脚本可访问当前作用域的变量,也可通过 set(... PARENT_SCOPE) 修改父作用域变量。

1.4.常见使用场景

1.导入自定义 CMake 脚本

项目结构

project/
├─ CMakeLists.txt
└─ cmake/
   └─ Utils.cmake    # 自定义工具函数

Utils.cmake

# 定义通用函数
# Turn on warnings on the given target
function(enable_warnings target)
    if(SPDLOG_BUILD_WARNINGS)
        if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
            list(APPEND MSVC_OPTIONS "/W3")
            if(MSVC_VERSION GREATER 1900) # Allow non fatal security warnings for msvc 2015
                list(APPEND MSVC_OPTIONS "/WX")
            endif()
        endif()

        target_compile_options(
            ${target_name}
            PRIVATE $<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>,$<CXX_COMPILER_ID:GNU>>:
                    -Wall
                    -Wextra
                    -Wconversion
                    -pedantic
                    -Werror
                    -Wfatal-errors>
                    $<$<CXX_COMPILER_ID:MSVC>:${MSVC_OPTIONS}>)
    endif()
endfunction()

CMakeLists.txt

# 导入自定义脚本
include(cmake/Utils.cmake)

# 使用脚本中定义的函数
add_executable(my_app src/main.cpp)
enable_warnings(my_app)  # 应用警告选项

2.导入 CMake 标准模块

# 导入 FetchContent 模块(用于依赖管理)
include(FetchContent)

# 使用模块功能
FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 10.1.1
)
FetchContent_MakeAvailable(fmt)

# 链接依赖
target_link_libraries(my_app PRIVATE fmt::fmt)

3.导入工具链文件(交叉编译)

# 项目根目录 CMakeLists.txt
if(CROSS_COMPILE)
    include(cmake/arm-gcc-toolchain.cmake)  # 导入工具链
endif()

arm-gcc-toolchain.cmake

# 配置交叉编译工具链
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

1.5.与 add_subdirectory 的区别

特性 include() add_subdirectory()
作用域 共享当前作用域 创建新的子作用域
文件类型 纯 CMake 脚本 完整的子项目
构建目标 不生成目标 可定义目标(如 add_library
典型场景 导入工具函数、模块 管理子项目、源代码目录

1.6.注意事项

1.脚本组织

将通用逻辑封装到 cmake/ 目录下的脚本中:

project/
├─ CMakeLists.txt
└─ cmake/
   ├─ CompilerOptions.cmake  # 编译选项
   ├─ CodeCoverage.cmake     # 代码覆盖率
   └─ FindMyLib.cmake        # 自定义 Find 模块

2.模块导入

使用 list(APPEND CMAKE_MODULE_PATH ...) 添加自定义模块路径:

list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
include(FindMyLib)  # 从 cmake/ 目录加载自定义模块

3.条件导入

根据平台或选项选择性导入脚本:

if(WIN32)
    include(cmake/WindowsUtils.cmake)
else()
    include(cmake/UnixUtils.cmake)
endif()

4.变量作用域

  • 导入脚本中的变量默认在当前作用域生效,可能覆盖已有变量。
  • 建议使用函数封装逻辑,避免全局变量污染:
# 推荐写法
function(my_function arg)
    # 函数内的变量是局部的
    set(result "${arg}_processed" PARENT_SCOPE)
endfunction()

5.循环导入

  • 避免脚本之间的循环导入(如 A.cmake 导入 B.cmake,而 B.cmake 又导入 A.cmake),会导致无限递归。

6.模块版本兼容性

某些模块(如 CPack)依赖 CMake 版本,需确保版本兼容:

cmake_minimum_required(VERSION 3.16)  # 确保支持 FetchContent
include(FetchContent)

2.include_guard

2.1.简介

   include_guard() 是 CMake 3.10 引入的一个命令,用于防止 CMake 脚本被重复包含,类似于 C/C++ 头文件中的 #pragma once 或头文件保护宏(#ifndef ... #define ... #endif)。

        基本语法:

include_guard([GLOBAL|DIRECTORY])
  • GLOBAL:在全局范围内检查重复包含(默认行为)。
  • DIRECTORY:仅在当前目录及其子目录的 CMake 脚本中检查重复包含。

2.2.核心作用

  1. 防止重复定义:避免在多次包含同一脚本时,重复定义函数、宏或设置变量。
  2. 提高性能:减少不必要的脚本执行,加速 CMake 配置过程。
  3. 避免循环依赖:防止脚本间的循环包含(如 A.cmake 包含 B.cmake,而 B.cmake 又包含 A.cmake)。

2.3.工作原理

1.唯一标识符生成

  • include_guard() 会根据当前脚本的绝对路径生成一个唯一标识符(如 CMAKE_INCLUDE_GUARD_/path/to/script.cmake)。
  • 该标识符存储在 CMake 的缓存变量中(CMAKE_INCLUDE_GUARD_*)。

2.重复检查

  • 每次执行 include_guard() 时,检查该标识符是否已存在。
  • 若存在,则立即返回,跳过脚本剩余部分;否则继续执行并标记为已包含。

3.作用域选项

  • GLOBAL:在整个 CMake 项目中检查重复(默认)。
  • DIRECTORY:仅在当前目录及其子目录的 CMake 脚本中检查重复(适用于模块化项目)。

2.4.常见使用场景

1.通用工具函数脚本

cmake/Utils.cmake

include_guard()  # 防止重复包含

# 定义函数
function(enable_warnings target)
    target_compile_options(${target} PRIVATE
        -Wall -Wextra -Wpedantic
    )
endfunction()

# 定义宏
macro(add_example name)
    add_executable(${name} examples/${name}.cpp)
    target_link_libraries(${name} PRIVATE my_lib)
endmacro()

CMakeLists.txt

# 多次包含同一脚本(实际项目中可能因条件分支导致)
if(DEBUG)
    include(cmake/Utils.cmake)
endif()

# 其他条件也可能包含
if(BUILD_TESTS)
    include(cmake/Utils.cmake)  # 第二次包含,被 include_guard 阻止
endif()

# 正常包含(仅执行一次)
include(cmake/Utils.cmake)

# 使用脚本中定义的函数
add_executable(my_app src/main.cpp)
enable_warnings(my_app)

2.第三方依赖模块

cmake/FindMyLib.cmake

include_guard()

# 查找库
find_library(MYLIB_LIBRARY NAMES mylib)
find_path(MYLIB_INCLUDE_DIR NAMES mylib.h)

# 设置结果
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MyLib DEFAULT_MSG
    MYLIB_LIBRARY MYLIB_INCLUDE_DIR
)

2.5.include_guard() 优势

  • 自动生成标识符:无需手动命名变量,避免命名冲突。
  • 简洁:只需一行代码,提高可读性。
  • 兼容性:在 CMake 3.10+ 版本中稳定支持。

2.6.注意事项

1.在所有共享脚本中使用

  • 所有被多个地方包含的脚本(如工具函数、Find 模块)都应添加 include_guard()

2.放在脚本顶部

确保 include_guard() 是脚本中的第一个命令,避免在检查前执行不必要的代码。

# 正确位置
include_guard()

# 脚本内容
function(my_function)
    # ...
endfunction()

3.选择合适的作用域

  • 默认 GLOBAL:适用于全局工具函数或依赖模块。
  • DIRECTORY:适用于特定目录下的模块化脚本,避免与其他目录的脚本冲突。

4.版本兼容性

  • include_guard() 仅在 CMake 3.10+ 版本中可用。若需兼容旧版本,需手动实现保护逻辑。

5.条件包含

  • 若脚本中的部分内容需要在每次包含时执行(如更新变量),需将这些内容放在 include_guard() 之后,但可能导致重复定义,需谨慎处理。

3.include_directories

3.1.简介

  include_directories 是 CMake 中用于指定头文件搜索路径的命令,允许编译器在编译时找到项目依赖的头文件。

        基本语法:

include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])
  • FTER|BEFORE:指定路径添加到搜索列表的位置(默认 AFTER)。
  • SYSTEM:将目录标记为系统目录,减少编译警告。
  • dir1 [dir2 ...]:要添加的头文件搜索路径(相对路径或绝对路径)。

3.2.常见使用场景

1.简单项目

# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyApp)

# 添加项目头文件目录
include_directories(include)

# 添加源文件
add_executable(my_app src/main.cpp)

2. 多目录项目

# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyApp)

# 添加多个头文件目录
include_directories(
    include
    src/core
    src/gui
)

# 添加源文件
add_executable(my_app src/main.cpp)

3.外部依赖

# 查找并链接 OpenCV
find_package(OpenCV REQUIRED)

# 添加 OpenCV 头文件目录(通常由 find_package 设置)
include_directories(${OpenCV_INCLUDE_DIRS})

# 链接 OpenCV 库
target_link_libraries(my_app ${OpenCV_LIBS})

3.3.注意事项

1.全局作用域

  • include_directories 设置的路径对当前 CMakeLists.txt 及所有子目录有效。
  • 替代方案:使用 target_include_directories 为特定目标设置路径:
target_include_directories(my_app PRIVATE include)

2.相对路径与绝对路径

  • 相对路径基于当前 CMakeLists.txt 的位置。
  • 建议使用绝对路径(如 ${CMAKE_SOURCE_DIR}/include)提高可移植性。

3.路径顺序

  • 多个目录按添加顺序搜索,先找到的头文件优先使用。
  • 使用 BEFORE/AFTER 控制路径优先级:
include_directories(BEFORE src/override)  # 优先搜索

4.优先使用 target_include_directories

  • 提高代码隔离性,避免不同目标的路径冲突。
  • 支持 PRIVATE/PUBLIC/INTERFACE 作用域控制。

5.区分项目内与第三方路径

# 项目内头文件
target_include_directories(my_app PRIVATE include)

# 第三方库头文件(标记为 SYSTEM 减少警告)
target_include_directories(my_app SYSTEM PRIVATE third_party/include)

4.target_include_directories

4.1.简介

   target_include_directories 是 CMake 中用于精确控制目标(如可执行文件、库)头文件搜索路径的命令,相比全局的 include_directories,它提供了更细粒度的作用域控制。

        基本语法:

target_include_directories(
    <target>  # 目标名称(如 add_executable 或 add_library 创建的)
    [SYSTEM]  # 将目录标记为系统目录(减少编译警告)
    [BEFORE]  # 优先搜索这些目录(而非追加到末尾)
    <INTERFACE|PUBLIC|PRIVATE>  # 作用域
    [items1...]
    [<INTERFACE|PUBLIC|PRIVATE> [items2...]] ...
)
  • 作用域选项
    • PRIVATE:仅目标自身使用这些路径。
    • PUBLIC:目标和依赖该目标的其他目标都使用这些路径。
    • INTERFACE:仅依赖该目标的其他目标使用这些路径(目标自身不使用)。

4.2.核心作用

1.模块化头文件路径

  • 为特定目标设置头文件搜索路径,避免全局污染。
  • 例如:库 libA 的公开头文件路径对依赖它的项目可见,私有头文件仅内部使用。

2.构建依赖传递

  • 通过 PUBLIC/INTERFACE 作用域,自动将头文件路径传递给依赖链上的其他目标。
  • 例如:app → libB → libA,若 libB 以 PUBLIC 方式链接 libA,则 app 也能找到 libA 的头文件。

3.系统目录标记

  • 使用 SYSTEM 标记第三方库目录,减少编译器警告(如 -isystem 选项)。

4.3.常见使用场景

1.库项目的公开 / 私有头文件

项目结构

mylib/
├─ include/          # 公开头文件(供外部使用)
│  └─ mylib/api.h
├─ src/              # 源代码和私有头文件
│  ├─ impl.cpp
│  └─ detail/impl.h  # 私有头文件(不暴露给外部)
└─ CMakeLists.txt

CMakeLists.txt

add_library(mylib STATIC src/impl.cpp)

# 公开头文件路径(供依赖 mylib 的目标使用)
target_include_directories(mylib
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>  # 构建时
        $<INSTALL_INTERFACE:include>                            # 安装后
)

# 私有头文件路径(仅 mylib 自身使用)
target_include_directories(mylib
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src
)

2.第三方依赖的系统目录标记

add_executable(my_app src/main.cpp)

# 将 fmt 库的头文件标记为系统目录(减少警告)
target_include_directories(my_app
    SYSTEM PRIVATE
    ${FMT_INCLUDE_DIRS}
)

3.传递性依赖(多级项目)

# libA 项目
add_library(libA STATIC src/libA.cpp)
target_include_directories(libA
    PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}/include  # 公开头文件
)

# libB 项目(依赖 libA)
add_library(libB STATIC src/libB.cpp)
target_link_libraries(libB PUBLIC libA)  # PUBLIC 确保 libA 的头文件路径传递给依赖 libB 的目标

# app 项目(依赖 libB)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE libB)  # app 自动获得 libA 的头文件路径

4.4.与其他命令的对比

命令 作用域 目标针对性 传递性 推荐场景
include_directories 全局 简单项目、临时测试
target_include_directories 目标级 模块化项目、库开发
set_target_properties 目标级 特殊属性(如 CXX_STANDARD)

4.5.注意事项

1.分离公开 / 私有头文件

  • 库项目中,将公开 API 放在 include/ 目录,私有实现放在 src/ 目录。
  • 使用 PUBLIC 暴露公开头文件,PRIVATE 隐藏私有头文件。

2.使用生成器表达式(Generator Expressions)

处理构建时与安装后的路径差异:

target_include_directories(mylib
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>  # 构建时
        $<INSTALL_INTERFACE:include>                            # 安装后
)

3.避免重复路径

  • 同一目标的相同路径只需设置一次,PUBLIC 已包含 PRIVATE 的效果。

4.路径顺序

  • 多个 PRIVATE/PUBLIC/INTERFACE 块按添加顺序处理,先添加的路径优先级更高。
  • 使用 BEFORE 参数可将路径插入到搜索列表的前面:
target_include_directories(my_app
    BEFORE PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/override  # 优先搜索此目录
)

5.目标必须先存在

  • target_include_directories 必须在目标(如 add_executable)创建后调用。

5.总结

命令 核心功能 作用域 使用场景
include(<file>) 导入并执行外部 CMake 脚本或模块 全局 模块化 CMake 代码、导入工具函数
include_guard() 防止 CMake 脚本被重复包含 文件级 所有共享脚本的顶部
include_directories(...) 设置全局头文件搜索路径 全局 简单项目、快速原型开发
target_include_directories() 为特定目标设置头文件搜索路径(支持传递性依赖) 目标级 库开发、模块化项目

相关链接