导语
在前面几章中,我们已经掌握了如何组织项目结构、管理目标依赖、查找外部库以及控制编译链接过程。可以说,到这一步为止,我们讨论的都还局限于构建(Build)阶段——即如何把源代码变成可执行文件或库。然而,一个完整的软件交付流程不止于构建,还必须包括安装(Install)阶段。
安装的本质,是将构建产物(二进制、库、头文件、配置文件、资源等)按照一定的目录结构部署到目标系统中,使其可以被系统或其他项目使用。CMake 通过 install() 命令提供了强大且灵活的安装规则配置能力。从本节开始,我们将进入第六章,系统学习如何利用 CMake 实现专业级的安装、打包与发布。
本节作为安装篇的基础,将深入讲解 install 命令的各个子命令、目标分类安装、文件与目录部署、路径变量、组件化安装以及权限控制。请务必跟随代码示例亲手实践,因为这是你迈向软件发布的第一步。
install 命令概览与四种基本形式
CMake 的 install() 命令是一个重载命令,根据首个参数的不同,分为四种最常用的形式:
install(TARGETS ...)—— 安装构建目标(可执行文件、库等)install(FILES ...)—— 安装普通文件(配置文件、数据文件、头文件等)install(PROGRAMS ...)—— 安装可执行脚本或程序(与 FILES 类似,但默认赋予执行权限)install(DIRECTORY ...)—— 递归安装整个目录
此外,还有 install(SCRIPT ...) 和 install(CODE ...) 用于执行安装时脚本,我们稍后也会介绍。
一个值得注意的原则是:Modern CMake 强烈建议为每个需要交付的目标和文件显式编写 install 规则,而不是依赖构建目录的临时产物。这不仅让项目更专业,也是后续生成 CMake 配置包(Config Package)和 CPack 打包的基础。
目标安装:TARGETS 与产物分类
目标安装是最常见的需求。假设我们有一个可执行文件 myapp 和一个库 mylib,最基本的写法如下:
add_executable(myapp src/main.cpp)
add_library(mylib SHARED src/mylib.cpp)
install(TARGETS myapp mylib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
这里出现了三个关键分类关键字:RUNTIME、LIBRARY、ARCHIVE。理解它们的区别至关重要,因为在不同平台下,同一目标可能属于不同分类。
RUNTIME、LIBRARY、ARCHIVE、OBJECTS 的区别
- RUNTIME:指需要运行时加载的可执行文件。在 Windows 上,这包括
.exe和 DLL(因为 DLL 是运行时加载的);在 Unix 上,主要是可执行二进制。安装目录通常是bin。 - LIBRARY:指运行时库。在 Unix/Linux 上指
.so、.dylib;在 Windows 上,由于 DLL 属于 RUNTIME,此项主要指导入库(import library)或不参与运行时加载的库。安装目录通常是lib。 - ARCHIVE:指静态库(
.a、.lib)以及 Windows 上 DLL 对应的导入库(.lib)。这是开发者在链接阶段需要的文件。安装目录通常也是lib或单独放在lib/static。 - OBJECTS(CMake 3.9+):指目标编译生成的对象文件(
.o、.obj)。很少直接使用,但在某些需要将对象文件作为 SDK 分发的场景下很有用。
为了更清晰地展示平台差异,看一个更完整的示例:
add_library(mylib SHARED src/mylib.cpp)
add_library(mylib_static STATIC src/mylib.cpp)
install(TARGETS mylib mylib_static
RUNTIME DESTINATION bin # Windows: DLL 和 exe 放这里
LIBRARY DESTINATION lib # Unix: .so/.dylib;Windows: 留空或忽略
ARCHIVE DESTINATION lib # 静态库和 Windows 导入库
OBJECTS DESTINATION obj # 对象文件(极少用)
)
在 Windows 上构建上述项目时,mylib.dll 会被安装到 bin,而 mylib.lib(导入库)会被安装到 lib;mylib_static.lib 也会被安装到 lib。在 Linux 上,libmylib.so 进入 lib,而 libmylib_static.a 同样进入 lib。这种平台自适应正是 CMake 安装系统的强大之处。
头文件与 INCLUDES 目的地
对于库目标,除了二进制产物,通常还需要安装头文件。虽然头文件一般通过 install(FILES ...) 或 install(DIRECTORY ...) 安装,但 install(TARGETS) 也支持一个特殊的 INCLUDES DESTINATION 子句:
install(TARGETS mylib
EXPORT MyLibTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
INCLUDES DESTINATION 并不会真的把头文件复制过去,而是会在后续生成导出目标(Export Targets)时,将 include 路径自动写入目标的 INTERFACE_INCLUDE_DIRECTORIES 属性中。这对于创建可供其他项目通过 find_package 使用的配置文件非常关键。
文件安装:FILES 与 PROGRAMS
普通文件部署
当需要安装源代码树中的配置文件、数据文件或文档时,使用 install(FILES ...):
# 安装单个文件
install(FILES
"${CMAKE_CURRENT_SOURCE_DIR}/config/app.ini"
DESTINATION etc/myapp
)
# 安装多个头文件(不推荐逐个列举,目录安装更合适)
install(FILES
"${CMAKE_CURRENT_SOURCE_DIR}/include/mylib/core.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/mylib/utils.h"
DESTINATION include/mylib
)
DESTINATION 路径如果是相对路径,则相对于安装前缀(CMAKE_INSTALL_PREFIX)解析。如果是绝对路径,则直接使用。
可执行脚本与权限差异
install(PROGRAMS ...) 的语法与 FILES 完全一致,但区别在于:PROGRAMS 会默认给安装后的文件添加可执行权限。这非常适合安装 shell 脚本、Python 脚本或其他解释型可执行文件:
install(PROGRAMS
"${CMAKE_CURRENT_SOURCE_DIR}/scripts/myapp-cli.sh"
"${CMAKE_CURRENT_SOURCE_DIR}/scripts/launch.py"
DESTINATION bin
)
安装后,myapp-cli.sh 和 launch.py 将具有可执行权限,用户可以直接运行。如果你用 install(FILES ...) 安装脚本,则默认没有执行权限,用户需要手动 chmod +x。
目录安装:递归复制与过滤
当需要批量安装头文件、资源文件、着色器代码或文档目录时,install(DIRECTORY ...) 是最方便的选择。它会递归复制源目录的内容到目标位置。
install(DIRECTORY
"${CMAKE_CURRENT_SOURCE_DIR}/include/"
DESTINATION include
FILES_MATCHING PATTERN "*.h"
)
install(DIRECTORY
"${CMAKE_CURRENT_SOURCE_DIR}/assets/"
DESTINATION share/myapp/assets
)
需要注意源目录路径末尾的斜杠:"include/" 表示安装该目录内部的内容到 destination/include/;而 "include"(无斜杠)则会将整个 include 文件夹复制到 destination/include/include/,这通常不是你想要的。
文件过滤与排除
install(DIRECTORY) 支持通过 PATTERN、REGEX、EXCLUDE 等子句精确控制哪些文件应该被安装:
install(DIRECTORY
"${CMAKE_CURRENT_SOURCE_DIR}/src/shaders/"
DESTINATION share/myapp/shaders
PATTERN "*.glsl" # 只安装 .glsl 文件
PATTERN "*.tmp" EXCLUDE # 排除临时文件
PATTERN "internal_*" EXCLUDE # 排除内部文件
PERMISSIONS OWNER_READ OWNER_WRITE # 设置安装后的权限
)
更复杂的场景下,可以组合多个 PATTERN 子句。CMake 会按照它们在命令中出现的顺序依次匹配。例如,先排除所有 .svn 和 .git 目录是常见做法:
install(DIRECTORY
"${CMAKE_CURRENT_SOURCE_DIR}/resources/"
DESTINATION share/myapp/resources
PATTERN ".git" EXCLUDE
PATTERN ".svn" EXCLUDE
PATTERN "CMakeLists.txt" EXCLUDE
PATTERN "*.md" EXCLUDE
)
安装时脚本:SCRIPT 与 CODE
有时候,安装过程不仅仅是复制文件,还需要执行一些额外的逻辑,比如创建系统用户、生成动态配置文件、更新共享库缓存(ldconfig)或打印安装后提示信息。CMake 提供了两种在安装时执行 CMake 代码的方式:
install(CODE …)
直接在 install 命令中嵌入 CMake 代码字符串:
install(CODE "message(STATUS "Installation complete. Please set ENV var MYAPP_HOME to ${CMAKE_INSTALL_PREFIX}.")")
# 更复杂的代码:创建目录(虽然 install(DIRECTORY) 也能做,但这展示了能力)
install(CODE "
file(MAKE_DIRECTORY ${CMAKE_INSTALL_PREFIX}/var/log/myapp)
file(WRITE ${CMAKE_INSTALL_PREFIX}/var/log/myapp/.gitkeep "")
")
注意字符串中的变量需要使用转义形式 ${...},因为它们不是在配置时求值,而是在安装时(install 阶段)求值。
install(SCRIPT …)
对于复杂的安装后逻辑,更好的做法是将代码写入单独的 .cmake 脚本文件,然后在安装时调用:
# CMakeLists.txt
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/PostInstall.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/PostInstall.cmake"
@ONLY
)
install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/PostInstall.cmake")
配套脚本 PostInstall.cmake.in 可能如下:
# PostInstall.cmake.in
message(STATUS "Running post-install script...")
message(STATUS "Install prefix: @CMAKE_INSTALL_PREFIX@")
# 示例:在 Unix 上更新动态链接器缓存
if(UNIX AND NOT APPLE)
execute_process(
COMMAND ldconfig
RESULT_VARIABLE ldconfig_result
)
if(NOT ldconfig_result EQUAL 0)
message(WARNING "ldconfig failed, you may need to run it manually or set LD_LIBRARY_PATH")
endif()
endif()
# 示例:安装后打印使用说明
file(READ "@CMAKE_CURRENT_SOURCE_DIR@/USAGE.txt" usage_text)
message(STATUS "${usage_text}")
使用 configure_file(... @ONLY) 可以确保在配置阶段将 @VAR@ 变量替换为实际值,生成的脚本在安装时无需再解析 CMake 变量。
安装路径变量与 GNUInstallDirs
CMAKE_INSTALL_PREFIX
所有相对路径的 DESTINATION 都是基于 CMAKE_INSTALL_PREFIX 解析的。其默认值因平台而异:
- Unix/Linux/macOS:默认是
/usr/local - Windows:默认是
C:Program Files${PROJECT_NAME}或C:Program Files (x86)${PROJECT_NAME}
用户可以通过以下方式覆盖:
# 命令行指定前缀
cmake --install build --prefix /opt/myapp
# 或配置时指定
cmake -B build -DCMAKE_INSTALL_PREFIX=/opt/myapp
GNUInstallDirs 标准
手动硬编码 bin、lib、include 等路径在跨平台或遵循 Linux 发行版规范时可能不够严谨。CMake 提供了 GNUInstallDirs 模块,定义了一套标准的安装目录变量:
include(GNUInstallDirs)
install(TARGETS myapp
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # -> bin
)
install(TARGETS mylib
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} # -> lib 或 lib64
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(FILES mylib.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} # -> include
)
install(FILES myapp.1
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 # -> share/man/man1
)
install(FILES myapp.desktop
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications # -> share/applications
)
使用这些变量的好处是:在 64 位系统上,CMAKE_INSTALL_LIBDIR 可能是 lib64 而不是 lib;在 Debian 多架构系统中,它会自动适应正确的路径。这使得你的项目更容易被各大 Linux 发行版接受。
组件化安装:COMPONENT
大型项目通常不需要一次性安装所有内容。例如,终端用户只需要运行时(Runtime),而开发者需要头文件和静态库(Development),测试人员可能需要测试数据(Tests)。CMake 的 COMPONENT 机制允许将安装规则分组,从而实现选择性安装。
# Runtime 组件(终端用户必需)
install(TARGETS myapp
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT runtime
)
# Development 组件(开发者需要)
install(TARGETS mylib
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
COMPONENT dev
)
install(DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
COMPONENT dev
)
# Documentation 组件(可选)
install(FILES README.md LICENSE
DESTINATION ${CMAKE_INSTALL_DOCDIR}
COMPONENT docs
)
# 示例程序(可选)
install(TARGETS example1 example2
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT examples
)
定义组件后,在安装时可以指定只安装某个组件:
# 只安装运行时
cmake --install build --component runtime
# 只安装开发文件
cmake --install build --component dev
# 安装除文档外的所有组件(使用 CPack 时更常用)
组件化安装也是 CPack 打包的基础。后续章节我们会看到,CPack 可以为每个组件生成独立的安装包(如 .deb 中的 myapp-runtime.deb、myapp-dev.deb 等)。
安装权限与属性控制
默认情况下,CMake 安装的文件权限遵循构建系统的默认行为,通常是 644(文件)和 755(目录)。但在某些场景下,你需要显式控制权限,比如安装 setuid 二进制、限制配置文件的可读性,或确保脚本具有执行权限。
PERMISSIONS 子句
install(FILES ...)、install(PROGRAMS ...) 和 install(DIRECTORY ...) 都支持 PERMISSIONS 子句:
# 安装一个敏感配置文件,仅所有者可读写
install(FILES secrets.conf
DESTINATION etc/myapp
PERMISSIONS OWNER_READ OWNER_WRITE
)
# 安装 setuid 根权限工具(谨慎使用)
install(TARGETS myprivtool
RUNTIME DESTINATION sbin
PERMISSIONS OWNER_READ OWNER_EXECUTE OWNER_WRITE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE
SETUID
)
# 目录安装时设置默认权限
install(DIRECTORY logs/
DESTINATION var/log/myapp
DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE
)
可用的权限关键字包括:OWNER_READ、OWNER_WRITE、OWNER_EXECUTE、GROUP_READ、GROUP_WRITE、GROUP_EXECUTE、WORLD_READ、WORLD_WRITE、WORLD_EXECUTE、SETUID、SETGID。
安装后修改权限的注意事项
如果你在 install(TARGETS) 上尝试使用 PERMISSIONS,会发现它并不直接支持。这是因为目标产物的权限通常由构建系统本身决定。如果需要修改目标产物的权限,建议在安装后通过 install(CODE ...) 或 install(SCRIPT ...) 调用 file(CHMOD ...)(CMake 3.19+):
# CMake 3.19+
install(CODE "
file(CHMOD ${CMAKE_INSTALL_PREFIX}/bin/myapp
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE
)
")
综合示例:一个完整的安装配置
让我们把本节内容整合成一个贴近实战的示例。假设我们有一个简单的 C++ 项目,包含一个可执行文件、一个共享库、头文件、配置文件和脚本:
cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 1.2.3 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(GNUInstallDirs)
# 目标定义
add_library(mylib SHARED src/mylib.cpp)
target_include_directories(mylib PUBLIC
$
$
)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)
# 1. 目标安装(注意 EXPORT 为下节铺垫)
install(TARGETS myapp mylib
EXPORT MyAppTargets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
# 2. 头文件安装
install(DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
FILES_MATCHING PATTERN "*.h"
)
# 3. 配置文件安装
install(FILES config/myapp.ini
DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/myapp
COMPONENT runtime
)
# 4. 脚本安装(赋予执行权限)
install(PROGRAMS scripts/myapp-backup.sh
DESTINATION ${CMAKE_INSTALL_BINDIR}
COMPONENT runtime
)
# 5. 文档安装
install(FILES README.md LICENSE
DESTINATION ${CMAKE_INSTALL_DOCDIR}
COMPONENT docs
)
# 6. 安装时创建日志目录
install(CODE "
file(MAKE_DIRECTORY ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LOCALSTATEDIR}/log/myapp)
" COMPONENT runtime)
构建并测试安装:
cmake -B build -DCMAKE_INSTALL_PREFIX=$PWD/out
cmake --build build
cmake --install build
# 查看安装目录结构
tree out
预期输出结构:
out/
├── bin/
│ ├── myapp
│ └── myapp-backup.sh
├── include/
│ └── mylib.h
├── lib/
│ ├── libmylib.so -> libmylib.so.1
│ ├── libmylib.so.1 -> libmylib.so.1.2.3
│ └── libmylib.so.1.2.3
├── etc/
│ └── myapp/
│ └── myapp.ini
├── share/
│ ├── doc/
│ │ └── MyApp/
│ │ ├── README.md
│ │ └── LICENSE
│ └── ...
└── var/
└── log/
└── myapp/
小结
本节我们系统学习了 CMake 的安装规则配置,涵盖了以下核心知识点:
install(TARGETS)的四种产物分类:RUNTIME、LIBRARY、ARCHIVE、OBJECTS,以及平台差异。install(FILES)与install(PROGRAMS)的区别,后者自动赋予执行权限。install(DIRECTORY)的递归复制能力,以及通过PATTERN和EXCLUDE进行过滤。install(SCRIPT)和install(CODE)实现安装时自定义逻辑。- 使用
CMAKE_INSTALL_PREFIX和GNUInstallDirs规范安装路径。 - 通过
COMPONENT实现组件化安装,为后续打包奠定基础。 - 使用
PERMISSIONS控制文件和目录的访问权限。
掌握了安装规则,你的项目就已经具备了”可交付”的基本能力。但仅仅复制文件还不够——为了让其他 CMake 项目能够通过 find_package 找到并使用你的库,你需要创建导出目标(Export Targets)和包配置文件(Package Config Files)。这正是下一节 6.2 导出目标与配置包 将要深入探讨的内容。我们将学习如何生成 MyAppConfig.cmake,让你的库真正融入 CMake 生态。


没有回复内容