引言

CMake是一个开源、跨平台的构建自动化工具,它使用平台无关的配置文件(CMakeLists.txt)来控制软件编译过程,并生成标准的构建文件(如Unix的Makefile或Windows Visual Studio的项目文件)。在C++项目开发中,随着项目规模的增长和复杂度的提高,手动管理编译过程变得越来越困难,而CMake正是解决这一问题的利器。

CMake的出现解决了传统构建系统的诸多痛点:它支持跨平台开发,可以生成多种构建系统的原生项目文件;它提供了强大的依赖管理功能,能够轻松处理复杂的库依赖关系;它具有模块化设计,允许开发者将大型项目分解为可管理的组件。通过使用CMake,开发者可以专注于代码实现,而不是构建系统的细节,从而提高开发效率和项目可维护性。

CMake基础

CMake的基本语法

CMake使用自己的脚本语言,语法简单直观。CMake命令不区分大小写,但参数和变量是区分大小写的。命令的基本格式为command(arg1 arg2 ...),注释以#开头。

# 这是一个CMake注释 message(STATUS "Hello, CMake!") # 输出信息 

CMake中的变量使用set()命令设置,使用${VAR_NAME}语法引用:

set(MY_VARIABLE "some value") message(STATUS "Variable value: ${MY_VARIABLE}") 

CMakeLists.txt文件结构

每个CMake项目都至少包含一个CMakeLists.txt文件,通常位于项目根目录。一个基本的CMakeLists.txt文件通常包含以下内容:

# 指定最低CMake版本要求 cmake_minimum_required(VERSION 3.10) # 定义项目名称和版本 project(MyProject VERSION 1.0 LANGUAGES CXX) # 设置C++标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 添加可执行文件 add_executable(my_app main.cpp) # 链接库 target_link_libraries(my_app some_library) 

基本命令介绍

CMake提供了丰富的命令集,以下是一些最常用的命令:

  • cmake_minimum_required(): 指定所需的最低CMake版本
  • project(): 定义项目名称和版本
  • add_executable(): 添加可执行文件目标
  • add_library(): 添加库目标
  • target_include_directories(): 为目标添加包含目录
  • target_link_libraries(): 链接库到目标
  • find_package(): 查找外部依赖包
  • include_directories(): 添加全局包含目录
  • link_directories(): 添加全局链接目录
  • set(): 设置变量
  • option(): 定义用户可选择的选项

项目结构管理

简单单目录项目

对于小型项目,所有源文件可能都位于一个目录中。这种情况下,CMakeLists.txt文件相对简单:

cmake_minimum_required(VERSION 3.10) project(SimpleProject VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 添加所有源文件 add_executable(simple_app main.cpp function1.cpp function1.h function2.cpp function2.h ) # 如果有额外的包含目录 target_include_directories(simple_app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) 

多目录项目

随着项目规模增长,通常会将代码组织到多个目录中。CMake使用add_subdirectory()命令来处理子目录:

假设项目结构如下:

my_project/ ├── CMakeLists.txt ├── src/ │ ├── CMakeLists.txt │ ├── main.cpp │ ├── module1/ │ │ ├── CMakeLists.txt │ │ ├── module1.cpp │ │ └── module1.h │ └── module2/ │ ├── CMakeLists.txt │ ├── module2.cpp │ └── module2.h └── include/ └── common.h 

根目录的CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.10) project(MyProject VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 添加全局包含目录 include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) # 添加子目录 add_subdirectory(src) 

src目录的CMakeLists.txt文件:

# 添加子目录 add_subdirectory(module1) add_subdirectory(module2) # 添加可执行文件 add_executable(my_app main.cpp) # 链接模块库 target_link_libraries(my_app module1 module2 ) 

module1目录的CMakeLists.txt文件:

# 添加库 add_library(module1 module1.cpp) # 添加包含目录 target_include_directories(module1 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) 

module2目录的CMakeLists.txt文件与module1类似。

子目录管理

在大型项目中,合理组织子目录结构非常重要。以下是一些常见的子目录组织方式:

  1. 按功能模块划分:每个功能模块有自己的目录,包含相关的源文件和头文件。

  2. 按文件类型划分:将源文件、头文件、测试文件等分别放在不同目录中。

  3. 混合方式:结合上述两种方式,例如按功能模块划分,但每个模块内部再按文件类型细分。

下面是一个更复杂的项目结构示例:

complex_project/ ├── CMakeLists.txt ├── src/ │ ├── CMakeLists.txt │ ├── core/ │ │ ├── CMakeLists.txt │ │ ├── algorithm.cpp │ │ └── algorithm.h │ ├── utils/ │ │ ├── CMakeLists.txt │ │ ├── logger.cpp │ │ └── logger.h │ └── main.cpp ├── include/ │ └── project/ │ └── config.h ├── tests/ │ ├── CMakeLists.txt │ ├── test_core.cpp │ └── test_utils.cpp ├── third_party/ │ └── CMakeLists.txt └── cmake/ └── FindSomeLib.cmake 

根目录的CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.10) project(ComplexProject VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 设置输出目录 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 添加自定义模块路径 list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) # 添加子目录 add_subdirectory(third_party) add_subdirectory(src) add_subdirectory(tests) 

依赖管理

查找依赖包

CMake提供了find_package()命令来查找系统上已安装的库和包。这个命令会搜索特定的模块文件或配置文件,以确定库是否安装,并提供导入的目标或变量来使用这些库。

# 查找Boost库 find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system) # 查找OpenCV find_package(OpenCV 4 REQUIRED) # 查找Threads库 find_package(Threads REQUIRED) # 使用找到的库 add_executable(my_app main.cpp) target_link_libraries(my_app Boost::filesystem Boost::system ${OpenCV_LIBS} Threads::Threads ) 

如果CMake没有内置的查找模块,你可以自己编写一个。通常,这些自定义模块放在项目目录下的cmake子目录中。

例如,创建一个FindSomeLib.cmake文件:

# 尝试找到头文件 find_path(SOMELIB_INCLUDE_DIR NAMES somelib.h PATHS /usr/include /usr/local/include ) # 尝试找到库文件 find_library(SOMELIB_LIBRARY NAMES somelib PATHS /usr/lib /usr/local/lib ) # 设置变量 include(FindPackageHandleStandardArgs) find_package_handle_standard_args(SomeLib DEFAULT_MSG SOMELIB_LIBRARY SOMELIB_INCLUDE_DIR ) # 如果找到,创建导入目标 if(SOMELIB_FOUND) add_library(SomeLib::SomeLib UNKNOWN IMPORTED) set_target_properties(SomeLib::SomeLib PROPERTIES IMPORTED_LOCATION ${SOMELIB_LIBRARY} INTERFACE_INCLUDE_DIRECTORIES ${SOMELIB_INCLUDE_DIR} ) endif() 

使用ExternalProject管理外部依赖

对于需要下载和编译的外部依赖,CMake提供了ExternalProject模块。这对于那些没有提供CMake支持或需要特殊构建步骤的库特别有用。

include(ExternalProject) # 下载并构建Google Test ExternalProject_Add( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG main PREFIX ${CMAKE_CURRENT_BINARY_DIR}/googletest INSTALL_COMMAND "" ) # 获取gtest和gmock的包含目录和库文件 ExternalProject_Get_Property(googletest source_dir binary_dir) set(GTEST_INCLUDE_DIR ${source_dir}/googletest/include) set(GMOCK_INCLUDE_DIR ${source_dir}/googlemock/include) set(GTEST_LIBRARY_PATH ${binary_dir}/lib/${CMAKE_FIND_LIBRARY_PREFIXES}gtest.a) set(GTEST_MAIN_LIBRARY_PATH ${binary_dir}/lib/${CMAKE_FIND_LIBRARY_PREFIXES}gtest_main.a) set(GMOCK_LIBRARY_PATH ${binary_dir}/lib/${CMAKE_FIND_LIBRARY_PREFIXES}gmock.a) set(GMOCK_MAIN_LIBRARY_PATH ${binary_dir}/lib/${CMAKE_FIND_LIBRARY_PREFIXES}gmock_main.a) # 创建导入目标 add_library(gtest STATIC IMPORTED) add_library(gtest_main STATIC IMPORTED) add_library(gmock STATIC IMPORTED) add_library(gmock_main STATIC IMPORTED) set_target_properties(gtest PROPERTIES IMPORTED_LOCATION ${GTEST_LIBRARY_PATH} INTERFACE_INCLUDE_DIRECTORIES ${GTEST_INCLUDE_DIR} ) set_target_properties(gtest_main PROPERTIES IMPORTED_LOCATION ${GTEST_MAIN_LIBRARY_PATH} INTERFACE_INCLUDE_DIRECTORIES ${GTEST_INCLUDE_DIR} ) set_target_properties(gmock PROPERTIES IMPORTED_LOCATION ${GMOCK_LIBRARY_PATH} INTERFACE_INCLUDE_DIRECTORIES ${GMOCK_INCLUDE_DIR} ) set_target_properties(gmock_main PROPERTIES IMPORTED_LOCATION ${GMOCK_MAIN_LIBRARY_PATH} INTERFACE_INCLUDE_DIRECTORIES ${GMOCK_INCLUDE_DIR} ) # 添加依赖关系 add_dependencies(gtest googletest) add_dependencies(gtest_main googletest) add_dependencies(gmock googletest) add_dependencies(gmock_main googletest) 

使用FetchContent下载依赖

CMake 3.11及更高版本提供了FetchContent模块,它比ExternalProject更简洁易用。FetchContent可以在配置阶段下载依赖,而不是构建阶段,这使得依赖项可以在同一CMake运行中直接使用。

include(FetchContent) # 声明要下载的内容 FetchContent_Declare( fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git GIT_TAG 9.1.0 ) # 下载内容 FetchContent_MakeAvailable(fmt) # 使用下载的库 add_executable(my_app main.cpp) target_link_libraries(my_app fmt::fmt) 

FetchContent还支持声明多个依赖项,并自动处理它们之间的依赖关系:

include(FetchContent) # 声明多个依赖 FetchContent_Declare( catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_TAG v3.3.2 ) FetchContent_Declare( json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.2 ) # 下载所有依赖 FetchContent_MakeAvailable(catch2 json) # 使用依赖 add_executable(my_tests test_main.cpp) target_link_libraries(my_tests Catch2::Catch2WithMain nlohmann_json::nlohmann_json) 

构建类型与配置

Debug和Release配置

CMake支持多种构建类型,最常见的是Debug和Release。Debug配置通常包含调试信息,不进行优化;Release配置则启用优化,不包含调试信息。

# 设置构建类型 if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") endif() # 针对不同构建类型的编译器标志 set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -Wextra -g") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -DNDEBUG") 

在构建项目时,可以通过命令行指定构建类型:

# Debug构建 cmake -DCMAKE_BUILD_TYPE=Debug -S . -B build cmake --build build # Release构建 cmake -DCMAKE_BUILD_TYPE=Release -S . -B build cmake --build build 

自定义构建类型

除了标准的构建类型,你还可以定义自己的构建类型:

# 定义自定义构建类型 set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE} -g -pg") set(CMAKE_CXX_FLAGS_SANITIZE "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address -fsanitize=undefined") # 添加到构建类型列表 set(CMAKE_BUILD_TYPE "${CMAKE_BUILD_TYPE}" CACHE STRING "Choose the type of build.") set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo" "Profile" "Sanitize") 

编译器选项设置

CMake允许你为特定目标设置编译器选项,而不是全局设置。这样可以更精细地控制构建过程:

add_library(my_lib src/my_lib.cpp) # 设置特定目标的编译选项 target_compile_options(my_lib PRIVATE -Wall -Wextra $<$<CONFIG:Debug>:-g> $<$<CONFIG:Release>:-O3> PUBLIC $<$<PLATFORM_ID:Linux>:-pthread> ) # 使用生成器表达式设置条件编译选项 target_compile_definitions(my_lib PRIVATE MY_LIB_EXPORTS $<$<CONFIG:Debug>:DEBUG_MODE> PUBLIC $<$<PLATFORM_ID:Windows>:WIN32_LEAN_AND_MEAN> ) 

跨平台开发

处理平台差异

CMake提供了多种方式来处理不同操作系统和编译器之间的差异:

# 检测操作系统 if(WIN32) # Windows特定的代码 add_definitions(-DWIN32) elseif(UNIX AND NOT APPLE) # Linux特定的代码 add_definitions(-DLINUX) elseif(APPLE) # macOS特定的代码 add_definitions(-DMACOS) endif() # 检测编译器 if(MSVC) # MSVC特定的设置 add_compile_options(/W4) elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") # GCC/Clang特定的设置 add_compile_options(-Wall -Wextra) endif() # 使用平台特定的路径 if(WIN32) set(PLATFORM_LIBS "ws2_32") elseif(UNIX) set(PLATFORM_LIBS "pthread") endif() target_link_libraries(my_app ${PLATFORM_LIBS}) 

条件编译

CMake提供了多种条件编译的方法,包括生成器表达式,它们可以在生成构建系统时评估条件:

add_executable(my_app main.cpp) # 使用生成器表达式条件链接库 target_link_libraries(my_app $<$<PLATFORM_ID:Windows>:ws2_32> $<$<PLATFORM_ID:Linux>:pthread> $<$<CXX_COMPILER_ID:GNU>:gcc_s> ) # 条件包含目录 target_include_directories(my_app PRIVATE $<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/debug_headers> $<$<CONFIG:Release>:${CMAKE_CURRENT_SOURCE_DIR}/release_headers> ) # 条件编译定义 target_compile_definitions(my_app PRIVATE $<$<CONFIG:Debug>:DEBUG_MODE> $<$<CONFIG:Release>:NDEBUG> $<$<PLATFORM_ID:Windows>:PLATFORM_WINDOWS> $<$<PLATFORM_ID:Linux>:PLATFORM_LINUX> ) 

生成器表达式

生成器表达式是CMake的强大功能,它们在生成构建系统时评估,而不是在配置时。这使得它们能够根据构建配置、目标属性等条件生成不同的构建规则:

# 基本生成器表达式 target_compile_definitions(my_app PRIVATE # 只在Debug模式下定义DEBUG $<$<CONFIG:Debug>:DEBUG> # 只在非Windows平台定义UNIX $<$<NOT:$<PLATFORM_ID:Windows>>:UNIX> # 链接特定版本的库 $<$<VERSION_GREATER_EQUAL:${CMAKE_VERSION},3.12>:NEW_FEATURE> ) # 复杂生成器表达式 target_link_libraries(my_app PRIVATE # 在Debug模式下链接调试库,否则链接普通库 $<$<CONFIG:Debug>:mylibd> $<$<NOT:$<CONFIG:Debug>>:mylib> # 根据编译器类型链接不同的库 $<$<CXX_COMPILER_ID:GNU>:gcc_specific_lib> $<$<CXX_COMPILER_ID:Clang>:clang_specific_lib> $<$<CXX_COMPILER_ID:MSVC>:msvc_specific_lib> ) # 文件生成器表达式 target_sources(my_app PRIVATE # 只在Debug模式下包含调试源文件 $<$<CONFIG:Debug>:debug_helpers.cpp> # 根据平台包含特定源文件 $<$<PLATFORM_ID:Windows>:win_specific.cpp> $<$<PLATFORM_ID:Linux>:linux_specific.cpp> ) 

高级特性

自定义命令和目标

CMake允许你添加自定义命令和目标,用于执行特定的构建步骤,如代码生成、文件处理等:

# 添加自定义命令 add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated_code.cpp COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_code.py -i ${CMAKE_CURRENT_SOURCE_DIR}/data/input.json -o ${CMAKE_CURRENT_BINARY_DIR}/generated_code.cpp DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_code.py ${CMAKE_CURRENT_SOURCE_DIR}/data/input.json COMMENT "Generating source code from JSON" ) # 添加自定义目标 add_custom_target(generate_code DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated_code.cpp ) # 将生成的代码添加到可执行文件 add_executable(my_app main.cpp ${CMAKE_CURRENT_BINARY_DIR}/generated_code.cpp ) # 确保在构建可执行文件先生成代码 add_dependencies(my_app generate_code) 

测试集成

CMake通过CTest模块提供了测试支持。你可以定义测试用例,并使用CTest来运行它们:

# 启用测试 enable_testing() # 添加测试可执行文件 add_executable(unit_tests test_main.cpp test_math.cpp test_string.cpp ) # 链接测试库 target_link_libraries(unit_tests my_lib Catch2::Catch2WithMain ) # 添加测试用例 add_test(NAME MathTests COMMAND unit_tests "[math]") add_test(NAME StringTests COMMAND unit_tests "[string]") # 设置测试属性 set_tests_properties(MathTests PROPERTIES WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} TIMEOUT 30 ) # 添加需要特殊设置的测试 add_test(NAME SpecialTest COMMAND special_test_tool --input test_input.txt) set_tests_properties(SpecialTest PROPERTIES ENVIRONMENT "TEST_MODE=1;LOG_LEVEL=debug" DEPENDS MathTests ) 

安装规则

CMake允许你定义安装规则,指定在执行make install时应该安装哪些文件以及安装到何处:

# 安装目标 install(TARGETS my_lib my_app # 库文件安装位置 LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin # 头文件安装位置 PUBLIC_HEADER DESTINATION include ) # 安装文件 install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/README.md ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE DESTINATION share/doc/my_project ) # 安装目录 install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ DESTINATION include FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" ) # 安装脚本 install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/scripts/myscript.py DESTINATION bin ) # 配置文件安装 install(FILES ${CMAKE_CURRENT_BINARY_DIR}/config.h DESTINATION include/my_project ) 

打包

CMake支持创建源码包和二进制包,便于分发:

# 设置包信息 set(CPACK_PACKAGE_NAME "MyProject") set(CPACK_PACKAGE_VERSION "1.0.0") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "My Project Description") set(CPACK_PACKAGE_VENDOR "My Company") set(CPACK_PACKAGE_CONTACT "support@mycompany.com") # 设置包生成器 if(WIN32) set(CPACK_GENERATOR "ZIP;NSIS") elseif(APPLE) set(CPACK_GENERATOR "ZIP;DragNDrop") else() set(CPACK_GENERATOR "TGZ;DEB;RPM") endif() # 包含CPack include(CPack) 

要创建包,可以运行:

# 构建项目 cmake --build build # 创建包 cpack --config build/CPackConfig.cmake 

最佳实践

项目结构建议

一个良好的CMake项目结构应该清晰、可维护且易于扩展。以下是一个推荐的项目结构:

my_project/ ├── CMakeLists.txt # 根CMakeLists.txt ├── README.md # 项目说明 ├── LICENSE # 许可证 ├── cmake/ # CMake辅助文件 │ ├── FindSomeLib.cmake # 自定义查找模块 │ └── Utils.cmake # 自定义函数和宏 ├── src/ # 源代码 │ ├── CMakeLists.txt │ ├── main.cpp │ ├── core/ # 核心模块 │ │ ├── CMakeLists.txt │ │ ├── core.cpp │ │ └── core.h │ └── utils/ # 工具模块 │ ├── CMakeLists.txt │ ├── utils.cpp │ └── utils.h ├── include/ # 公共头文件 │ └── my_project/ │ └── config.h.in # 配置模板 ├── tests/ # 测试 │ ├── CMakeLists.txt │ ├── test_core.cpp │ └── test_utils.cpp ├── examples/ # 示例 │ ├── CMakeLists.txt │ └── example1.cpp ├── docs/ # 文档 │ └── ... ├── scripts/ # 脚本 │ └── ... └── third_party/ # 第三方依赖 └── ... 

可维护性技巧

  1. 使用现代CMake实践:尽量使用基于目标的命令(如target_include_directories())而不是全局命令(如include_directories())。

  2. 创建辅助函数和宏:将重复的CMake代码封装成函数或宏,放在单独的.cmake文件中:

# cmake/Utils.cmake function(add_my_library name) add_library(${name} ${ARGN}) target_include_directories(${name} PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> $<INSTALL_INTERFACE:include> ) set_target_properties(${name} PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_STANDARD 17 ) endfunction() function(add_my_executable name) add_executable(${name} ${ARGN}) set_target_properties(${name} PROPERTIES CXX_STANDARD 17 ) endfunction() 
  1. 使用配置文件模板:对于需要根据构建配置变化的头文件,使用.in模板文件:
# configure_file将模板中的@VAR@替换为变量值 configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/include/my_project/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/include/my_project/config.h @ONLY ) # 将生成的目录添加到包含路径 target_include_directories(my_app PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/include ) 
  1. 使用目标属性:利用目标属性来管理编译选项、定义和包含目录:
set_target_properties(my_lib PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON POSITION_INDEPENDENT_CODE ON VISIBILITY_INLINES_HIDDEN ON ) 

性能优化

  1. 使用CMake预设:CMake 3.19+支持预设文件,可以预先定义常用的构建配置:
# CMakePresets.json { "version": 2, "configurePresets": [ { "name": "debug", "displayName": "Debug", "generator": "Ninja", "binaryDir": "${sourceDir}/build/debug", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_CXX_FLAGS": "-Wall -Wextra -g" } }, { "name": "release", "displayName": "Release", "generator": "Ninja", "binaryDir": "${sourceDir}/build/release", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_CXX_FLAGS": "-O3 -DNDEBUG" } } ] } 
  1. 使用Unity构建:对于包含大量源文件的项目,可以使用Unity构建来减少编译时间:
# 启用Unity构建 set_target_properties(my_lib PROPERTIES UNITY_BUILD ON UNITY_BUILD_BATCH_SIZE 8 ) 
  1. 使用CCache:配置CMake使用CCache来加速重复编译:
# 查找CCache find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) # 设置CMake使用CCache set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) endif() 
  1. 使用Ninja生成器:对于大型项目,Ninja生成器通常比Makefile更快:
cmake -G Ninja -S . -B build cmake --build build 

案例分析

一个复杂项目的CMake配置示例

假设我们要构建一个名为”ImageProcessor”的图像处理库,它有以下特点:

  • 跨平台支持(Windows、Linux、macOS)
  • 依赖多个第三方库(OpenCV、Boost、fmt)
  • 包含多个模块(核心、滤镜、IO)
  • 提供示例和测试
  • 支持安装和打包

项目结构:

ImageProcessor/ ├── CMakeLists.txt ├── README.md ├── LICENSE ├── cmake/ │ ├── FindSomeLib.cmake │ └── Utils.cmake ├── src/ │ ├── CMakeLists.txt │ ├── core/ │ │ ├── CMakeLists.txt │ │ ├── image.cpp │ │ └── image.h │ ├── filters/ │ │ ├── CMakeLists.txt │ │ ├── blur.cpp │ │ ├── blur.h │ │ ├── sharpen.cpp │ │ └── sharpen.h │ └── io/ │ ├── CMakeLists.txt │ ├── loader.cpp │ ├── loader.h │ ├── saver.cpp │ └── saver.h ├── include/ │ └── imageprocessor/ │ └── config.h.in ├── tests/ │ ├── CMakeLists.txt │ ├── test_core.cpp │ ├── test_filters.cpp │ └── test_io.cpp ├── examples/ │ ├── CMakeLists.txt │ ├── demo.cpp │ └── benchmark.cpp └── docs/ └── ... 

根CMakeLists.txt文件:

cmake_minimum_required(VERSION 3.15) project(ImageProcessor VERSION 1.0.0 DESCRIPTION "A cross-platform image processing library" LANGUAGES CXX ) # 设置C++标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 添加自定义模块路径 list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) # 包含辅助函数 include(Utils) # 设置输出目录 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 查找依赖 find_package(OpenCV 4 REQUIRED COMPONENTS core imgproc imgcodecs) find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system) find_package(fmt 9 REQUIRED) # 配置头文件 configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/include/imageprocessor/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/include/imageprocessor/config.h @ONLY ) # 添加子目录 add_subdirectory(src) add_subdirectory(tests) add_subdirectory(examples) # 打包支持 include(CPack) 

src/CMakeLists.txt文件:

# 添加子目录 add_subdirectory(core) add_subdirectory(filters) add_subdirectory(io) # 创建接口库,用于导出头文件 add_library(ImageProcessor INTERFACE) target_include_directories(ImageProcessor INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include> $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include> $<INSTALL_INTERFACE:include> ) # 安装规则 install(TARGETS ImageProcessor EXPORT ImageProcessorTargets LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin ) # 安装头文件 install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../include/imageprocessor DESTINATION include ) # 安装生成的头文件 install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/imageprocessor/config.h DESTINATION include/imageprocessor ) # 导出目标 install(EXPORT ImageProcessorTargets FILE ImageProcessorTargets.cmake NAMESPACE ImageProcessor:: DESTINATION lib/cmake/ImageProcessor ) # 创建配置文件 include(CMakePackageConfigHelpers) write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/ImageProcessorConfigVersion.cmake" VERSION ${PROJECT_VERSION} COMPATIBILITY AnyNewerVersion ) # 安装配置文件 install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ImageProcessorConfigVersion.cmake" DESTINATION lib/cmake/ImageProcessor ) 

src/core/CMakeLists.txt文件:

# 添加库 add_library(ImageProcessorCore image.cpp ) # 链接依赖 target_link_libraries(ImageProcessorCore PUBLIC ImageProcessor OpenCV::core fmt::fmt PRIVATE Boost::filesystem ) # 设置属性 set_target_properties(ImageProcessorCore PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON ) # 安装规则 install(TARGETS ImageProcessorCore EXPORT ImageProcessorTargets LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin ) 

src/filters/CMakeLists.txt文件:

# 添加库 add_library(ImageProcessorFilters blur.cpp sharpen.cpp ) # 链接依赖 target_link_libraries(ImageProcessorFilters PUBLIC ImageProcessor ImageProcessorCore OpenCV::imgproc ) # 设置属性 set_target_properties(ImageProcessorFilters PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON ) # 安装规则 install(TARGETS ImageProcessorFilters EXPORT ImageProcessorTargets LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin ) 

src/io/CMakeLists.txt文件:

# 添加库 add_library(ImageProcessorIO loader.cpp saver.cpp ) # 链接依赖 target_link_libraries(ImageProcessorIO PUBLIC ImageProcessor ImageProcessorCore OpenCV::imgcodecs PRIVATE Boost::system ) # 设置属性 set_target_properties(ImageProcessorIO PROPERTIES POSITION_INDEPENDENT_CODE ON CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON ) # 安装规则 install(TARGETS ImageProcessorIO EXPORT ImageProcessorTargets LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin ) 

tests/CMakeLists.txt文件:

# 启用测试 enable_testing() # 添加测试可执行文件 add_executable(tests test_core.cpp test_filters.cpp test_io.cpp ) # 链接库 target_link_libraries(tests ImageProcessorCore ImageProcessorFilters ImageProcessorIO Catch2::Catch2WithMain ) # 添加测试 add_test(NAME CoreTests COMMAND tests "[core]") add_test(NAME FilterTests COMMAND tests "[filters]") add_test(NAME IOTests COMMAND tests "[io]") 

examples/CMakeLists.txt文件:

# 添加示例可执行文件 add_executable(demo demo.cpp) target_link_libraries(demo ImageProcessorCore ImageProcessorFilters ImageProcessorIO ) # 添加基准测试 add_executable(benchmark benchmark.cpp) target_link_libraries(benchmark ImageProcessorCore ImageProcessorFilters ImageProcessorIO benchmark::benchmark ) # 安装示例 install(TARGETS demo benchmark RUNTIME DESTINATION bin ) 

常见问题及解决方案

  1. 问题:找不到依赖库

解决方案:确保库已安装,并检查find_package()的参数。如果库没有标准的CMake支持,可以编写自定义的Find模块或使用find_path()find_library()手动查找。

 # 手动查找库 find_path(MYLIB_INCLUDE_DIR NAMES mylib.h PATHS /usr/include /usr/local/include ) find_library(MYLIB_LIBRARY NAMES mylib PATHS /usr/lib /usr/local/lib ) if(MYLIB_INCLUDE_DIR AND MYLIB_LIBRARY) add_library(MyLib::MyLib UNKNOWN IMPORTED) set_target_properties(MyLib::MyLib PROPERTIES IMPORTED_LOCATION ${MYLIB_LIBRARY} INTERFACE_INCLUDE_DIRECTORIES ${MYLIB_INCLUDE_DIR} ) else() message(FATAL_ERROR "MyLib not found") endif() 
  1. 问题:链接错误,找不到符号

解决方案:确保所有必要的源文件已添加到目标中,并且所有依赖库已正确链接。使用target_link_libraries()而不是link_libraries(),并确保链接顺序正确。

 # 正确的链接顺序 target_link_libraries(my_app lib1 # 依赖于lib2 lib2 # 依赖于lib3 lib3 # 不依赖其他库 ) 
  1. 问题:Windows和Unix的路径分隔符问题

解决方案:使用CMake的路径处理函数,而不是硬编码路径分隔符。

 # 使用file(TO_CMAKE_PATH)转换路径 file(TO_CMAKE_PATH "$ENV{PROGRAMFILES}/MyLib" MYLIB_PATH) # 使用正斜杠作为路径分隔符 set(MY_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include") # 使用file(GLOB)或file(GLOB_RECURSE)查找文件 file(GLOB_RECURSE SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") 
  1. 问题:构建时间过长

解决方案:使用Unity构建、CCache和Ninja生成器来加速构建。

 # 启用Unity构建 set_target_properties(my_lib PROPERTIES UNITY_BUILD ON UNITY_BUILD_BATCH_SIZE 8 ) # 使用CCache find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM}) endif() 
  1. 问题:跨平台兼容性问题

解决方案:使用CMake的平台检测功能和生成器表达式来处理平台差异。

 # 检测平台 if(WIN32) add_definitions(-DWIN32_LEAN_AND_MEAN) set(PLATFORM_LIBS "ws2_32") elseif(UNIX AND NOT APPLE) add_definitions(-DLINUX) set(PLATFORM_LIBS "pthread") elseif(APPLE) add_definitions(-DMACOS) set(PLATFORM_LIBS "") endif() # 使用生成器表达式 target_link_libraries(my_app ${PLATFORM_LIBS} $<$<PLATFORM_ID:Windows>:win_specific_lib> $<$<PLATFORM_ID:Linux>:linux_specific_lib> ) 

总结

CMake是一个强大而灵活的构建系统,它为C++项目提供了跨平台构建、依赖管理和项目组织的能力。通过本文的介绍,我们了解了CMake的基本概念、语法和高级特性,以及如何使用CMake来管理复杂的C++项目。

CMake的主要优势在于:

  • 跨平台支持:CMake可以在Windows、Linux、macOS等多种平台上生成原生的构建文件。
  • 强大的依赖管理:通过find_package()FetchContentExternalProject等机制,CMake可以轻松处理复杂的依赖关系。
  • 模块化设计:CMake支持将大型项目分解为多个模块和子目录,便于管理和维护。
  • 灵活的配置:CMake支持多种构建类型、编译器选项和条件编译,可以满足不同的开发需求。
  • 丰富的生态系统:CMake有大量的模块和工具支持,如CTest、CPack等,覆盖了测试、打包等多个方面。

要深入学习CMake,推荐以下资源:

  • CMake官方文档
  • Professional CMake: A Practical Guide,一本深入介绍CMake的书籍
  • Effective Modern CMake,一篇介绍现代CMake实践的文章
  • CMake Discourse论坛,一个活跃的CMake社区

通过掌握CMake,你可以更高效地管理C++项目,专注于代码实现而不是构建系统的细节,从而提高开发效率和项目质量。