4. 安装规则进阶:install()命令的深入使用
好,咱们继续往下走。这一章我要跟你聊聊 install() 命令的那些进阶玩法。说实话,很多人在 CMake 里写安装规则,翻来覆去就是 install(TARGETS ...) 那一套。但实际项目里,尤其是当你需要做复杂部署的时候,光靠这个远远不够。
我个人习惯把 install() 分成四类场景来理解:安装目标、安装文件、安装目录、以及执行脚本。咱们一个一个拆开讲。
4.1 TARGETS 与 FILES 的区别
先看最常用的两个:TARGETS 和 FILES。它们看起来都是「把东西放到安装目录」,但本质完全不同。
install(TARGETS ...) 是专门用来安装 CMake 目标的。什么叫目标?就是通过 add_executable()、add_library() 创建出来的东西。它会自动处理目标的各种产物——比如动态库的 .so 文件和 .so.1.0.0 符号链接,或者可执行文件的调试符号。你不需要手动指定这些。
举个例子:
add_library(myapp SHARED src/myapp.cpp)
install(TARGETS myapp
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
PUBLIC_HEADER DESTINATION include
)
这里我只写了一个目标名,CMake 会自动把 .so 扔到 lib,把可执行文件扔到 bin,把头文件扔到 include。嗯,这里要注意:PUBLIC_HEADER 需要你在目标上提前设置 set_target_properties(myapp PROPERTIES PUBLIC_HEADER "include/myapp.h")。
install(FILES ...) 就简单多了。它就是把文件原封不动地拷贝过去。没有符号链接,没有调试符号,没有任何「智能处理」。说白了,就是 cp 命令的 CMake 版本。
install(FILES config.json README.md
DESTINATION share/myapp
)
我在项目中遇到过一个问题:有人用 install(FILES ...) 去安装一个动态库,结果部署到客户机器上,库文件是有了,但符号链接全丢了。程序一运行就报错「找不到 libxxx.so」。这就是用错了命令。动态库必须用 TARGETS 来安装。
核心区别一句话总结:
TARGETS安装的是「构建产物」,CMake 知道它的结构和依赖FILES安装的是「普通文件」,CMake 只负责拷贝
4.2 DIRECTORY 的使用场景
接下来是 install(DIRECTORY ...)。这个命令我用的频率其实比 FILES 还高。为什么?因为真实项目里,你往往需要安装整个目录结构——比如配置文件目录、资源文件目录、或者第三方依赖的目录。
它的语法长这样:
install(DIRECTORY config/ resources/
DESTINATION share/myapp
FILES_MATCHING PATTERN "*.json" "*.xml"
PATTERN ".git" EXCLUDE
)
这里有几个关键点:
- 目录名后面的斜杠很重要:
config/表示安装目录下的内容,不包含 config 目录本身;config没有斜杠,就会把整个 config 目录带过去。我刚开始用的时候被这个坑过好几次。 FILES_MATCHING只匹配文件:它不会过滤目录。如果你想排除某些目录,用PATTERN ... EXCLUDE。- 支持通配符:
PATTERN "*.tmp" EXCLUDE可以排除所有临时文件。
我曾经接手过一个项目,它的资源文件有上千个,分布在几十个目录里。原来的维护者用 install(FILES ...) 一个一个列,每次新增文件都要改 CMakeLists.txt。我改成 install(DIRECTORY ...) 之后,整个安装规则从 200 行缩到了 5 行。你想想看,这维护成本差了多少。
我的建议:
如果你的资源文件目录结构稳定,直接用 DIRECTORY 安装整个目录。如果只有零星几个文件,用 FILES。如果既有文件又有目录,混着用也没问题——install() 命令可以写多次。
4.3 SCRIPT 的使用场景
最后是 install(SCRIPT ...) 和 install(CODE ...)。这两个命令允许你在安装过程中执行自定义的 CMake 脚本或代码。说白了,就是安装时的「钩子」。
什么时候需要它?我举几个真实场景:
- 安装完成后,需要生成一个配置文件,里面的路径是动态的
- 需要设置文件权限,比如把某个脚本设为可执行
- 需要创建符号链接,但
TARGETS的自动符号链接满足不了你的需求 - 需要执行外部命令,比如运行
ldconfig刷新动态库缓存
看个例子:
# 安装后生成配置文件
install(CODE [[
set(CONFIG_PATH "${CMAKE_INSTALL_PREFIX}/etc/myapp.conf")
file(WRITE ${CONFIG_PATH}
"[myapp]\n"
"data_dir=${CMAKE_INSTALL_PREFIX}/share/myapp\n"
"log_dir=${CMAKE_INSTALL_PREFIX}/var/log\n"
)
]])
# 或者从外部脚本文件加载
install(SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/post_install.cmake")
CODE 后面跟的是 CMake 代码字符串,SCRIPT 后面跟的是 .cmake 文件路径。我个人更推荐用 SCRIPT,因为代码写在单独文件里,更容易维护和测试。
避坑指南:
我曾经在 install(CODE ...) 里写了一个 file(REMOVE ...) 操作,想清理一些临时文件。结果因为路径写错了,把客户机器上的重要配置文件给删了。嗯,从那以后我定了一个规矩:SCRIPT 里的代码,必须做路径检查。比如先判断文件是否存在,再判断路径是否包含预期字符串。
4.4 四种安装方式的对比
为了方便你快速选择,我整理了一张表:
| 命令 | 适用场景 | 自动处理 | 典型用途 |
|---|---|---|---|
install(TARGETS) |
安装构建目标(库、可执行文件) | 符号链接、调试符号、RPATH | 动态库、静态库、可执行文件 |
install(FILES) |
安装单个文件 | 无 | 配置文件、文档、许可证 |
install(DIRECTORY) |
安装整个目录结构 | 目录递归、模式匹配 | 资源文件、头文件、数据文件 |
install(SCRIPT/CODE) |
安装时执行自定义操作 | 无(完全自定义) | 生成配置、设置权限、执行命令 |
4.5 组合使用的实战技巧
实际项目中,这四种方式经常组合使用。我给你看一个我常用的模式:
# 1. 安装主程序
install(TARGETS myapp
RUNTIME DESTINATION bin
)
# 2. 安装动态库(带版本号)
install(TARGETS mylib
LIBRARY DESTINATION lib
PUBLIC_HEADER DESTINATION include
)
# 3. 安装资源文件
install(DIRECTORY assets/ config/
DESTINATION share/myapp
FILES_MATCHING PATTERN "*"
PATTERN ".svn" EXCLUDE
)
# 4. 安装后执行初始化
install(CODE [[
message(STATUS "Post-install: setting up environment...")
execute_process(
COMMAND ldconfig ${CMAKE_INSTALL_PREFIX}/lib
)
]])
这个模式的好处是:每个 install() 只做一件事,职责清晰。而且 CPack 在打包时,会按照你写的顺序依次执行这些安装规则。所以如果你有依赖关系,比如先装库再装程序,顺序一定要写对。
好了,这一章的内容就这些。下一章我会讲 CPack 的生成器选择——什么时候用 TGZ,什么时候用 DEB,什么时候用 NSIS。这些都是我在实际部署中踩过的坑,到时候跟你细聊。