16、安装规则:install命令详解。如何安装目标、头文件、可执行文件,以及生成正确的导出目标。

说到CMake的安装规则,我刚开始接触时也觉得挺绕的。说白了,install()命令就是告诉CMake:编译完的东西,该往哪儿放。但如果你只是简单地把文件拷贝过去,那后续别人想用你的库,可就麻烦了。

我个人习惯把安装分成两类:一类是“临时安装”,就是开发阶段自己用;另一类是“正式安装”,要生成导出目标,让别人能通过find_package()找到你。今天咱们重点讲后者。

16.1 install() 命令的基本语法

先看最常用的几种形式:

# 安装目标(库、可执行文件)
install(TARGETS mylib myapp
    EXPORT MyLibTargets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
    INCLUDES DESTINATION include
)

# 安装头文件
install(DIRECTORY include/
    DESTINATION include
    FILES_MATCHING PATTERN "*.h"
)

# 安装可执行文件
install(TARGETS myapp
    RUNTIME DESTINATION bin
)

# 安装配置文件
install(FILES mylib-config.cmake
    DESTINATION lib/cmake/mylib
)

嗯,这里要注意:DESTINATION是相对于安装前缀的路径。默认前缀是/usr/local(Linux)或C:/Program Files(Windows)。你可以通过cmake --install . --prefix /my/path来指定。

16.2 安装目标:TARGETS 的细节

我在项目中遇到过一个问题:同一个目标,在不同平台上可能是共享库、静态库或可执行文件。CMake用LIBRARYARCHIVERUNTIME来区分:

关键字 适用平台 典型文件
LIBRARY Linux/macOS .so, .dylib
ARCHIVE 所有平台 .a, .lib(静态库或导入库)
RUNTIME 所有平台 可执行文件, .dll(Windows)
INCLUDES 所有平台 仅用于导出目标的头文件路径

举个例子:如果你在Windows上构建一个共享库,.dll会走RUNTIME.lib导入库会走ARCHIVE。我曾经因为没写RUNTIME DESTINATION,导致Windows下dll没被安装,折腾了半天才发现。

注意: 如果你只写了LIBRARY DESTINATION,在Windows上共享库的dll不会被安装!必须同时指定RUNTIME DESTINATION

16.3 安装头文件:DIRECTORY 与 FILES

头文件安装有两种方式:

  • DIRECTORY:安装整个目录,可以带模式匹配。我建议用这种方式,因为可以保留目录结构。
  • FILES:安装单个文件。适合安装配置文件或少量头文件。
# 推荐方式:安装 include/ 下所有 .h 文件,保留目录结构
install(DIRECTORY include/
    DESTINATION include
    FILES_MATCHING PATTERN "*.h"
    PATTERN "internal" EXCLUDE  # 排除内部头文件
)

# 不推荐:逐个文件安装,容易漏
install(FILES include/mylib.h include/mylib_utils.h
    DESTINATION include
)

你想想看,如果头文件有几十个,用FILES方式得多累?而且容易漏掉新增的文件。用DIRECTORY加模式匹配,一劳永逸。

小技巧:DIRECTORY后面加斜杠/,表示只安装目录下的内容,不安装目录本身。不加斜杠,会安装整个目录(包括目录名)。

16.4 生成正确的导出目标

这是最核心的部分。导出目标,说白了就是让别人能通过find_package(mylib)找到你的库,并且能直接使用你定义的目标名(比如mylib::mylib)。

要实现这个,需要三步:

  1. 在 install(TARGETS) 中指定 EXPORT:告诉CMake,这个安装要生成一个导出集合。
  2. 安装导出文件:用install(EXPORT)生成xxxTargets.cmake文件。
  3. 编写配置文件:写一个xxxConfig.cmakeinclude()上面的目标文件。

看个完整例子:

# 第一步:安装目标,并指定导出集合名
install(TARGETS mylib
    EXPORT MyLibTargets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
    INCLUDES DESTINATION include
)

# 第二步:安装导出文件
install(EXPORT MyLibTargets
    FILE mylibTargets.cmake
    NAMESPACE mylib::
    DESTINATION lib/cmake/mylib
)

# 第三步:安装配置文件
install(FILES mylibConfig.cmake
    DESTINATION lib/cmake/mylib
)

然后你的mylibConfig.cmake里只需要写一行:

include("${CMAKE_CURRENT_LIST_DIR}/mylibTargets.cmake")

这样,别人用的时候:

find_package(mylib REQUIRED)
target_link_libraries(myapp PRIVATE mylib::mylib)

是不是很清爽?

核心要点: 导出目标的关键是 EXPORT 关键字和 NAMESPACE。命名空间可以避免目标名冲突,是专业库的标配。

16.5 安装规则的整体流程

我画了一张图,帮你理清整个安装和导出的流程:

CMake 安装与导出流程 CMakeLists.txt cmake --build . cmake --install . lib/ 下的库文件 bin/ 下的可执行文件 include/ 下的头文件 (保留目录结构) lib/cmake/ 下的 xxxConfig.cmake 用户 find_package()

从图上可以看到,安装不仅仅是拷贝文件。真正专业的是右边那条线——生成CMake配置文件。这样你的库才能被其他项目优雅地使用。

16.6 避坑指南

我曾经踩过几个坑,分享给你:

  • 忘记指定 NAMESPACE:如果不加命名空间,用户用的时候就是mylib而不是mylib::mylib。虽然也能用,但不够规范,而且容易和别的库冲突。
  • 头文件路径不对:安装头文件时,如果DESTINATION写的是include/mylib,那用户引用时就要写#include <mylib/xxx.h>。我建议统一用include,然后在头文件里用子目录区分。
  • 忘了安装 PUBLIC_HEADER:如果你在add_library()里用PUBLIC_HEADER属性指定了头文件,记得在install(TARGETS)里加上PUBLIC_HEADER DESTINATION include
重要提醒: 导出目标时,install(EXPORT)生成的xxxTargets.cmake文件里,会记录所有目标的路径。如果你移动了安装位置,记得重新运行cmake --install

16.7 实战建议

我个人习惯在项目的根CMakeLists.txt里统一管理安装规则,而不是分散在各个子目录。这样一目了然,也方便后期维护。

另外,我建议你写一个cmake/install.cmake文件,把所有的安装逻辑放进去,然后在主CMakeLists.txtinclude()它。这样主文件更清爽,安装规则也容易复用。

嗯,关于安装规则,今天就聊到这儿。记住:安装不是终点,让别人能用才是。生成正确的导出目标,是专业CMake项目的标志。