ARTICLE · 1214942
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 MyLibFILE_SET HEADERSFILE_SET anotherHeaderFileSet)
用户可以根据需要修改安装位置,如果需要修改某类产物的安装位置,可以在对应的构建类型后面指定额外选项,例如:include(GNUInstallDirs)install(TARGETS MyApp MyLibRUNTIMEDESTINATION ${CMAKE_INSTALL_BINDIR}/SubfolderFILE_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 targetinstall(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 SqrtTableFILE_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 MyLibEXPORT MyProjectTargets)
将目标加入导出集合之后,生成导出文件。案例如下,执行安装之后,CMake会自动生成lib/cmake/MyProject/MyProjectTargets.cmake。
include(GNUInstallDirs)install(EXPORT MyProjectTargetsDESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProjectNAMESPACE 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.cmakeDESTINATION${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 exportinstall(TARGETS TutorialEXPORT 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 SqrtTableEXPORT TutorialTargetsFILE_SET HEADERS)# TODO6: Install the TutorialTargets export to: ${CMAKE_INSTALL_LIBDIR}/cmake/Tutorial And give them a namespace of "Tutorial::"install(EXPORT TutorialTargetsDESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/TutorialNAMESPACE Tutorial::)# TODO7: Install the config file at cmake/TutorialConfig.cmake to the same destination as the TutorialTargets export.install(FILEScmake/TutorialConfig.cmakeDESTINATION ${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.cmakeCOMPATIBILITY ExactVersion)install(FILES${CMAKE_CURRENT_BINARY_DIR}/TutorialConfigVersion.cmakeDESTINATIONlib/cmake/Tutorial)
对于Header-only库或纯脚本库等与处理器架构无关的软件包,可以使用 ARCH_INDEPENDENT。
这样生成的版本文件会声明该项目不依赖目标平台架构,从而允许不同架构共享同一份安装内容。
write_basic_package_version_file(TutorialConfigVersion.cmakeCOMPATIBILITY SameMajorVersionARCH_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.cmakeCOMPATIBILITY ExactVersion)# TODO12: Add the generated TutorialConfigVersion.cmake file to the file list of the TODO7 install() command.install(FILEScmake/TutorialConfig.cmake${CMAKE_CURRENT_BINARY_DIR}/TutorialConfigVersion.cmakeDESTINATION ${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















































































