15、CMake中的安装规则:install()命令详解

说实话,install() 这条命令,是我早期用 CMake 时最头疼的部分之一。为什么?因为编译本身很少出错,但一涉及到「把东西装到哪」这个问题,各种路径混乱、文件遗漏的问题就全冒出来了。我记得有一次,项目打包后跑起来说找不到动态库,查了半天,原来是 .so 文件装错了目录。

嗯,今天我们就来彻底搞懂 install()。说白了,它就是告诉 CMake:哪些文件要装走,装到哪里去,装完之后要不要执行点什么脚本。

15.1 install() 的核心作用

在 CMake 的世界里,install() 定义了「安装规则」。它本身不执行安装,而是生成安装脚本。当你运行 cmake --install . 或者 make install 时,这些规则才会生效。

我个人习惯把 install() 分成四类来记:

  • TARGETS:安装目标(可执行文件、库)
  • FILES:安装普通文件(头文件、配置文件)
  • DIRECTORIES:安装整个目录
  • SCRIPT:安装时执行自定义脚本

下面我们一个一个来看。

install() 命令四大安装类型 install() TARGETS 可执行文件、库 FILES 头文件、配置文件 DIRECTORIES 整个目录递归安装 SCRIPT 自定义安装脚本 配合 CMAKE_INSTALL_PREFIX 控制安装根目录

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 根本不会装过去。嗯,这个坑我踩过。

重要提示: 对于 Windows 上的 DLL,必须同时指定 RUNTIME 和 ARCHIVE,缺一不可。

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_MATCHINGPATTERN 来过滤文件:

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")
注意: SCRIPT 中不能使用 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}

你可以通过三种方式修改它:

  1. 命令行cmake -DCMAKE_INSTALL_PREFIX=/opt/myapp ..
  2. CMakeLists.txt 中设置set(CMAKE_INSTALL_PREFIX "/opt/myapp" CACHE PATH "...")
  3. 安装时指定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() 的四个类型 —— TARGETS 装目标、FILES 装文件、DIRECTORIES 装目录、SCRIPT 装脚本。配合 CMAKE_INSTALL_PREFIX 和 GNUInstallDirs,就能写出跨平台的安装规则。

好了,关于 install() 命令就讲到这里。下一章我们会聊聊 CPack 打包,到时候这些安装规则会直接变成安装包的内容。嗯,到时候见。


公众号:蓝海资料掘金营,微信deep3321