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 章 — 依赖管理 →