15、CMake中的安装规则:install()命令详解
说实话,install() 这条命令,是我早期用 CMake 时最头疼的部分之一。为什么?因为编译本身很少出错,但一涉及到「把东西装到哪」这个问题,各种路径混乱、文件遗漏的问题就全冒出来了。我记得有一次,项目打包后跑起来说找不到动态库,查了半天,原来是 .so 文件装错了目录。
嗯,今天我们就来彻底搞懂 install()。说白了,它就是告诉 CMake:哪些文件要装走,装到哪里去,装完之后要不要执行点什么脚本。
15.1 install() 的核心作用
在 CMake 的世界里,install() 定义了「安装规则」。它本身不执行安装,而是生成安装脚本。当你运行 cmake --install . 或者 make install 时,这些规则才会生效。
我个人习惯把 install() 分成四类来记:
- TARGETS:安装目标(可执行文件、库)
- FILES:安装普通文件(头文件、配置文件)
- DIRECTORIES:安装整个目录
- SCRIPT:安装时执行自定义脚本
下面我们一个一个来看。
15.2 TARGETS:安装目标文件
这是最常用的安装方式。你编译出来的可执行文件、静态库、动态库,都靠它来装走。
add_executable(myapp main.cpp)
install(TARGETS myapp RUNTIME DESTINATION bin)
这里有个关键点:目标类型决定安装目录。CMake 把目标分为三类:
| 目标类型 | 关键字 | 典型目录 | 说明 |
|---|---|---|---|
| 可执行文件 | RUNTIME | bin/ | .exe、可执行脚本 |
| 动态库 | LIBRARY | lib/ 或 lib64/ | .so、.dylib |
| 静态库 | ARCHIVE | lib/ 或 lib64/ | .a、.lib |
你想想看,如果一个目标同时生成动态库和静态库,可以这样写:
add_library(mylib SHARED source1.cpp)
add_library(mylib_static STATIC source1.cpp)
install(TARGETS mylib mylib_static
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib/static
)
我在项目中遇到过一个问题:Windows 下动态库的 .dll 文件属于 RUNTIME,而 .lib 导入库属于 ARCHIVE。如果你只写了 LIBRARY DESTINATION,那 .dll 根本不会装过去。嗯,这个坑我踩过。
15.3 FILES:安装普通文件
头文件、配置文件、文档……这些不属于编译目标,用 FILES 来装。
install(FILES config.ini settings.json
DESTINATION etc/myapp
)
这里有个小细节:FILES 后面跟的是源文件路径(相对于当前 CMakeLists.txt),而 DESTINATION 是相对于安装前缀的路径。
我个人习惯把公共头文件单独装到一个 include/ 目录下:
install(FILES
${CMAKE_CURRENT_SOURCE_DIR}/include/mylib/api.h
${CMAKE_CURRENT_SOURCE_DIR}/include/mylib/defs.h
DESTINATION include/mylib
)
file(GLOB) 收集,但我建议还是显式列出,这样依赖关系更清晰。
15.4 DIRECTORIES:安装整个目录
有时候你需要把整个目录结构原封不动地装过去。比如资源文件、数据文件、HTML 页面等。
install(DIRECTORY resources/
DESTINATION share/myapp
)
注意这个斜杠 / 的用法:
- 带斜杠:
resources/—— 安装目录内容,不包含目录本身 - 不带斜杠:
resources—— 安装目录本身,即share/myapp/resources/
我曾经因为少写了一个斜杠,导致安装后多了一层目录,程序读不到资源文件。排查了半天才发现是这个问题。
DIRECTORIES 还支持 FILES_MATCHING 和 PATTERN 来过滤文件:
install(DIRECTORY src/
DESTINATION include
FILES_MATCHING
PATTERN "*.h"
PATTERN ".git" EXCLUDE
)
这个例子只安装 .h 头文件,同时排除 .git 目录。嗯,很实用。
15.5 SCRIPT:安装时执行自定义脚本
有些安装步骤不是简单的文件拷贝。比如:注册服务、生成配置文件、设置权限等。这时候就需要 SCRIPT 了。
install(SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/cmake/post_install.cmake)
这个脚本本身也是 CMake 语法,在安装过程中被 cmake -P 执行。你可以用它来做任何事:
# post_install.cmake
message(STATUS "Running post-install script...")
file(WRITE "${CMAKE_INSTALL_PREFIX}/etc/myapp/version.txt"
"Version: ${PROJECT_VERSION}\n")
install() 命令,因为安装规则已经解析完毕。它只适合做安装后的「收尾工作」。
另外还有 CODE 关键字,可以直接在 CMakeLists.txt 里写内联脚本:
install(CODE [[
message(STATUS "Installation complete!")
file(APPEND "${CMAKE_INSTALL_PREFIX}/install.log"
"Installed at ${CMAKE_INSTALL_PREFIX}\n")
]])
我个人觉得 CODE 适合简单的操作,复杂逻辑还是单独写个 .cmake 文件更清爽。
15.6 安装路径配置
所有 DESTINATION 都是相对于 CMAKE_INSTALL_PREFIX 的。这个变量默认值在 Unix 上是 /usr/local,在 Windows 上是 C:/Program Files/${PROJECT_NAME}。
你可以通过三种方式修改它:
- 命令行:
cmake -DCMAKE_INSTALL_PREFIX=/opt/myapp .. - CMakeLists.txt 中设置:
set(CMAKE_INSTALL_PREFIX "/opt/myapp" CACHE PATH "...") - 安装时指定:
cmake --install . --prefix /opt/myapp
我建议用第三种方式,因为这样不会污染 CMake 缓存,灵活性最高。
另外,GNUInstallDirs 模块提供了一组标准路径变量,可以帮你写出更规范的安装规则:
include(GNUInstallDirs)
install(TARGETS myapp
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(FILES api.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
这些变量会自动适配不同平台的习惯。比如 Linux 上 CMAKE_INSTALL_LIBDIR 可能是 lib64,而 macOS 上是 lib。省心不少。
好了,关于 install() 命令就讲到这里。下一章我们会聊聊 CPack 打包,到时候这些安装规则会直接变成安装包的内容。嗯,到时候见。