CMake极大地简化了跨平台软件项目的构建过程,提高了开发者的生产力,是现代C++项目开发不可或缺的工具之一。
CMake 是一个开源的、跨平台的自动化构建系统,一般用于 C++ 项目的构建过程。它最初设计用于简化编写外部构建文件的过程,以适应不同的编译器和操作系统。CMake 使用简单的配置文件(通常称为 CMakeLists.txt
)来描述项目结构、依赖关系、编译选项和其他构建相关的设置,然后根据这些配置自动生成适合特定平台(如 Unix Makefiles、Microsoft Visual Studio、Xcode 等)的构建文件。
本文全面介绍现代 CMake 的实践技巧与最佳实践,旨在帮助开发者简化及高效管理 C++ 项目跨平台编译、调试、测试、打包过程。展示了多种使用 CMake 的方式,包括快速设置构建目录、编译不同配置以及安装和测试项目,同时提及了如何在 VSCode 中利用插件无缝集成 CMake 工作流,提升开发效率。不仅能掌握CMake的基本操作,还能深入理解其高级功能,从而在实际项目中运用现代CMake实践,构建高质量的 C++ 项目。
主要特点:
-
跨平台性:CMake 支持多种操作系统和编译器,包括 Windows、Linux、macOS、Android、iOS 以及 GCC、Clang、MSVC 等编译器,使得开发者能以统一的方式在不同平台上构建项目。
-
简洁的语法:CMake 提供了一套高级的脚本语言,允许用户以相对简洁的命令描述构建逻辑,避免了手动编写复杂的 Makefile 或 IDE 项目文件。
-
模块化:项目可以通过
add_subdirectory
命令包含子目录,每个子目录可以有自己的CMakeLists.txt
文件,这样可以实现模块化管理和复用构建逻辑。 -
编译选项:CMake 支持通过命令行参数或 GUI 工具动态修改构建配置,比如选择构建类型(Debug、Release)、启用或禁用特定功能、指定安装路径等。
-
库管理:CMake 支持静态库、动态库的创建与链接,并通过
target_link_libraries
、target_include_directories
等命令管理依赖关系。特别是INTERFACE
、PUBLIC
、PRIVATE
属性的使用,使得库的使用者能清晰地了解如何正确链接和使用库。 -
测试与安装:CMake 可以集成单元测试(如 Google Test),并通过
ctest
命令执行测试。同时,install
命令支持定义安装规则,方便将构建结果部署到指定位置。 -
代码生成:CMake 可以通过
configure_file
命令基于模板生成源代码或配置头文件,方便传递编译时的宏定义或版本信息。 -
现代CMake实践:现代CMake强调使用更清晰、更面向对象的风格,减少全局变量的使用,更多地依赖于目标属性和接口库来控制编译和链接行为,这有助于提高代码的可维护性和可读性。
命令行方式使用 cmake
# cmake [<options>] -S <path-to-source> -B <path-to-build>
# 方式1:在源码目录执行
cmake -S . -B ./build
# 方式2:进入编译目录执行
cd build
cmake ..
cmake --build . --config Release
# 方式3:创建两个子目录分别编译
cd debug
cmake --build . --config Debug
cd ../release
cmake --build . --config Release
# 安装
cmake --install . --prefix "D:/path-to-install"
# 测试,./build 目录中执行
ctest -vv -C Release # ctest 跟随 cmake 安装
# 打包成安装包
cpack -C Release
vscode 插件方式使用cmake
点击状态栏按钮可快速编译、调试、运行
安装 vscode 插件: “twxs.cmake”, “ms-vscode.cmake-tools”
设置使用的 cmake 路径:
指定源码根目录:
cmake 指南
基于官网教程:CMake Tutorial
含可执行程序、链接库编译,自动测试及英语打包的核心 cmake 功能示例
项目设置
# 指定 cmake 最低版本,以保证兼容性
cmake_minimum_required(VERSION 3.15)
# 设置项目名称及版本
project(Tutorial VERSION 1.0 LANGUAGES CXX)
# 打印信息
message(STATUS "PROJECT_BINARY_DIR: " ${PROJECT_BINARY_DIR})
# 运行时在哪里查找动态库
if(APPLE)
set(CMAKE_INSTALL_RPATH "@executable_path/../lib")
elseif(UNIX)
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")
endif()
# 表达式生成器 $<...>,如
# target_include_directories(tgt PRIVATE /opt/include/$<CXX_COMPILER_ID>)
# 根据所使用的c++编译器生成 /opt/include/GNU, /opt/include/Clang等。
# 它们支持条件链接、编译时使用的条件定义、条件包含目录等等。
# 条件可能基于构建配置、目标属性、平台信息或任何其他可查询信息。
# 文档:https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html#introduction
编译可执行程序
# 生成的可执行文件带d,适用于单配置生成器如(gcc),不适用于多配置生成器(例如Visual Studio)
set(CMAKE_DEBUG_POSTFIX d)
# 通过指定 INTERFACE 作用域创建接口库,目标都链接该接口库,以实现通过唯一源设置编译参数
add_library(tutorial_compiler_flags INTERFACE)
target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)
# add compiler warning flags just when building this project via the BUILD_INTERFACE genex
# BUILD_INTERFACE 指定仅在编译项目时添加编译器警告
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(tutorial_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
# control where the static and shared libraries are built so that on windows
# we don't need to tinker with the path to run the executable
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
# 默认编译为动态库, cmake .. -DBUILD_SHARED_LIBS=OFF 编译为静态库
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
# 从 .in 生成头文件,传递项目版本号
# #define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
configure_file(TutorialConfig.h.in TutorialConfig.h)
# 添加 cmake 子目录, 目录中需含有 CMakeLists.txt,会继承父级变量
add_subdirectory(MathFunctions)
# add the executable
add_executable(Tutorial tutorial.cxx)
set_target_properties(Tutorial PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX})
# 添加库的现代cmake方式: 因为 MathFunctions library define its own usage requirements, 仅指定要链接该库就行了
# PUBLIC 作用域之后的库被链接到目标,并成为对外链接库接口的一部分。
# PRIVATE 后面的库和目标被链接到目标,但不作为对外链接库接口的一部分。
# INTERFACE 后面的库被附加到链接接口,不链接当前<目标>。
target_link_libraries(Tutorial PUBLIC MathFunctions tutorial_compiler_flags)
# 添加头文件路径,因为前面生成的 TutorialConfig.h 在项目编译目录中,需要指定该目录以找到该文件
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)
编译库
# add the library that runs
add_library(MathFunctions MathFunctions.cxx)
# let our library define its own usage requirements
# 添加 INTERFACE 作用域声明任何使用该库的项目都包含当前源码路径,但该库不需要依赖该头文件
# 说明:
# 如果头文件只被库的实现使用,则使用 PRIVATE 关键字指定它。
# 如果在库的头文件中额外使用了依赖项(例如用于类继承),那么应该将其指定为 PUBLIC 依赖项。
# 如果不被库的实现使用,而只是向外部提供头文件的依赖项应该被指定为 INTERFACE 依赖项。
# 通过表达式生成器 $<...> 设置编译和安装使用不同的路径。
target_include_directories(MathFunctions
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
# 用户选项,是否使用自定义的方法
option(USE_MYMATH "Use tutorial provided math implementation" ON)
if(USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
include(MakeTable.cmake) # generates Table.h
# library that just does sqrt
add_library(SqrtLibrary STATIC
mysqrt.cxx
${CMAKE_CURRENT_BINARY_DIR}/Table.h
)
# state that we depend on our binary dir to find Table.h
target_include_directories(SqrtLibrary PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
)
# state that SqrtLibrary need PIC when the default is shared libraries
set_target_properties(SqrtLibrary PROPERTIES
POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS}
)
# link SqrtLibrary to tutorial_compiler_flags
target_link_libraries(SqrtLibrary PUBLIC tutorial_compiler_flags)
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
# link MathFunctions to tutorial_compiler_flags
target_link_libraries(MathFunctions PUBLIC tutorial_compiler_flags)
# windows DLL 需要, define the symbol stating we are using the declspec(dllexport) when building on windows
target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")
# 写入版本信息
set_property(TARGET MathFunctions PROPERTY VERSION "1.0.0")
set_property(TARGET MathFunctions PROPERTY SOVERSION "1")
# 声明为库目标,默认为静态库 == add_library(MathFunctions STATIC MathFunctions.cxx)
# 启用 BUILD_SHARED_LIBS 变量会覆盖强制编译动态库
add_library(MathFunctions MathFunctions.cxx)
测试
# 启用测试功能
enable_testing()
# 测试是否可正常运行
add_test(NAME Runs COMMAND Tutorial 25)
# 测试无参时打印提示信息
add_test(NAME Usage COMMAND Tutorial)
set_tests_properties(Usage
PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number"
)
# define a function to simplify adding tests,批量测试
function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg}
PROPERTIES PASS_REGULAR_EXPRESSION ${result}
)
endfunction()
do_test(Tutorial 4 "4 is 2")
do_test(Tutorial 9 "9 is 3")
do_test(Tutorial 5 "5 is 2.236")
do_test(Tutorial 7 "7 is 2.645")
do_test(Tutorial 25 "25 is 5")
do_test(Tutorial -25 "-25 is (-nan|nan|0)")
do_test(Tutorial 0.0001 "0.0001 is 0.01")
安装
- 主应用安装
# CMAKE_INSTALL_PREFIX 用于指定安装的根目录可以通过 --prefix 覆盖
# 安装可执行程序到 bin/ 目录
install(TARGETS Tutorial DESTINATION bin)
# 安装文件头文件到 include/ 目录,还可安装一个目录到指定文件夹
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
DESTINATION include
)
# 安装库的 cmake 配置, 通过 find_package(MathFunctions 1.0) 使用.
install(EXPORT MathFunctionsTargets
FILE MathFunctionsTargets.cmake
DESTINATION lib/cmake/MathFunctions
)
include(CMakePackageConfigHelpers)
# 生成 cmake config file
configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake"
INSTALL_DESTINATION "lib/cmake/example"
NO_SET_AND_CHECK_MACRO
NO_CHECK_REQUIRED_COMPONENTS_MACRO
)
# 生成 cmake 包版本信息文件,记录包的版本和兼容性
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
VERSION "${Tutorial_VERSION_MAJOR}.${Tutorial_VERSION_MINOR}"
COMPATIBILITY AnyNewerVersion
)
# 安装生成的文件
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake
DESTINATION lib/cmake/MathFunctions
)
# generate the export targets for the build tree needs to be after the install(TARGETS ) command
# 以上为我们的项目生成了一个可重定位的 CMake 配置,可以在项目安装或打包后使用。
# 如果我们希望我们的项目也能在构建目录中使用需要在添加以下内容:
export(EXPORT MathFunctionsTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsTargets.cmake"
)
- 库安装
# 通过变量 installable_libs 指定要安装的目标
set(installable_libs MathFunctions tutorial_compiler_flags)
if(TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
# EXPORT 关键字会生成一个CMake文件,包含 install 命令中列出的所有目标。
install(TARGETS ${installable_libs}
EXPORT MathFunctionsTargets
DESTINATION lib)
# install include headers
install(FILES MathFunctions.h DESTINATION include)
打包
通过 CPack 生成应用安装包
# setup installer
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set(CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set(CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)
理解 PRIVATE、PUBLIC、INTERFACE
理解目标属性 及 PRIVATE、PUBLIC、INTERFACE 范围关键字
INTERFACE_*
中的目标属性是 usage requirements:使用要求,表示被其他目标使用时,会收到这些目标属性- 在编译源文件时,会使用
INCLUDE_DIRECTORIES
、COMPILE_DEFINITIONS
和COMPILE_OPTIONS
定义的内容。 target_include_directories()
、target_compile_definitions()
和target_compile_options()
指定编译目标的构建规范和使用要求。通过这些命令分别填充INCLUDE_DIRECTORIES
、COMPILE_DEFINITIONS
和COMPILE_OPTIONS
目标属性及INTERFACE_*
目标属性。- PRIVATE 模式只填充目标属性的非
INTERFACE_*
变量,而 INTERFACE 模式只填充INTERFACE_*
变量。PUBLIC模式填充各自目标属性的两个变量。 - 使用要求可以在依赖目标中传播,通过 target_link_libraries 控制传播范围。
set(srcs archive.cpp zip.cpp)
if (LZMA_FOUND)
list(APPEND srcs lzma.cpp)
endif()
add_library(archive SHARED ${srcs})
if (LZMA_FOUND)
# PRIVATE 模式仅填充 COMPILE_DEFINITIONS,相当于编译本目标 archive 时指定 -DBUILDING_WITH_LZMA,不会传播到依赖该库的目标
target_compile_definitions(archive PRIVATE BUILDING_WITH_LZMA)
endif()
# 填充 INTERFACE_COMPILE_DEFINITIONS,编译本目标 archive 时不使用 INTERFACE_* 中的目标属性。可传播到依赖该库的目标
target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)
add_executable(consumer)
# 链接 archive 到 consumer,并且收到 archive 的使用要求 INTERFACE_COMPILE_DEFINITIONS
# 相当于编译可执行程序时带有 -DUSING_ARCHIVE_LIB
target_link_libraries(consumer archive)
add_library(archive archive.cpp)
target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)
add_library(serialization serialization.cpp)
target_compile_definitions(serialization INTERFACE USING_SERIALIZATION_LIB)
add_library(archiveExtras extras.cpp)
# 生成 archiveExtras 时会带有使用要求中的目标属性 -DUSING_ARCHIVE_LIB 和 -DUSING_SERIALIZATION_LIB
target_link_libraries(archiveExtras PUBLIC archive) # 会传播填充 INTERFACE_COMPILE_DEFINITIONS
target_link_libraries(archiveExtras PRIVATE serialization)
add_executable(consumer consumer.cpp)
# 生成 consumer 会使用 -DUSING_ARCHIVE_LIB,接收到 INTERFACE_COMPILE_DEFINITIONS
target_link_libraries(consumer archiveExtras)
工具
-
cmake 安装包下载: https://cmake.org/download/
-
MSYS2
MSYS2(Minimal SYStem 2)是一个集成了大量的GNU工具链、工具和库的开源软件包集合。它提供了一个类似于Linux的shell环境,可以在Windows系统中编译和运行许多Linux应用程序和工具。MSYS2基于MinGW-w64平台,提供了一个完整的开发环境,包括GCC编译器、GDB调试器、Make、Git版本控制系统和许多其他开发工具。除了常用的开发库和工具之外,MSYS2还提供了许多专门针对Windows平台的库和工具,方便开发人员进行跨平台开发和移植工作。由于MSYS2拥有比较完整的Linux工具链和库,因此它成为了许多跨平台开发和移植项目的首选工具。另外,使用MSYS2也可以轻松地在Windows系统中搭建一个类似于Linux的软件开发环境,方便开发人员进行开发和调试工作。
- 通过 MSYS2 安装 gcc: https://code.visualstudio.com/docs/cpp/config-mingw
pacman -S --needed base-devel mingw-w64-ucrt-x86_64-toolchain
cmake 替代品
xmake: 一个基于 Lua 的轻量级跨平台构建工具,使用 xmake.lua 维护项目构建,相比 makefile/CMakeLists.txt,配置语法更加简洁直观。
更多参考
官方文档:https://cmake.org/getting-started/
https://leimao.github.io/blog/CMake-Public-Private-Interface/
https://kubasejdak.com/modern-cmake-is-like-inheritance