21. 6.1 安装规则配置

导语

在前面几章中,我们已经掌握了如何组织项目结构、管理目标依赖、查找外部库以及控制编译链接过程。可以说,到这一步为止,我们讨论的都还局限于构建(Build)阶段——即如何把源代码变成可执行文件或库。然而,一个完整的软件交付流程不止于构建,还必须包括安装(Install)阶段。

安装的本质,是将构建产物(二进制、库、头文件、配置文件、资源等)按照一定的目录结构部署到目标系统中,使其可以被系统或其他项目使用。CMake 通过 install() 命令提供了强大且灵活的安装规则配置能力。从本节开始,我们将进入第六章,系统学习如何利用 CMake 实现专业级的安装、打包与发布。

本节作为安装篇的基础,将深入讲解 install 命令的各个子命令、目标分类安装、文件与目录部署、路径变量、组件化安装以及权限控制。请务必跟随代码示例亲手实践,因为这是你迈向软件发布的第一步。

install 命令概览与四种基本形式

CMake 的 install() 命令是一个重载命令,根据首个参数的不同,分为四种最常用的形式:

  1. install(TARGETS ...) —— 安装构建目标(可执行文件、库等)
  2. install(FILES ...) —— 安装普通文件(配置文件、数据文件、头文件等)
  3. install(PROGRAMS ...) —— 安装可执行脚本或程序(与 FILES 类似,但默认赋予执行权限)
  4. install(DIRECTORY ...) —— 递归安装整个目录

此外,还有 install(SCRIPT ...)install(CODE ...) 用于执行安装时脚本,我们稍后也会介绍。

一个值得注意的原则是:Modern CMake 强烈建议为每个需要交付的目标和文件显式编写 install 规则,而不是依赖构建目录的临时产物。这不仅让项目更专业,也是后续生成 CMake 配置包(Config Package)和 CPack 打包的基础。

目标安装:TARGETS 与产物分类

目标安装是最常见的需求。假设我们有一个可执行文件 myapp 和一个库 mylib,最基本的写法如下:

add_executable(myapp src/main.cpp)
add_library(mylib SHARED src/mylib.cpp)

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

这里出现了三个关键分类关键字:RUNTIMELIBRARYARCHIVE。理解它们的区别至关重要,因为在不同平台下,同一目标可能属于不同分类。

RUNTIME、LIBRARY、ARCHIVE、OBJECTS 的区别

  • RUNTIME:指需要运行时加载的可执行文件。在 Windows 上,这包括 .exe 和 DLL(因为 DLL 是运行时加载的);在 Unix 上,主要是可执行二进制。安装目录通常是 bin
  • LIBRARY:指运行时库。在 Unix/Linux 上指 .so.dylib;在 Windows 上,由于 DLL 属于 RUNTIME,此项主要指导入库(import library)或不参与运行时加载的库。安装目录通常是 lib
  • ARCHIVE:指静态库(.a.lib)以及 Windows 上 DLL 对应的导入库(.lib)。这是开发者在链接阶段需要的文件。安装目录通常也是 lib 或单独放在 lib/static
  • OBJECTS(CMake 3.9+):指目标编译生成的对象文件(.o.obj)。很少直接使用,但在某些需要将对象文件作为 SDK 分发的场景下很有用。

为了更清晰地展示平台差异,看一个更完整的示例:

add_library(mylib SHARED src/mylib.cpp)
add_library(mylib_static STATIC src/mylib.cpp)

install(TARGETS mylib mylib_static
    RUNTIME DESTINATION bin                # Windows: DLL 和 exe 放这里
    LIBRARY DESTINATION lib                # Unix: .so/.dylib;Windows: 留空或忽略
    ARCHIVE DESTINATION lib                # 静态库和 Windows 导入库
    OBJECTS DESTINATION obj                # 对象文件(极少用)
)

在 Windows 上构建上述项目时,mylib.dll 会被安装到 bin,而 mylib.lib(导入库)会被安装到 libmylib_static.lib 也会被安装到 lib。在 Linux 上,libmylib.so 进入 lib,而 libmylib_static.a 同样进入 lib。这种平台自适应正是 CMake 安装系统的强大之处。

头文件与 INCLUDES 目的地

对于库目标,除了二进制产物,通常还需要安装头文件。虽然头文件一般通过 install(FILES ...)install(DIRECTORY ...) 安装,但 install(TARGETS) 也支持一个特殊的 INCLUDES DESTINATION 子句:

install(TARGETS mylib
    EXPORT MyLibTargets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
    INCLUDES DESTINATION include
)

INCLUDES DESTINATION 并不会真的把头文件复制过去,而是会在后续生成导出目标(Export Targets)时,将 include 路径自动写入目标的 INTERFACE_INCLUDE_DIRECTORIES 属性中。这对于创建可供其他项目通过 find_package 使用的配置文件非常关键。

文件安装:FILES 与 PROGRAMS

普通文件部署

当需要安装源代码树中的配置文件、数据文件或文档时,使用 install(FILES ...)

# 安装单个文件
install(FILES 
    "${CMAKE_CURRENT_SOURCE_DIR}/config/app.ini"
    DESTINATION etc/myapp
)

# 安装多个头文件(不推荐逐个列举,目录安装更合适)
install(FILES 
    "${CMAKE_CURRENT_SOURCE_DIR}/include/mylib/core.h"
    "${CMAKE_CURRENT_SOURCE_DIR}/include/mylib/utils.h"
    DESTINATION include/mylib
)

DESTINATION 路径如果是相对路径,则相对于安装前缀(CMAKE_INSTALL_PREFIX)解析。如果是绝对路径,则直接使用。

可执行脚本与权限差异

install(PROGRAMS ...) 的语法与 FILES 完全一致,但区别在于:PROGRAMS 会默认给安装后的文件添加可执行权限。这非常适合安装 shell 脚本、Python 脚本或其他解释型可执行文件:

install(PROGRAMS 
    "${CMAKE_CURRENT_SOURCE_DIR}/scripts/myapp-cli.sh"
    "${CMAKE_CURRENT_SOURCE_DIR}/scripts/launch.py"
    DESTINATION bin
)

安装后,myapp-cli.shlaunch.py 将具有可执行权限,用户可以直接运行。如果你用 install(FILES ...) 安装脚本,则默认没有执行权限,用户需要手动 chmod +x

目录安装:递归复制与过滤

当需要批量安装头文件、资源文件、着色器代码或文档目录时,install(DIRECTORY ...) 是最方便的选择。它会递归复制源目录的内容到目标位置。

install(DIRECTORY 
    "${CMAKE_CURRENT_SOURCE_DIR}/include/"
    DESTINATION include
    FILES_MATCHING PATTERN "*.h"
)

install(DIRECTORY 
    "${CMAKE_CURRENT_SOURCE_DIR}/assets/"
    DESTINATION share/myapp/assets
)

需要注意源目录路径末尾的斜杠:"include/" 表示安装该目录内部的内容到 destination/include/;而 "include"(无斜杠)则会将整个 include 文件夹复制到 destination/include/include/,这通常不是你想要的。

文件过滤与排除

install(DIRECTORY) 支持通过 PATTERNREGEXEXCLUDE 等子句精确控制哪些文件应该被安装:

install(DIRECTORY 
    "${CMAKE_CURRENT_SOURCE_DIR}/src/shaders/"
    DESTINATION share/myapp/shaders
    PATTERN "*.glsl"                         # 只安装 .glsl 文件
    PATTERN "*.tmp" EXCLUDE                  # 排除临时文件
    PATTERN "internal_*" EXCLUDE             # 排除内部文件
    PERMISSIONS OWNER_READ OWNER_WRITE       # 设置安装后的权限
)

更复杂的场景下,可以组合多个 PATTERN 子句。CMake 会按照它们在命令中出现的顺序依次匹配。例如,先排除所有 .svn.git 目录是常见做法:

install(DIRECTORY 
    "${CMAKE_CURRENT_SOURCE_DIR}/resources/"
    DESTINATION share/myapp/resources
    PATTERN ".git" EXCLUDE
    PATTERN ".svn" EXCLUDE
    PATTERN "CMakeLists.txt" EXCLUDE
    PATTERN "*.md" EXCLUDE
)

安装时脚本:SCRIPT 与 CODE

有时候,安装过程不仅仅是复制文件,还需要执行一些额外的逻辑,比如创建系统用户、生成动态配置文件、更新共享库缓存(ldconfig)或打印安装后提示信息。CMake 提供了两种在安装时执行 CMake 代码的方式:

install(CODE …)

直接在 install 命令中嵌入 CMake 代码字符串:

install(CODE "message(STATUS "Installation complete. Please set ENV var MYAPP_HOME to ${CMAKE_INSTALL_PREFIX}.")")

# 更复杂的代码:创建目录(虽然 install(DIRECTORY) 也能做,但这展示了能力)
install(CODE "
    file(MAKE_DIRECTORY ${CMAKE_INSTALL_PREFIX}/var/log/myapp)
    file(WRITE ${CMAKE_INSTALL_PREFIX}/var/log/myapp/.gitkeep "")
")

注意字符串中的变量需要使用转义形式 ${...},因为它们不是在配置时求值,而是在安装时(install 阶段)求值。

install(SCRIPT …)

对于复杂的安装后逻辑,更好的做法是将代码写入单独的 .cmake 脚本文件,然后在安装时调用:

# CMakeLists.txt
configure_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/cmake/PostInstall.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/PostInstall.cmake"
    @ONLY
)

install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/PostInstall.cmake")

配套脚本 PostInstall.cmake.in 可能如下:

# PostInstall.cmake.in
message(STATUS "Running post-install script...")
message(STATUS "Install prefix: @CMAKE_INSTALL_PREFIX@")

# 示例:在 Unix 上更新动态链接器缓存
if(UNIX AND NOT APPLE)
    execute_process(
        COMMAND ldconfig
        RESULT_VARIABLE ldconfig_result
    )
    if(NOT ldconfig_result EQUAL 0)
        message(WARNING "ldconfig failed, you may need to run it manually or set LD_LIBRARY_PATH")
    endif()
endif()

# 示例:安装后打印使用说明
file(READ "@CMAKE_CURRENT_SOURCE_DIR@/USAGE.txt" usage_text)
message(STATUS "${usage_text}")

使用 configure_file(... @ONLY) 可以确保在配置阶段将 @VAR@ 变量替换为实际值,生成的脚本在安装时无需再解析 CMake 变量。

安装路径变量与 GNUInstallDirs

CMAKE_INSTALL_PREFIX

所有相对路径的 DESTINATION 都是基于 CMAKE_INSTALL_PREFIX 解析的。其默认值因平台而异:

  • Unix/Linux/macOS:默认是 /usr/local
  • Windows:默认是 C:Program Files${PROJECT_NAME}C:Program Files (x86)${PROJECT_NAME}

用户可以通过以下方式覆盖:

# 命令行指定前缀
cmake --install build --prefix /opt/myapp

# 或配置时指定
cmake -B build -DCMAKE_INSTALL_PREFIX=/opt/myapp

GNUInstallDirs 标准

手动硬编码 binlibinclude 等路径在跨平台或遵循 Linux 发行版规范时可能不够严谨。CMake 提供了 GNUInstallDirs 模块,定义了一套标准的安装目录变量:

include(GNUInstallDirs)

install(TARGETS myapp
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}        # -> bin
)

install(TARGETS mylib
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}        # -> lib 或 lib64
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

install(FILES mylib.h 
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}            # -> include
)

install(FILES myapp.1
    DESTINATION ${CMAKE_INSTALL_MANDIR}/man1           # -> share/man/man1
)

install(FILES myapp.desktop
    DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications  # -> share/applications
)

使用这些变量的好处是:在 64 位系统上,CMAKE_INSTALL_LIBDIR 可能是 lib64 而不是 lib;在 Debian 多架构系统中,它会自动适应正确的路径。这使得你的项目更容易被各大 Linux 发行版接受。

组件化安装:COMPONENT

大型项目通常不需要一次性安装所有内容。例如,终端用户只需要运行时(Runtime),而开发者需要头文件和静态库(Development),测试人员可能需要测试数据(Tests)。CMake 的 COMPONENT 机制允许将安装规则分组,从而实现选择性安装。

# Runtime 组件(终端用户必需)
install(TARGETS myapp
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    COMPONENT runtime
)

# Development 组件(开发者需要)
install(TARGETS mylib
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    COMPONENT dev
)
install(DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
    COMPONENT dev
)

# Documentation 组件(可选)
install(FILES README.md LICENSE
    DESTINATION ${CMAKE_INSTALL_DOCDIR}
    COMPONENT docs
)

# 示例程序(可选)
install(TARGETS example1 example2
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    COMPONENT examples
)

定义组件后,在安装时可以指定只安装某个组件:

# 只安装运行时
cmake --install build --component runtime

# 只安装开发文件
cmake --install build --component dev

# 安装除文档外的所有组件(使用 CPack 时更常用)

组件化安装也是 CPack 打包的基础。后续章节我们会看到,CPack 可以为每个组件生成独立的安装包(如 .deb 中的 myapp-runtime.debmyapp-dev.deb 等)。

安装权限与属性控制

默认情况下,CMake 安装的文件权限遵循构建系统的默认行为,通常是 644(文件)和 755(目录)。但在某些场景下,你需要显式控制权限,比如安装 setuid 二进制、限制配置文件的可读性,或确保脚本具有执行权限。

PERMISSIONS 子句

install(FILES ...)install(PROGRAMS ...)install(DIRECTORY ...) 都支持 PERMISSIONS 子句:

# 安装一个敏感配置文件,仅所有者可读写
install(FILES secrets.conf
    DESTINATION etc/myapp
    PERMISSIONS OWNER_READ OWNER_WRITE
)

# 安装 setuid 根权限工具(谨慎使用)
install(TARGETS myprivtool
    RUNTIME DESTINATION sbin
    PERMISSIONS OWNER_READ OWNER_EXECUTE OWNER_WRITE
                  GROUP_READ GROUP_EXECUTE
                  WORLD_READ WORLD_EXECUTE
                  SETUID
)

# 目录安装时设置默认权限
install(DIRECTORY logs/
    DESTINATION var/log/myapp
    DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
                            GROUP_READ GROUP_EXECUTE
                            WORLD_READ WORLD_EXECUTE
)

可用的权限关键字包括:OWNER_READOWNER_WRITEOWNER_EXECUTEGROUP_READGROUP_WRITEGROUP_EXECUTEWORLD_READWORLD_WRITEWORLD_EXECUTESETUIDSETGID

安装后修改权限的注意事项

如果你在 install(TARGETS) 上尝试使用 PERMISSIONS,会发现它并不直接支持。这是因为目标产物的权限通常由构建系统本身决定。如果需要修改目标产物的权限,建议在安装后通过 install(CODE ...)install(SCRIPT ...) 调用 file(CHMOD ...)(CMake 3.19+):

# CMake 3.19+
install(CODE "
    file(CHMOD ${CMAKE_INSTALL_PREFIX}/bin/myapp
         PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
                     GROUP_READ GROUP_EXECUTE
                     WORLD_READ WORLD_EXECUTE
    )
")

综合示例:一个完整的安装配置

让我们把本节内容整合成一个贴近实战的示例。假设我们有一个简单的 C++ 项目,包含一个可执行文件、一个共享库、头文件、配置文件和脚本:

cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 1.2.3 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(GNUInstallDirs)

# 目标定义
add_library(mylib SHARED src/mylib.cpp)
target_include_directories(mylib PUBLIC
    $
    $
)

add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)

# 1. 目标安装(注意 EXPORT 为下节铺垫)
install(TARGETS myapp mylib
    EXPORT MyAppTargets
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

# 2. 头文件安装
install(DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
    FILES_MATCHING PATTERN "*.h"
)

# 3. 配置文件安装
install(FILES config/myapp.ini
    DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/myapp
    COMPONENT runtime
)

# 4. 脚本安装(赋予执行权限)
install(PROGRAMS scripts/myapp-backup.sh
    DESTINATION ${CMAKE_INSTALL_BINDIR}
    COMPONENT runtime
)

# 5. 文档安装
install(FILES README.md LICENSE
    DESTINATION ${CMAKE_INSTALL_DOCDIR}
    COMPONENT docs
)

# 6. 安装时创建日志目录
install(CODE "
    file(MAKE_DIRECTORY ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LOCALSTATEDIR}/log/myapp)
" COMPONENT runtime)

构建并测试安装:

cmake -B build -DCMAKE_INSTALL_PREFIX=$PWD/out
cmake --build build
cmake --install build

# 查看安装目录结构
tree out

预期输出结构:

out/
├── bin/
│   ├── myapp
│   └── myapp-backup.sh
├── include/
│   └── mylib.h
├── lib/
│   ├── libmylib.so -> libmylib.so.1
│   ├── libmylib.so.1 -> libmylib.so.1.2.3
│   └── libmylib.so.1.2.3
├── etc/
│   └── myapp/
│       └── myapp.ini
├── share/
│   ├── doc/
│   │   └── MyApp/
│   │       ├── README.md
│   │       └── LICENSE
│   └── ...
└── var/
    └── log/
        └── myapp/

小结

本节我们系统学习了 CMake 的安装规则配置,涵盖了以下核心知识点:

  • install(TARGETS) 的四种产物分类:RUNTIMELIBRARYARCHIVEOBJECTS,以及平台差异。
  • install(FILES)install(PROGRAMS) 的区别,后者自动赋予执行权限。
  • install(DIRECTORY) 的递归复制能力,以及通过 PATTERNEXCLUDE 进行过滤。
  • install(SCRIPT)install(CODE) 实现安装时自定义逻辑。
  • 使用 CMAKE_INSTALL_PREFIXGNUInstallDirs 规范安装路径。
  • 通过 COMPONENT 实现组件化安装,为后续打包奠定基础。
  • 使用 PERMISSIONS 控制文件和目录的访问权限。

掌握了安装规则,你的项目就已经具备了”可交付”的基本能力。但仅仅复制文件还不够——为了让其他 CMake 项目能够通过 find_package 找到并使用你的库,你需要创建导出目标(Export Targets)包配置文件(Package Config Files)。这正是下一节 6.2 导出目标与配置包 将要深入探讨的内容。我们将学习如何生成 MyAppConfig.cmake,让你的库真正融入 CMake 生态。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……