夜雨聆风学习资料网

ARTICLE · 1214942

CMake Tutorial系列教程:9、安装机制

CMake Tutorial系列教程:9、安装机制

在实际开发中,构建目录通常并不适合直接分发和使用:可执行文件、库文件和头文件分散在源码树和构建树的不同位置,也缺少统一的方式让其他项目发现和使用这些目标。

为例解决这一问题,CMake提供了完整的安装框架,通过 install()命令将构建产物从源码目录和构建目录复制到统一的安装目录中,并为后续项目消费这些库和程序做好准备。

对于现代目标驱动的开发方式,通常只需要使用 install()安装目标本身,无需手动复制文件。

CMake根据安装对象的类型将目标产物划分为不同的构建类型(Artiface Kind),例如静态库属于 ARCHIVE,动态库属于 LIBRARY,可执行文件属于 RUNTIME,头文件通常通过 FILE_SET HEADERS或 PUBLIC_HEADER进行安装。

对于这些类型,CMake已经预定义了默认安装位置:

  • 可执行文件安装到 bin

  • 库文件安装到 lib/

  • 头文件安装到 include/

因此大多数情况下,项目无需显式指定安装路径,只需让CMake使用默认规则即可。

安装目标的根目录称为安装前缀,用户可以在配置阶段通过 --install-prefix、CMake Presets的 installDir字段或 CMAKE_INSTALL_PREFIX变量指定默认安装位置,也可以在执行安装时通过 cmake --install --prefix临时指定安装目录。

需要注意的是,项目开发者通常不应在 CMakeLists.txt中强制设置 CMAKE_INSTALL_PREFIX,因为这会阻止用户根据自身需求修改安装路径。

为了保证跨平台一致性,CMake使用一组 CMAKE_INSTALL_<dir>变量表示标准安装目录,如 CMAKE_INSTALL_BINDIR、CMAKE_INSTALL_LIBDIR和 CMAKE_INSTALL_INCLUDEDIR。

这些变量默认并不存在,如果项目需要基于标准目录进一步划分子目录,应该先引入 include(GNUInstallDirs)模块。该模块会自动为各种安装目录提供合理的默认值,从而使项目能够以统一、可移植的方式定义安装规则。


练习1:安装构建产物

现代CMake的目标驱动开发模式,安装构建产物变得非常简单,只需要使用一次 install()命令,就可以完成可执行文件、库文件以及相关头文件的安装工作。

install(TARGETS <target>... [...])install(IMPORTED_RUNTIME_ARTIFACTS <target>... [...])install({FILES | PROGRAMS} <file>... [...])install(DIRECTORY <dir>... [...])install(SCRIPT <file> [...])install(CODE <code> [...])install(EXPORT <export-name> [...])install(PACKAGE_INFO <package-name> [...])install(RUNTIME_DEPENDENCY_SET <set-name> [...])install(SBOM <sbom-name> [...])
下面是一个简单案例,MyApp和 MyLib都是需要安装的目标,对于可执行文件、静态库和动态库等常见产物,CMake会根据目标类型自动判断其属于 ARCHIVE、LIBRARY还是 RUNTIME,并安装到默认目录中,因此通常不需要指定安装位置。

但头文件集合(FIEL_SET)不会自动安装,必须显式列出名称,否则CMake无法知道哪些头文件需要复制到安装目录。

这个案例中,安装了两个头文件集合 HEADERS和 anotherHeaderFileSet。

install(  TARGETS MyApp MyLib  FILE_SET HEADERS  FILE_SET anotherHeaderFileSet)
用户可以根据需要修改安装位置,如果需要修改某类产物的安装位置,可以在对应的构建类型后面指定额外选项,例如:
include(GNUInstallDirs)install(  TARGETS MyApp MyLib  RUNTIME    DESTINATION ${CMAKE_INSTALL_BINDIR}/Subfolder  FILE_SET HEADERS)
首先引入 GNUInstallDirs模块,获得标准安装目录变量,然后通过 RUNTIME DESTINATION指定可执行文件的安装位置,其中 ${CMAKE_INSTALL_BINDIR}实际上就是 bin/。

需要注意的是对象类型目标,对象库本身不会生成独立的库文件,如果在安装规则中没有为 OBJECT构件指定目标目录,就会表现得类似于 INTERFACE库:仅安装其公开头文件,而不安装对象文件本身。

程序由 TUTORIAL_BUILD_UTILITIES选项控制是否构建,将安装规则放在控制之内,修改 CMakeLists.txt文件。

if(TUTORIAL_BUILD_UTILITIES)  add_subdirectory(Tutorial)  # TODO1: Install the Tutorial target  install(TARGETS Tutorial)endif()# TODO2: Install the MathFunctions, OpAdd, OpMul, OpSub, SqrtTable, and MathLogger targets. Ensure you name their header file set so the headers will be installed.install(  TARGETS MathFunctions OpAdd OpMul OpSub MathLogger SqrtTable  FILE_SET HEADERS)
当执行 cmake --install build,CMake会自动根据目标类型判断安装位置。

Tutorial为可执行程序,属于 RUNTIME类型,默认安装到 bin/目录。

这种写法充分体现了CMake的目标驱动思想,只需要声明需要安装的目标,不需要关心具体生成哪些文件。

install()安装了很多库目标,FILE_SET HEADERS表示安装这些目标关联的头文件集合。

对于每个目标,如果之前通过 target_source(MyLib PUBLIC FILE_SET HEADERS FILES MyLib.h)的方式定义了头文件集合,那么安装的时候会自动复制到对应位置。

补全上述代码之后,在终端执行如下命令,并为安装命令设置安装目录 install/,所有安装文件将会输出到这个目录。

$ cmake --preset tutorial$ cmake --build build# 指定安装目录$ cmake --install build --prefix install
安装命令的语法结构为 cmake --install <build-dir> [options],其中 <build-dir>为构建目录,就是 cmake -B build生成的目录。
  • --config:用于多配置生成器选择安装哪个配置,常见配置生成器有 Visual Studio、Xcode等,这些生成器一次配置后可以同时生成 Debug、Release、RelWithDebInfo、MinSizeRel等类型,安装的时候应该指定类型,例如 cmake --install build --config Debug。对于普通 Ninja或Makefile只有一个配置,就不需要指定。

  • --component:参数有 Runtime和 Development,分别用于安装运行时文件和开发文件。

  • --default-directory-permissions:指定安装时新创建目录的权限,权限有 OWNER_READ OWNER_WRITE OWNER_EXECUTE等。

  • -j或 --parallel:指定并行安装任务个数。

  • --prefix:安装文件输出目录。

  • --strip:安装时自动剥离调试符号。

  • -v或 --verbose:显示详细安装过程。

练习2:导出目标

通过 install(TARGETS)将可执行文件、库文件和头文件安装到安装树后,用户已经能够手动链接这些文件。但对于其他CMake项目而言,仅有安装后的文件本身还不够,因为原工程中的目标及其使用要求不会自动保留下来。

例如安装后的目录只有 .a和 .h等文件,原项目中的信息已经不存在,对于其他CMake项目来说,这些安装后的文件与第三方预编译库没有本质区别,无法自动获得头文件搜索路径、编译选项等。

因此需要一种机制,在其他项目中重新导入这些安装后的文件,并恢复其对应的CMake目标及相关属性。

安装时增加 EXPORT参数,并指定导出集合。案例如下,导出的目标 MyApp和 MyLib会加入导出集合(Export Set)MyProjectTargets,后续 install(EXPORT)将根据该集合生成对应的目标导出文件。

install(  TARGETS MyApp MyLib  EXPORT MyProjectTargets)
将目标加入导出集合之后,生成导出文件。案例如下,执行安装之后,CMake会自动生成 lib/cmake/MyProject/MyProjectTargets.cmake。
include(GNUInstallDirs)install(  EXPORT MyProjectTargets  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject  NAMESPACE MyProject::)
假设导出目标 MathFunctions,如果指定 NAMESPACE Tutorial::,那么导出后得到的目标名称变成 Tutorial::MathFunctions,这就可以避免名称冲突。

这里的 Tutorial::MathFunctions并不是重新创建的目标,而是一个导入目标(Imported Target)。使用的时候,加上前缀命名空间,例如 Tutorial::MathFunctions,通常把项目名称定为命名空间。

理论上可以直接用 Targets.cmake获得所有目标,但实际项目一般不会这么做,因为 Targets.cmake只负责导出目标,而项目通常还需要版本检查、依赖检查、配置变量等。

所以通常会提供一个 <ProjectName>Config.cmake,作为统一入口。Config文件如下,其中 CMAKE_CURRENT_LIST_DIR表示当前正在执行的 .cmake文件所在目录。

# MyProjectConfig.cmakeinclude(  ${CMAKE_CURRENT_LIST_DIR}/MyProjectTargets.cmake)
当用户执行 find_package(MyProject)时,CMake首先查找并加载 MyProjectConfig.cmake,再由该文件导入 MyProjectTargets.cmake中定义的所有目标。

Config文件还需要安装,安装方式如下,安装生成的 .cmake文件将会放在 /cmake/MyProject/目录下。

install(  FILES cmake/MyProjectConfig.cmake  DESTINATION    ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject)
练习2把库文件、可执行文件和头文件安装到安装树中,让其他CMake项目能够通过 find_package()恢复这些目标及其依赖关系,完善 CMakeLists.txt的代码。
  • 修改安装规则,导入 TutorialTargets,就是把 Tutorial目标加入 TutorialTargets导出集合。

  • 引入 GNUInstallDirs,用于 CMAKE_INSTALL_LIBDIR等标准安装目录变量加载,类似变量还有 CMAKE_INSTALL_BINDIR、CMAKE_INSTALL_INCLUDEDIR、CMAKE_INSTALL_DATADIR等。

  • 导出所有目标,并在安装阶段自动生成 TutorialTargets.cmake,且安装在 lib/cmake/Tutorial/|。

  • 创建配置文件 cmake/TutorialConfig.cmake,并安装配置文件。

if(TUTORIAL_BUILD_UTILITIES)  add_subdirectory(Tutorial)  # TODO1: Install the Tutorial target  # install(TARGETS Tutorial)  # TODO3: Add the Tutorial target to the TutorialTargets export  install(    TARGETS Tutorial    EXPORT TutorialTargets  )endif()# TODO4: Include the GNUInstallDirs moduleinclude(GNUInstallDirs)# 修改原先的安装规则,导出所有库目标# TODO2: Install the MathFunctions ...# install(#   TARGETS MathFunctions OpAdd OpMul OpSub MathLogger SqrtTable#   FILE_SET HEADERS# )# TODO5: Add the targets from TODO2 to the TutorialTargets exportinstall(  TARGETS MathFunctions OpAdd OpMul OpSub MathLogger SqrtTable  EXPORT TutorialTargets  FILE_SET HEADERS)# TODO6: Install the TutorialTargets export to: ${CMAKE_INSTALL_LIBDIR}/cmake/Tutorial And give them a namespace of "Tutorial::"install(  EXPORT TutorialTargets  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Tutorial  NAMESPACE Tutorial::)# TODO7: Install the config file at cmake/TutorialConfig.cmake to the same destination as the TutorialTargets export.install(  FILES    cmake/TutorialConfig.cmake  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Tutorial)
修改配置文件 cmake/TutorialConfig.cmake的代码,加载所有导出的目标。
# TODO8: Include the TutorialTargets.cmake file,it will be located in the same list directory as this file when it is run.include(${CMAKE_CURRENT_LIST_DIR}/TutorialTargets.cmake)
补全上述代码之后,在终端执行如下命令,安装完成后,导出文件和配置文件位于 install/lib/cmake/Tutorial/目录,其中 TutorialConfig.cmake是 find_package(Tutorial)的入口文件,而 TutorialTargets.cmake负责导入所有已安装目标。
$ cmake --preset tutorial$ cmake --build build$ cmake --install build --prefix install

练习3:导出版本信息

为导出的CMake项目生成版本信息文件,其他项目在执行 find_package() 时能够先检查版本兼容性,再决定是否导入目标。

install(EXPORT TutorialTargets ...) 导出来目标文件 TutorialTargets.cmake,其他项目使用 find_package(Tutorial 1.0 REQUIRED) 时,CMake需要知道Tutorial项目是否存在、版本是否满足要求。

当其他项目执行 find_package(Tutorial 1.0 REQUIRED) 时,CMake首先查找 TutorialConfig.cmake 和 TutorialConfigVersion.cmake。如果存在版本文件,则优先加载 TutorialConfigVersion.cmake 判断版本是否满足要求。只有版本检查通过后,才会继续加载 TutorialConfig.cmake,而该文件通常会进一步 include(TutorialTargets.cmake) 导入目标。

因此版本检查,能够避免在版本不兼容时加载错误的目标定义。

CMake提供了 include(CMakePackageConfigHelpers),调用 write_basic_package_version_file() 会自动生成 TutorialConfigVersion.cmake。

该文件不仅记录项目版本号,还包含版本比较逻辑,用于在执行 find_package() 时判断请求版本是否兼容,并设置 PACKAGE_VERSION_COMPATIBLE、PACKAGE_VERSION_EXACT 等变量。

假设 project() 定义项目并指定版本信息,然后引入 CMakePackageConfigHelpers 模块,调用 write_basic_package_version_file() 命令生成版本信息,即 TutorialConfigVersion.cmake 文件就会记录 PACKAGE_VERSION = 1.0,并包含版本比较逻辑。

随后 install() 安装,版本信息将会放在 lib/cmake/Tutorial/ 目录下。

COMPATIBILITY 选项控制版兼容策略,可选值有 ExactVersion、SameMajorVersion、SameMinorVersion、AnyNewerVersion,分别表示要求版本完全一致、主版本相同、主版本和次版本相同、当前项目版本大于等于请求版本。

project(Tutorial VERSION 1.0)include(CMakePackageConfigHelpers)write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/TutorialConfigVersion.cmake COMPATIBILITY ExactVersion)install( FILES ${CMAKE_CURRENT_BINARY_DIR}/TutorialConfigVersion.cmake DESTINATION lib/cmake/Tutorial)

对于Header-only库或纯脚本库等与处理器架构无关的软件包,可以使用 ARCH_INDEPENDENT。

这样生成的版本文件会声明该项目不依赖目标平台架构,从而允许不同架构共享同一份安装内容。

write_basic_package_version_file( TutorialConfigVersion.cmake COMPATIBILITY SameMajorVersion ARCH_INDEPENDENT)

在前面导出和安装目标的基础上,完善练习3的 CMakeLists.txt,为Tutorial增加版本管理能力。

  • project() 增加版本控制参数 VERSION,CMake会自动定义一系列变量:PROJECT_VERSION、PROJECT_VERSION_MAJOR、PROJECT_VERSION_MINOR、PROJECT_VERSION_PATCH。如果没有显式指定 VERSION 参数,write_basic_package_version_file() 默认使用当前项目的 PROJECT_VERSION 作为生成的项目版本号。

  • 引入 CMakePackageConfigHelpers 模块,用于生成 XXXConfig.cmake 和 XXXConfigVersion.cmake 等相关文件,其中最重要的两个命令为 configure_package_config_file() 和 write_basic_package_version_file(),分别用于生成配置文件 XXXConfig.cmake 和版本控制文件 XXXConfigVersion.cmake。

  • install() 把生成的版本文件安装到项目目录,这里有 TutorialConfig.cmake 和 TutorialConfigVersion.cmake 两个文件,都在 install/lib/cmake/Tutorial/ 目录。

# TODO9: Add a VERSION parameter to the project() command for version 1.0.0# project(Tutorial)project(Tutorial VERSION 1.0.0)# TODO10: Include CMakePackageConfigHelpersinclude(CMakePackageConfigHelpers)# TODO11: Use write_basic_package_version_file to write a TutorialConfigVersion.cmake file to the CMAKE_CURRENT_BINARY_DIR. The version compatibility should be ExactVersion.write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/TutorialConfigVersion.cmake COMPATIBILITY ExactVersion)# TODO12: Add the generated TutorialConfigVersion.cmake file to the file list of the TODO7 install() command.install( FILES cmake/TutorialConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/TutorialConfigVersion.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Tutorial)# TODO7: Install the config file ...# install(# FILES# cmake/TutorialConfig.cmake# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Tutorial# )

补全上述代码之后,在终端执行如下命令,安装完成后,导出文件和配置文件位于 install/lib/cmake/Tutorial/ 目录,其中包含版本信息文件 TutorialConfigVersion.cmake。

$ cmake --preset tutorial$ cmake --build build$ cmake --install build --prefix install

相关学习资料