强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

CMake 从入门到精通:完整教程 / 第 14 章:模块系统

第 14 章:模块系统

14.1 模块概述

CMake 模块是可复用的 .cmake 脚本文件,通过 include()find_package() 加载。

模块来源
├── CMake 内置模块      → ${CMAKE_ROOT}/Modules/
├── 项目自定义模块      → ${CMAKE_MODULE_PATH} 中的目录
└── 包提供的模块        → 通过 find_package 安装

14.2 CMake 内置模块

14.2.1 常用内置模块

模块 用途 典型用法
GNUInstallDirs 标准安装目录 include(GNUInstallDirs)
CTest 测试配置 include(CTest)
CPack 打包配置 include(CPack)
CMakePackageConfigHelpers 包配置生成 include(CMakePackageConfigHelpers)
FetchContent 依赖获取 include(FetchContent)
CheckCXXCompilerFlag 编译器标志检查 include(CheckCXXCompilerFlag)
CheckIncludeFileCXX 头文件检查 include(CheckIncludeFileCXX)
CheckCXXSymbolExists 符号检查 include(CheckCXXSymbolExists)
CheckTypeSize 类型大小检查 include(CheckTypeSize)
CheckLibraryExists 库检查 include(CheckLibraryExists)
GoogleTest GTest 集成 include(GoogleTest)
GenerateExportHeader 导出头文件 include(GenerateExportHeader)
FindPkgConfig pkg-config 集成 include(FindPkgConfig)
CMakeDependentOption 依赖选项 include(CMakeDependentOption)
ProcessorCount 处理器数量 include(ProcessorCount)

14.2.2 系统检测模块

# 编译器标志检测
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-fsanitize=address" HAS_ASAN)

# 头文件检测
include(CheckIncludeFileCXX)
check_include_file_cxx("filesystem" HAS_FILESYSTEM)
check_include_file_cxx("optional" HAS_OPTIONAL)
check_include_file_cxx("variant" HAS_VARIANT)

# 符号检测
include(CheckCXXSymbolExists)
check_cxx_symbol_exists(std::filesystem::exists "filesystem" HAS_STD_FILESYSTEM)

# 类型大小
include(CheckTypeSize)
check_type_size("long long" SIZEOF_LONG_LONG)
check_type_size("void*" SIZEOF_VOID_PTR BUILTIN_TYPES_ONLY)

# 库检测
include(CheckLibraryExists)
check_library_exists(m sin "" HAS_LIBM)
check_library_exists(rt clock_gettime "" HAS_LIBRT)

14.2.3 GNUInstallDirs

include(GNUInstallDirs)

install(TARGETS myapp
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}    # bin
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}    # lib
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}    # lib
)

install(DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}        # include
)

install(FILES myapp.conf
    DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/myapp  # etc/myapp
)

14.2.4 CMakeDependentOption

include(CMakeDependentOption)

option(USE_OPENSSL "使用 OpenSSL" ON)
# SSL 证书验证仅在 USE_OPENSSL 为 ON 时可用
cmake_dependent_option(VERIFY_CERTS "验证 SSL 证书" ON "USE_OPENSSL" OFF)

14.2.5 ProcessorCount

include(ProcessorCount)
ProcessorCount(N)
if(NOT N EQUAL 0)
    set(CTEST_TEST_ARGS ${CTEST_TEST_ARGS} PARALLEL_LEVEL ${N})
endif()

14.3 自定义 Find 模块

14.3.1 完整的 Find 模块

# cmake/FindRapidJSON.cmake
#[=======================================================================[.rst:
FindRapidJSON
-------------

查找 RapidJSON 库。

导入目标
^^^^^^^^

``RapidJSON::RapidJSON`` - 头文件库目标

结果变量
^^^^^^^^

``RapidJSON_FOUND``        - 是否找到
``RapidJSON_VERSION``      - 版本号
``RapidJSON_INCLUDE_DIRS`` - 包含目录

缓存变量
^^^^^^^^

``RapidJSON_INCLUDE_DIR`` - 头文件目录

用法示例
^^^^^^^^

.. code-block:: cmake

    find_package(RapidJSON REQUIRED)
    target_link_libraries(myapp PRIVATE RapidJSON::RapidJSON)

#]=======================================================================]

include(FindPackageHandleStandardArgs)

# 检查是否已创建目标
if(TARGET RapidJSON::RapidJSON)
    return()
endif()

# 查找头文件
find_path(RapidJSON_INCLUDE_DIR
    NAMES rapidjson/document.h
    PATHS
        ${RapidJSON_ROOT}
        $ENV{RapidJSON_ROOT}
    PATH_SUFFIXES include
)

# 读取版本
if(RapidJSON_INCLUDE_DIR AND EXISTS "${RapidJSON_INCLUDE_DIR}/rapidjson/rapidjson.h")
    file(STRINGS "${RapidJSON_INCLUDE_DIR}/rapidjson/rapidjson.h" _ver
        REGEX "^#define[ \t]+RAPIDJSON_MAJOR_VERSION[ \t]+[0-9]+")
    string(REGEX REPLACE "^#define[ \t]+RAPIDJSON_MAJOR_VERSION[ \t]+([0-9]+).*" "\\1"
        _major "${_ver}")
    file(STRINGS "${RapidJSON_INCLUDE_DIR}/rapidjson/rapidjson.h" _ver
        REGEX "^#define[ \t]+RAPIDJSON_MINOR_VERSION[ \t]+[0-9]+")
    string(REGEX REPLACE "^#define[ \t]+RAPIDJSON_MINOR_VERSION[ \t]+([0-9]+).*" "\\1"
        _minor "${_ver}")
    file(STRINGS "${RapidJSON_INCLUDE_DIR}/rapidjson/rapidjson.h" _ver
        REGEX "^#define[ \t]+RAPIDJSON_PATCH_VERSION[ \t]+[0-9]+")
    string(REGEX REPLACE "^#define[ \t]+RAPIDJSON_PATCH_VERSION[ \t]+([0-9]+).*" "\\1"
        _patch "${_ver}")
    set(RapidJSON_VERSION "${_major}.${_minor}.${_patch}")
    unset(_major _minor _patch _ver)
endif()

# 使用标准参数处理
find_package_handle_standard_args(RapidJSON
    REQUIRED_VARS RapidJSON_INCLUDE_DIR
    VERSION_VAR RapidJSON_VERSION
)

# 创建导入目标
if(RapidJSON_FOUND)
    set(RapidJSON_INCLUDE_DIRS ${RapidJSON_INCLUDE_DIR})
    
    if(NOT TARGET RapidJSON::RapidJSON)
        add_library(RapidJSON::RapidJSON INTERFACE IMPORTED)
        set_target_properties(RapidJSON::RapidJSON PROPERTIES
            INTERFACE_INCLUDE_DIRECTORIES "${RapidJSON_INCLUDE_DIR}"
        )
    endif()
    
    mark_as_advanced(RapidJSON_INCLUDE_DIR)
endif()

14.3.2 Find 模块的文件结构

FindXxx.cmake 的标准结构:
1. RST 文档注释
2. 头文件保护(检查 TARGET 是否存在)
3. 查找头文件(find_path)
4. 查找库文件(find_library)
5. 读取版本号
6. 调用 find_package_handle_standard_args
7. 创建 IMPORTED 目标
8. mark_as_advanced

14.3.3 find_package_handle_standard_args

include(FindPackageHandleStandardArgs)

# 简单形式
find_package_handle_standard_args(MyLib
    REQUIRED_VARS MyLib_INCLUDE_DIR MyLib_LIBRARY
    VERSION_VAR MyLib_VERSION
)

# 完整形式
find_package_handle_standard_args(MyLib
    REQUIRED_VARS MyLib_INCLUDE_DIR MyLib_LIBRARY
    VERSION_VAR MyLib_VERSION
    HANDLE_COMPONENTS              # 处理 COMPONENTS 参数
    CONFIG_MODE                    # Config 模式
    FAIL_MESSAGE "找不到 MyLib"   # 自定义失败消息
)

14.3.4 使用自定义 Find 模块

# 添加模块搜索路径
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

# 使用
find_package(RapidJSON REQUIRED)
target_link_libraries(myapp PRIVATE RapidJSON::RapidJSON)

14.4 Utility 模块

14.4.1 编写 Utility 模块

# cmake/MyUtils.cmake

# 添加编译选项的函数
function(my_add_warnings target)
    target_compile_options(${target} PRIVATE
        $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
        $<$<CXX_COMPILER_ID:MSVC>:/W4>
    )
endfunction()

# 设置输出目录的函数
function(my_set_output_dirs target)
    set_target_properties(${target} PROPERTIES
        RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
        LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
        ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
    )
endfunction()

# 版本号解析函数
function(my_parse_version version_string major minor patch)
    string(REPLACE "." ";" parts ${version_string})
    list(GET parts 0 _major)
    list(GET parts 1 _minor)
    list(GET parts 2 _patch)
    set(${major} ${_major} PARENT_SCOPE)
    set(${minor} ${_minor} PARENT_SCOPE)
    set(${patch} ${_patch} PARENT_SCOPE)
endfunction()

# 宏:创建库并配置
macro(my_add_library name)
    cmake_parse_arguments(ARG "" "" "SOURCES;DEPENDS" ${ARGN})
    add_library(${name} ${ARG_SOURCES})
    target_compile_features(${name} PUBLIC cxx_std_17)
    my_add_warnings(${name})
    my_set_output_dirs(${name})
    if(ARG_DEPENDS)
        target_link_libraries(${name} PUBLIC ${ARG_DEPENDS})
    endif()
endmacro()

14.4.2 使用 Utility 模块

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(MyUtils)

my_add_library(mylib
    SOURCES src/a.cpp src/b.cpp
    DEPENDS fmt::fmt Threads::Threads
)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)
my_add_warnings(app)

14.5 配置模块

14.5.1 配置头文件

# cmake/ConfigureHeader.cmake

function(my_configure_header input output)
    configure_file(${input} ${output} @ONLY)
endfunction()

# 使用
include(ConfigureHeader)
my_configure_header(
    ${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/config.h
)

14.5.2 config.h.in 模板

// config.h.in
#pragma once

#define APP_NAME "@PROJECT_NAME@"
#define APP_VERSION "@PROJECT_VERSION@"
#define APP_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define APP_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define APP_VERSION_PATCH @PROJECT_VERSION_PATCH@

#cmakedefine USE_OPENSSL
#cmakedefine USE_ZLIB
#cmakedefine HAS_STD_FILESYSTEM
#cmakedefine01 ENABLE_LOGGING

14.6 模块的组织与管理

14.6.1 项目模块目录结构

project/
├── cmake/
│   ├── FindRapidJSON.cmake      # Find 模块
│   ├── FindMyLib.cmake          # Find 模块
│   ├── MyUtils.cmake            # Utility 模块
│   ├── CompilerWarnings.cmake   # 编译选项
│   ├── Sanitizers.cmake         # Sanitizer 配置
│   ├── StaticAnalyzers.cmake    # 静态分析
│   ├── ProjectConfig.cmake.in   # 包配置模板
│   └── Version.cmake            # 版本管理
├── CMakeLists.txt
└── ...

14.6.2 模块注册

# 顶层 CMakeLists.txt
list(APPEND CMAKE_MODULE_PATH
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake
)

include(Version)
include(CompilerWarnings)
include(Sanitizers)

14.7 CMake 包配置(Package Config)

14.7.1 生成包配置

include(CMakePackageConfigHelpers)

# 模板文件:cmake/MyProjectConfig.cmake.in
# @PACKAGE_INIT@
# include(CMakeFindDependencyMacro)
# find_dependency(Threads)
# include("${CMAKE_CURRENT_LIST_DIR}/MyProjectTargets.cmake")
# check_required_components(MyProject)

configure_package_config_file(
    cmake/MyProjectConfig.cmake.in
    "${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfig.cmake"
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
    PATH_VARS CMAKE_INSTALL_INCLUDEDIR
)

write_basic_package_version_file(
    "${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfigVersion.cmake"
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion
)

install(FILES
    "${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfig.cmake"
    "${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfigVersion.cmake"
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)

install(EXPORT MyProjectTargets
    FILE MyProjectTargets.cmake
    NAMESPACE MyProject::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)

14.8 业务场景

场景:多模块项目的模块管理

# cmake/Modules.cmake
# 统一管理所有自定义模块

set(MY_CMAKE_MODULES_DIR "${CMAKE_CURRENT_LIST_DIR}")

# 注册所有模块目录
list(APPEND CMAKE_MODULE_PATH
    "${MY_CMAKE_MODULES_DIR}"
    "${MY_CMAKE_MODULES_DIR}/find"
)

# 加载公共工具
include(CompilerWarnings)
include(Sanitizers)
include(StaticAnalyzers)

# 检查平台特性
include(CheckCXXCompilerFlag)
include(CheckIncludeFileCXX)

# 设置通用编译特性
add_library(project_defaults INTERFACE)
target_compile_features(project_defaults INTERFACE cxx_std_17)
target_compile_definitions(project_defaults INTERFACE
    $<$<CONFIG:Debug>:DEBUG_BUILD>
    PROJECT_VERSION="${PROJECT_VERSION}"
)

# 导出配置
if(BUILD_SHARED_LIBS)
    target_compile_definitions(project_defaults INTERFACE MYLIB_SHARED)
endif()

# CMakeLists.txt 中使用
include(Modules)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE project_defaults)

14.9 注意事项

问题 说明
CMAKE_MODULE_PATH 优先级 最先添加的路径最先搜索
include 与 find_package 区别 include 直接执行,find_package 查找配置文件
头文件保护 Find 模块应检查 TARGET 是否存在
版本号解析 不同包的版本号格式可能不同
命名冲突 自定义模块不要与内置模块同名

14.10 扩展阅读


上一章:第 13 章 — 高级特性 | 下一章:第 15 章 — 依赖管理 →