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

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)
GoogleTestGTest 集成include(GoogleTest)
GenerateExportHeader导出头文件include(GenerateExportHeader)
FindPkgConfigpkg-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 章 — 依赖管理 →