21、安装规则:install()命令详解、TARGETS/FILES/DIRECTORY/SCRIPT安装、安装权限与配置、导出目标

说实话,安装规则这块内容,很多C++开发者写CMake时都是「能用就行」。我以前也这么干——写个简单的install(TARGETS ...)就完事了。直到有一次,我需要把一个库部署到客户的嵌入式环境里,才发现安装规则里藏着不少门道。

今天我就把install()命令的几种常见用法,以及导出目标这套机制,给你掰开揉碎了讲清楚。

21.1 install() 命令的四种安装模式

install()命令说白了就四种模式:安装目标、安装文件、安装目录、安装脚本。我习惯把它们叫做「四大金刚」。咱们一个一个来看。

21.1.1 TARGETS 安装:最常用的模式

这是你平时用得最多的。把编译好的库、可执行文件安装到指定位置。

install(TARGETS mylib myapp
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib/static
)

这里有个细节我提醒你注意:RUNTIME、LIBRARY、ARCHIVE 这三个关键字,分别对应可执行文件、共享库、静态库。你想想看,如果只写一个DESTINATION,那所有类型的目标都塞到同一个目录里,是不是有点乱?

我的习惯: 即使项目只有一种目标类型,我也会显式写出对应的关键字。这样别人看CMakeLists.txt时,一眼就知道这个目标是什么类型。

21.1.2 FILES 安装:安装配置文件

有时候你不需要安装整个目标,只想把几个配置文件、头文件或者资源文件放过去。这时候用FILES模式。

install(FILES config.ini logo.png
    DESTINATION share/myapp
    PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ
)

嗯,这里要注意权限设置。我在项目中遇到过一个问题:安装后的配置文件只有root能读,普通用户一运行就报错。后来我加上了WORLD_READ权限才解决。

21.1.3 DIRECTORY 安装:整个目录搬过去

如果你有一整个目录的资源文件要安装,比如assets/data/,用DIRECTORY模式最方便。

install(DIRECTORY assets/ data/
    DESTINATION share/myapp
    FILES_MATCHING PATTERN "*.png" PATTERN "*.json"
    PATTERN ".git" EXCLUDE
)

这里有个坑:目录名后面加不加斜杠,行为完全不同。加斜杠表示「安装目录里的内容」,不加斜杠表示「安装目录本身」。我曾经因为这个搞错过,结果安装出来的目录结构多了一层。

21.1.4 SCRIPT 安装:执行自定义脚本

有些安装步骤没法用CMake的标准模式搞定,比如生成一个符号链接、修改配置文件里的路径。这时候就用SCRIPT模式,执行一个CMake脚本。

install(SCRIPT cmake/post_install.cmake)

我个人觉得,SCRIPT模式是最后的杀手锏。能用前三种模式解决的问题,尽量别用脚本。因为脚本里的逻辑没法被CMake的安装系统「理解」,后续做卸载、打包时容易出问题。

21.2 安装权限与配置

安装权限这块,很多人直接忽略。但如果你要把软件部署到生产环境,或者做成deb/rpm包,权限就很重要了。

CMake支持标准的Unix权限设置:

install(TARGETS mydaemon
    RUNTIME DESTINATION sbin
    PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
                GROUP_READ GROUP_EXECUTE
                WORLD_EXECUTE
)

上面这个例子,我给守护进程设置了755权限——所有者可读写执行,组和其他用户只能读和执行。为什么没有WORLD_WRITE?你想想看,一个系统守护进程如果谁都能写,那还得了?

避坑指南: 我曾经把一个库安装成了644权限(没有执行位),结果链接器报「无法加载共享库」。折腾了半天才发现是权限问题。共享库必须有WORLD_EXECUTE权限才能被加载。

21.3 导出目标:让其他项目找到你

导出目标(Export)是CMake里一个非常强大的机制。说白了,就是让你的库安装后,其他项目用find_package()就能找到。

21.3.1 基本导出流程

导出目标需要三步走:

  1. 给目标设置 EXPORT 名称
  2. 安装导出文件
  3. 生成并安装配置文件
# 第一步:安装目标时指定导出集
install(TARGETS mylib
    EXPORT MyLibTargets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
)

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

# 第三步:生成配置文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
    cmake/MyLibConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
    INSTALL_DESTINATION lib/cmake/MyLib
)
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
    DESTINATION lib/cmake/MyLib
)

你看,这三步走完,其他项目就可以这样用了:

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

21.3.2 命名空间的重要性

我注意到很多开源项目都用MyLib::这种命名空间。这不是花架子,而是有实际意义的。命名空间可以避免目标名冲突——万一你的库和另一个库都叫core,没有命名空间就乱套了。

我的建议: 所有导出的目标都加上命名空间。这不仅是规范,更是对使用你库的开发者负责。

21.4 知识体系总览

下面这张图,我把安装规则的核心逻辑梳理了一下。你可以对照着看,心里有个全局观。

install() 命令知识体系 install() 命令 TARGETS 安装 FILES 安装 DIRECTORY 安装 SCRIPT 安装 RUNTIME/LIBRARY/ARCHIVE EXPORT 导出集 PERMISSIONS 权限 配置文件安装 FILES_MATCHING 过滤 PATTERN EXCLUDE 排除 自定义安装逻辑 导出目标 (Export) install(EXPORT ...) + 配置文件生成

21.5 实战中的一些建议

最后,我分享几个实战中总结出来的经验:

  • 安装路径要灵活:别写死路径,用CMAKE_INSTALL_PREFIX变量。用户安装时可以指定-DCMAKE_INSTALL_PREFIX=/usr/local
  • 组件化安装:如果你的项目有多个组件(比如运行时库、开发头文件、文档),用COMPONENT关键字分组。这样用户可以用cmake --install . --component dev只安装开发组件。
  • 测试安装:在CI里加一个步骤,把项目安装到临时目录,然后编译一个测试程序链接安装后的库。这样可以提前发现安装问题。

嗯,关于安装规则,今天就聊到这里。这些内容看起来琐碎,但每个细节都可能成为你部署时的绊脚石。多写几个install()命令,多跑几次安装测试,慢慢就熟练了。


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