chapter11 - installing and packaging

Exporting without installation

使用 export 命令可以创建导出文件,然后其他工程只要 include 这个导出文件就能使用当前这个包中的 target,而不需要先将这个包用 install 安装在系统里。

export(TARGETS [target1 [target2 [...]]]
      [NAMESPACE <namespace>] [APPEND] FILE <path>
      [EXPORT_LINK_INTERFACE_LIBRARIES]

NAMESPACE is recommended as a hint, stating that the target has been imported from other projects.

APPEND tells CMake that it shouldn’t erase the contents of the file before writing. 也就是说本身会按照 FILE 来覆写文件,但是 APPEND 使得写入方式变成追加。

EXPORT_LINK_INTERFACE_LIBRARIES will export target link dependencies (including imported and config-specific variants).

例子:

cmake_minimum_required(VERSION 3.20.0)
project(ExportCalcCXX)
add_subdirectory(src bin)
set(EXPORT_DIR "${CMAKE_CURRENT_BINARY_DIR}/cmake")
export(TARGETS calc
  FILE "${EXPORT_DIR}/CalcTargets.cmake"
  NAMESPACE Calc::
)
...

这段代码将导出文件生成在了 binary tree 里面,这样的文件不会污染系统,但是可以被其他工程包含进去。还要注意 CMake 在 build tree 中导出的 target export file 里用的是绝对路径,所以不能轻易移动。上面代码还用到了一个名字空间 Calc::,因而创建的 target export file 中声明的 target 是 Calc::calc

export 命令还有一个形式:export(EXPORT <export> [NAMESPACE <namespace>] [FILE <path>])。这个形式不需要 targets,但是需要一个之前创建的 export 的名字。

However, it requires a <export> name rather than a list of exported targets. Such <export> instances are named lists of targets that are defined by install(TARGETS)

export 命令在 build tree 的生成阶段就会导出 target export file。使用 ${CMAKE_INSTALL_LIBDIR} 作为前缀是安全的(更保险一点的是先 include(GNUInstallDirs)),因为它是一个相对路径,生成阶段的相对位置是 build tree,所以不会真正安装到系统里面去。

cmake --install

cmake --install 接受这些参数(它们的参数都是前面不加等号的):

  1. --config:对多配置生成器有用。
  2. --component:选择要安装的部分(后面讲)。
  3. --default-directory-permissions:设置安装目录的权限(u=rwx,g=rx,o=rx)。
  4. --prefix:设置 CMAKE_INSTALL_PREFIX 的值(对 Windows 来说默认为 c:/Program Files/${PROJECT_NAME},对 unix 来说默认为 /usr/local)。
  5. -v, --verbose

install() 命令

  • install(TARGETS): This installs output artifacts such as libraries and executables.
  • install(FILES|PROGRAMS): This installs individual files and sets their permissions. 这两命令非常相似,但是设置的默认权限不同,显然 PROGRAMS 会设置写权限。
  • install(DIRECTORY): This installs whole directories.
  • install(SCRIPT|CODE): This runs a CMake script or a snippet during installation.
  • install(EXPORT): This generates and installs a target export file.

install 命令可以指定 DESTINATION/PERMISSIONS/CONFIGURATIONS/OPTIONAL 等参数。在 COMPONENT 模式下,还可以指定 EXCLUDE_FROM_ALL。如果给定的安装位置是相对路径,那么实际上的安装位置是 ${CMAKE_INSTALL_PREFIX} + ${DESTINATION}

install(TARGETS)

Note

OPTIONAL 指的是如果找不到要安装的文件也不报错。CONFIGURATIONS 指的是这个安装对哪些配置(Debug, Release)有效,处在无效配置时这条 install 命令被忽略。

install(TARGETS <target>... [EXPORT <export-name>]
        [<output-artifact-configuration> ...]
        [INCLUDES DESTINATION [<dir> ...]]
        )

<output-artifact-configuration> 指的是:

<TYPE> [DESTINATION <dir>] [PERMISSIONS permissions...]
       [CONFIGURATIONS [Debug|Release|...]]
       [COMPONENT <component>]
       [NAMELINK_COMPONENT <component>]
       [OPTIONAL] [EXCLUDE_FROM_ALL]
       [NAMELINK_ONLY|NAMELINK_SKIP]

其中的 <TYPE> 可以是:

  1. ARCHIVE 静态库(Static libraries (.a) and DLL import libraries for Windows-based systems (.lib))
  2. LIBRARY 动态库(Shared libraries (.so), but not DLLs)
  3. RUNTIME 可执行文件和 DLLs。
  4. OBJECT 目标文件
  5. FRAMEWORK macOS 专属
  6. BUNDLE macOS 专属
  7. PUBLIC_HEADER, PRIVATE_HEADER, RESOURCE

一个 target 可以同时指定安装多个 types。也就是说,有个 target 可能既需要安装静态库,又需要安装动态库,同时还安装头文件。如果一个 target 被指定了需要安装某一类成品(比如设置了 PUBLIC_HEADER 属性),但是这个成品没有在 install 命令中给出配置,那么在 3.20.0 之前就会忽略它的安装,而在这之后则是使用默认配置安装。参考:

The CMake documentation claims that if you only configure one artifact type (for example, LIBRARY), only this type will be installed. For CMake version 3.20.0, this is not true: all the artifacts will be installed as if they were configured with the default options. This can be solved by specifying <TYPE> EXCLUDE_FROM_ALL for all unwanted artifact types.

这个命令也有局限性,比如虽然 CONFIGURATIONS 是可以对每个成品类型单独设置,但是一个成品类型只能在 install 命令中出现一次。也就是说想要对 Debug 和 Release 两种配置单独设置某种成品的安装方式,需要两条 install 命令,不能放在一起写。

上面的这些变量也能够通过命令行设置(大概是因为它们是 cached 变量)。

GNUInstallDirs

CMake 有一个 GNUInstallDirs 模块。这个模块可以检查 GNU 平台的种类(比如说有些系统并不是安装在 /usr/local 中比较好),并正确设置好和安装路径相关的变量。在安装之前 include 一下就行了。

处理 public headers

The install(TARGETS) documentation recommends that we specify public headers (as a semicolon-separated list) in the PUBLIC_HEADER property of the library target.

add_library(calc STATIC calc.cpp)
target_include_directories(calc INTERFACE include)
# 也可以直接设置 INTERFACE_ 开头的变量
set_target_properties(calc PROPERTIES
  PUBLIC_HEADER src/include/calc/calc.h
)

因为设置了 calc 这个 target 的 PUBLIC_HEADER 属性,这下用 install 安装 calc 的时候,只要指定 PUBLIC_HEADER 也能将这些头文件也一起安装。

但是我们还需要解决一个问题,CMake 的默认安装位置很可能不是我们想要的。尤其是直接将头文件裸露在 /usr/local/include 里面这一点。包含 GNUInstallDirs 模块之后,CMAKE_INSTALL_INCLUDEDIR 会被重设。然后我们根据它注明我们要安装的位置(参数 DESTINATION):

cmake_minimum_required(VERSION 3.20.0)
project(InstallTargets CXX)
add_subdirectory(src bin)
include(GNUInstallDirs)
install(TARGETS calc
  ARCHIVE
  PUBLIC_HEADER
  DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/calc
)

Note

在 Debian 12 上测试,CMAKE_INSTALL_INCLUDEDIR 都是 include。拼接上默认的前缀路径就是 usr/local/include。

这个变量似乎在所有平台上其实都是一样的,但是也有一些变量在不同平台上表现不同,比如:

CMAKE_INSTALL_LIBDIR will vary by architecture and distribution – lib, lib64, or lib/<multiarch-tuple>.

书上给出的运行结果:

# cmake -S <source-tree> -B <build-tree>
# cmake --build <build-tree>
# cmake --install <build-tree>
-- Install configuration: ""
-- Installing: /usr/local/lib/libcalc.a
-- Installing: /usr/local/include/calc/calc.h

由于版本比较新,而且没有指定静态库成品的安装方式,所以按照默认方式安装了。

这种基于 target 的安装感觉比基于其他方式的安装更加灵活和方便,毕竟是把很多成品都捆绑在一起了。但是也有缺点,比如必须要给出所有的头文件,手动列举容易形成超长的列表。而且所有头文件都被安装在了同一个目录,丢失了层级关系:

Warning

This works well for this basic case, but there’s a slight drawback: files specified in this way don’t retain their directory structure. They will all be installed in the same destination, even if they’re nested in different base directories.

在 CMake 3.23.0 之后,target_sources 有新选项 FILE_SET 可以更好解决这个问题。

install(FILES|PROGRAMS)

签名:

install(<FILES|PROGRAMS> files...
        TYPE <type> | DESTINATION <dir>
        [PERMISSIONS permissions...]
        [CONFIGURATIONS [Debug|Release|...]]
        [COMPONENT <component>]
        [RENAME <name>] [OPTIONAL] [EXCLUDE_FROM_ALL])

这两命令非常相似,但是设置的默认权限不同,主要是 PROGRAMS 会设置写权限。下面是不同类型和默认的安装位置。

install(DIRECTORY)

签名:

install(DIRECTORY dirs...
        TYPE <type> | DESTINATION <dir>
        [FILE_PERMISSIONS permissions...]
        [DIRECTORY_PERMISSIONS permissions...]
        [USE_SOURCE_PERMISSIONS] [OPTIONAL] [MESSAGE_NEVER]
        [CONFIGURATIONS [Debug|Release|...]]
        [COMPONENT <component>] [EXCLUDE_FROM_ALL]
        [FILES_MATCHING]
        [[PATTERN <pattern> | REGEX <regex>] [EXCLUDE]
        [PERMISSIONS permissions...]] [...]

TYPE 选项和 install(FILES|PROGRAMS) 是一样的。

有个小细节需要注意:如果给的目录没有以 / 结尾,将会将目录本身复制到目的位置。如果以 / 结尾,将会把目录中的内容复制到目的位置,这个和 rsync 的拷贝有相同之处

There’s one detail worth noting: if the paths that are provided after the DIRECTORY keyword do not end with /, the last directory of the path will be appended to the destination, like so:

install(DIRECTORY a DESTINATION /x)

This will create a directory called /x/a and copy the contents of a to it. Now, look at the following code:

install(DIRECTORY a/ DESTINATION /x)

This will copy the contents of a directly to /x.

使用这个模式的时候可以用 DIRECTORY_PERMISSIONS 参数设置文件夹的 x 权限。这是因为文件夹的 x 权限表示用户是否能够进入目录和列举目录内容,是和文件有一点不同的。

还能过滤目录中的文件。可以用 PATTERN 来匹配 glob 表达式(以给定的 parttern 结尾的所有文件名都会被匹配上),或者用 REGEX 来匹配正则表达式。用 FILES_MATCHING 来选择文件(但是不会排除目录,因而目录结构会被保留)。用 EXCLUDE 来排除一部分已经匹配上的内容。

install(SCRIPT|CODE)

在安装的时候执行代码。这里的代码得是 CMake 的代码,并不是 shell 的代码:

cmake_minimum_required(VERSION 3.20.0)
project(InstallCode CXX)
add_subdirectory(src bin)

include(GNUInstallDirs)
install(TARGETS calc LIBRARY
        PUBLIC_HEADER
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/calc
       )

if (UNIX)
  install(CODE "execute_process(COMMAND ldconfig)")
endif()

这里的 "execute_process(COMMAND ldconfig)" 只能用奇异来形容。

也可以用 install(SCRIPT),但是它的参数是 CMake 脚本文件的路径而不是代码字符串。

install(EXPORT)

install(EXPORT <export-name> DESTINATION <dir>
        [NAMESPACE <namespace>] [[FILE <name>.cmake]|
        [PERMISSIONS permissions...]
        [CONFIGURATIONS [Debug|Release|...]]
        [EXPORT_LINK_INTERFACE_LIBRARIES]
        [COMPONENT <component>]
        [EXCLUDE_FROM_ALL])

It’s a combination of “plain” export(EXPORT) and other install() commands.

Note

好像能使用的 EXPORT 参数的 install() 命令除了 install(EXPORT) 本身也就只有 install(TARGETS) 了。

这个命令是用来安装 export 文件的。用 export 或者 install(TARGETS ... EXPORT) 可以生成 export 文件。这个命令不会在构建时在 binary tree 里产生文件哦!

写 Config file

Basic config file

A complete package definition consists of the target export files, the package’s config file, and the package’s version file, but technically, all that’s needed for find_package() to work is a config-file.

Config file 的命名:<PackageName>-config.cmake or <PackageName>Config.cmake。搜索路径有很多,一般可以放在 ${CMAKE_INSTALL_LIBDIR}/<PackageName>/cmake/ 下。

最简单的 config file 就是直接将 target export files 引用进来:

# CMakeLists.txt
install(EXPORT CalcTargets
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake
  NAMESPACE Calc::
)
install(FILES "CalcConfig.cmake"
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake
)
# CalcConfig.cmake
include("${CMAKE_CURRENT_LIST_DIR}/CalcTargets.cmake")

Advanced config file

更高级一点的是不自己写 CalcConfig.cmake,而是提供模板 CalcConfig.cmake.in 文件(一般按照这种方式命名)。

cmake_minimum_required(VERSION 3.20.0)
project(AdvancedConfig CXX)
include(GNUInstallDirs) # so it's available in ./src/
add_subdirectory(src bin)

install(TARGETS calc EXPORT CalcTargets ARCHIVE
  PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/calc
)
install(EXPORT CalcTargets
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake
  NAMESPACE Calc::
)
# 到这一步已经有了 target export file,但是没有 config file

include(CMakePackageConfigHelpers)
set(LIB_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR}/calc)
configure_package_config_file(
  ${CMAKE_CURRENT_SOURCE_DIR}/CalcConfig.cmake.in
  "${CMAKE_CURRENT_BINARY_DIR}/CalcConfig.cmake"
  INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake
  PATH_VARS LIB_INSTALL_DIR
)
# 到这一步已经根据模板文件创建了 config file

install(FILES "${CMAKE_CURRENT_BINARY_DIR}/CalcConfig.cmake"
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake
)

一般使用 CMakePackageConfigHelpers 模块来帮助完成这个操作。使用 install 分了两部,而不是一口气安装到系统目录,这是因为 configure 的时候不确定用户是否需要安装,所以不能轻易安装。只有用户使用 cmake --install 启动安装过程时,才应该将其安装到系统。另外就是在 configure 的时候就要明确目的安装路径了,因为可能有些安装文件是要硬编码最终的安装路径的。

PATH_VARS 是我们要从当前的 CMake 脚本向 *.cmake.in 文件传递的变量,这些变量可以在模板文件中引用。并且这个函数会对我们传入的变量重新计算,正确处理好绝对路径和相对路径。比如上面代码中传入的是 LIB_INSTALL_DIR,在模板文件中可以用 @PACKAGE_LIB_INSTALL_DIR@ 来引用,注意有一个 PACKAGE 前缀。如果传入的是 calc(相对路径),会被展开为 ${PACKAGE_PREFIX_DIR}/calc;传入的是 /calc(绝对路径),会被展开为 /calc(不做路径拼接)。

模板文件写法如下:

@PACKAGE_INIT@

set_and_check(CALC_LIB_DIR "@PACKAGE_LIB_INSTALL_DIR@")
include("${CALC_LIB_DIR}/cmake/CalcTargets.cmake")

check_required_components(Calc)

开头有一个固定的 @PACKAGE_INIT@ 头。set_and_checkcheck_required_components 都是 configure_package_config_file 提供的宏,会在 @PACKAGE_INIT@ 展开的时候补充出来。好像目前版本里就只提供了这两个宏。

set_and_check 会在设置变量后检查它是否为一个在操作系统上存在的路径,不存在则报错。check_required_components 的参数是当前包的名字,它会根据 CMake 的 find_package 调用规定检查传来的 COMPONENTS 是否全部被找到了(以 FOUND 为后缀的变量是否为真),没有找到则报错。所以这个宏要放在整个模板文件的最后使用。

基本上一头一尾是固定的,中间是一些 include 和变量设置。

如果有一些必须要满足的依赖可以使用 CMakeFindDependencyMacro 中的 find_dependency() 去找,注意这个宏有找不到就返回的性质。这个函数在 chapter07 - managing dependencies 已经提到过了。

Version file

CMakePackageConfigHelpers 还提供了生成 version 文件的功能。有了 version,用户就能像这样来查找包:

find_package(Calc 1.2.3 REQUIRED)

CMake will then search the config-file for Calc and check if a version file named <config-file>-version.cmake or <config-file>Version.cmake is present in the same directory, that is, CalcConfigVersion.cmake.

write_basic_package_version_file 的用法:

write_basic_package_version_file(<filename> [VERSION <ver>]
  COMPATIBILITY <AnyNewerVersion  | SameMajorVersion |
                 SameMinorVersion | ExactVersion>
  [ARCH_INDEPENDENT]
)

其中兼容性指定的是版本可能不同时的处理情况,如果使用 AnyNewerVersion,则可以向下兼容请求的版本。

First, we need to provide the <filename> property of the artifact we want to create; it must follow the rules we outlined earlier. Other than that, keep in mind that we should store all the generated files in the build tree.

注意这个命令还是将文件写入到 build tree 的。安装步骤是要分开的。我们可以提供一个 VERSION 参数,如果不提供则会使用我们的 project() 中标注的 VERSION 参数,如果 project() 也没有提供 VERSION 就会报错了。

Components

find_package 可以有 COMPONENTSOPTIONAL_COMPONENTS 参数。注意这些参数需要由 config 提供者在 config 文件中做好检查,如果不检查那么就会无事发生。

Requested components are passed to the config-file in the <package>_FIND_COMPONENTS variable (both optional and not). Additionally, for every non-optional component, a <package>_FIND_REQUIRED_<component> will be set. As package authors, we could write a macro to scan this list and check if we have provided all the required components. But we don’t need to – this is exactly what check_required_components() does. To use it, the config-file should set the <Package>_<Component>_FOUND variable when the necessary component is found. The macro at the end of the file will check if all the required variables were set.

对于必要的 components 做检查时,可以使用 CMakePackageConfigHelper 模块中提供的 check_required_components() 宏。到文件最后还没有设置 FOUND 变量的对应模块就被视为没有找到。

CMake 在命令行从 build tree 中安装 components(如果指定的某些 components 没有匹配成功,也不会报错,只是不安装而已):

cmake --install <build tree> --component=<component name>

install() 命令中可以给每一项都加 COMPONENT 参数,将给定项目归类到一个 COMPONENT 中。这样在限定 COMPONENT 时,就只会安装对应的文件。但是即便是标记了 COMPONENT,文件也会被默认安装,想要将其排除需要使用 EXCLUDE_FROM_ALL。

install(TARGETS calc EXPORT CalcTargets
  ARCHIVE
    COMPONENT lib
  PUBLIC_HEADER
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/calc
    COMPONENT headers
)

If no COMPONENT keyword is specified for the installed artifact, it will get a default value of Unspecified from the CMAKE_INSTALL_DEFAULT_COMPONENT_NAME variable.

Managing symbolic links for versioned shared libraries

在安装的时候 CMake 可能会使用符号链接帮助我们处理好 linker 的问题:

The target platform for your installation may use symbolic links to help linkers discover the currently installed version of a shared library. After creating a lib<name>.so symlink to the lib<name>.so.1 file, it’s possible to link this library by passing the -l<name> argument to the linker. The creation of such symlinks is handled by CMake’s install(TARGETS <target> LIBRARY) block when needed.

可以跳过符号链接的阶段,或者只进行符号链接的阶段。

CPack

以下是 CPack 支持的 package generators。请不要将 package generators 和 build generators 搞混淆。

使用 CPack 只需要在 CMakeLists.txt 正确写好 install 命令,然后加入对 CPack 相关变量的设置和对 CPack 模块的引用。

set(CPACK_PACKAGE_VENDOR "Rafal Swidzinski")
set(CPACK_PACKAGE_CONTACT "email@example.com")
set(CPACK_PACKAGE_DESCRIPTION "Simple Calculator")
include(CPack)

这里 CPack 模块的作用是从 project 命令的设置结果中推导一些 cpack 程序需要用到的变量。没有 CPack 也可以手动设置这些变量。

这几个变量是安装必不可缺的(还有很多不可缺少的变量,include(CPack) 就能推导):

变量名                       对应命令行参数(覆盖)
CPACK_PACKAGE_NAME          -P
CPACK_PACKAGE_VERSION       -R
CPACK_PACKAGE_VENDOR        --vendor

最终文件名是 $CPACK_PACKAGE_NAME-$CPACK_PACKAGE_VERSION-$CPACK_SYSTEM_NAME,比如 CPackPackage-1.2.3-Linux.zip(package generator 是 ZIP)。

为了让 CPack 能推导变量,project() 命令就要写的详细一点(其实也就只是需要一个明确的 version,毕竟名字是必需的、语言是无关的):

project(CPackPackage VERSION 1.2.3 LANGUAGES CXX)

其他选项:

-G <generators>      semicolon-separated list of package generators
                     可以在 CMakeLists.txt 的 include(CPack) 之前设置 CPACK_GENERATOR 变量以提供默认值
-C <configs>         semicolon-separated list of build configurations (debug, release)
-D <var>=<value>
--config <config-file>
                     覆盖 CPackConfig.cmake 的路径(这样就可以不命名为 CPackConfig.cmake)
-B <packageDirectory>
                     输出路径。默认是当前工作路径。

例子:

cpack -G "ZIP;7Z;DEB" -B pkgs

这样会生成三个包,分别是(书中给出的结果):

CPackPackage-1.2.3-Linux.7z
CPackPackage-1.2.3-Linux.deb
CPackPackage-1.2.3-Linux.zip

因为 cpack 还会生成临时的目录 _CPack_Packages,所以最好显式提供 -B 参数,不要生成在当前路径。就算是在 build 里面,将多个包分组在同一个文件夹里面也是更好的做法。

Caution

cpack 要在 build tree 里面跑,这个和 ctest 一样。

为什么要在 build tree 里跑呢?因为 build tree 中有生成的 CPackConfig.cmake。如果在别处跑,就不会自动加载这个文件,从而报错:CPack Error: CPack project name not specified。这样的报错很隐晦,因为 cpack 程序并不要求存在 CPackConfig.cmake 才能运行,而我们也使用了 include(CPack) 的写法,没有手动提供必需的变量。

如果直接在 source tree 里跑,能够通过 --config 选项传入 build tree 中的 CPackConfig.cmake 路径,也是可以成功的。

Note

cpack 在 multi-config generator(至少 Ninja Multi-Config 是这样)的 build tree 中默认寻找的是 Release 版本。使用 -C Debug 来允许 Debug 版本的安装。

感觉 CPack 很好用,在写好 install 命令的基础上几乎不必花时间去适配 CPack。主要还是 install 比较复杂。