第25章:安装

在使用项目的源代码、创建各种资源、使构建健壮,并实现自动化测试等工作之后,发布已构建的软件是非常重要的一步。它直接影响用户对项目的第一印象,如果做得不好,可能会导致软件在使用之前就遭到拒绝。

开发人员和用户对项目的期望不同。对于某些人来说,仅仅提供对源码库的访问,并让用户自己构建出软件就已经足够了。虽然这可能是交付的一部分,但并非所有用户都希望在如此低级别的参与度。相反,他们通常会期望预先构建的二进制包,以便在他们的机器上安装和使用,最好是通过一些熟悉的包管理系统。给定各种包管理器和交付格式,将给项目维护者带来令人生畏的挑战。然而,它们之间有足够多的共同元素,通过一些规划,可以支持大多数流行的平台,并覆盖主要平台。

项目的生命周期中,交付阶段考虑得越早,最终的打包和部署阶段就可能越顺利。好的出发点是在开发开始前,或在项目开始前,尽早提出以下问题:

  • 应该支持哪些平台,包括最初支持的平台和将来可能支持的平台吗?为了支持项目的特性,是否有最低的平台API或SDK版本要求?

  • 每个平台上,用户熟悉的包格式是什么?项目可以以这些格式交付吗?有没有什么特定的软件包格式比其他格式更重要,或者是有强制性的格式需求?

  • 是否需要或期望的软件包格式?对软件必须如何布局、构建或交付有需求?项目资源是否必须以特定的格式、方案、位置等方式提供?

  • 终端用户想要同时安装软件的多个版本吗?

  • 是否应该在没有管理权限的情况下支持安装软件?

  • 软件是否可以重新定位,以便用户可以将其安装在系统的任何位置(包括在Windows的任何盘符上)?

  • 项目是否希望通过PATH环境变量在部署计算机上?提供一个或多个可执行文件?项目中是否有不应该在PATH上暴露的部分?

  • 项目是否提供了其他CMake项目构建中使用的东西(库、可执行文件、头文件、资源等等)?

这些问题将影响软件在安装时的布局,这反过来又会影响源代码访问自身资源的方式等等。甚至可能影响软件可用的功能,所以尽早理解这些事情可以节省时间,避免浪费精力。

本章着重于布局方面,以及如何将必要的文件组装到需要的位置。还演示了如何通过提供配置包来简化其他CMake项目的使用。具有某些背景的开发人员可能认为,这些方面属于make安装领域。下一章通过讨论CMake和CPack可以产生的各种包格式来完成这个拼图。使用这里描述的安装功能,可以将软件安装到一个干净的区域内,然后将这些内容生成最终的软件包。

25.1. 目录结构

决定如何部署已安装的产品之前,了解部署平台的约束是一个必要的步骤。只有清楚了这些细节,CMake项目才能开始定义要安装到哪里。可以进行一些鸟瞰式观察,这些观察可能对一个项目的安装布局有很大的影响。

  • Apple对格式(捆绑包、框架等)的规定很严格,这样灵活性就很低,不过也清楚地说明项目需要如何交付。正如在第22章提到的,CMake已经在构建阶段自动处理大部分内容,使应用程序为进行Xcode的最后一步做好准备,即执行最终的应用签名、创建包和提交应用程序。如果使用CMake/CPack的安装阶段,那么就会是简单地将遵循指定布局的方式进行打包。

  • 对于Linux发行版的一部分,对于项目来说,肯定会有每种类型文件应该安装在何处的指导原则。文件系统层次标准构成了大多数应用布局的基础,许多其他基于Unix的系统遵循类似的结构。即使不打算直接包含在发行版中,FHS仍然可以作为很好的指南,指导如何构造一个包,以便在许多基于Unix的系统上进行平稳和健壮的安装。

  • 有些项目可能希望在用户的PATH环境变量提供一个或多个可执行文件,这样就可以从终端或命令行轻松地调用它们。Windows上,如果一个项目安装通过添加一个包含它自己的DLL的目录来修改PATH,那么其他应用程序可能会选择相应DLL,而不是之前预装的DLL(例如,从它们自己的私有目录或一个标准的系统范围的位置)。工具包(如Qt)的DLL经常成为这种情况的受害者,因为包以不应该的方式修改了PATH。如果项目想要将自己的可执行文件增加到PATH中,应该确保该目录中不存在无用的DLL,并将DLL与可执行文件放在同一目录中以便Windows在运行时找到它们,从而避免DLL冲突。典型的解决方案是创建一个只包含启动脚本的目录,然后安全地将这些脚本添加到PATH环境变量中。

25.1.1. 相对目录

除了部署到Apple平台之外,所有主流平台之间存在很大的共性(或者至少是潜在的共性)。可以认为安装位置由基本路径和该路径下的相对路径组成。基本路径可能类似/usr/…,/ opt/…或者C:\Program Files,不同平台之间的差异很大,但在这个基础上的相对路径非常相似。一种常见的安排是将可执行文件(对于Windows是dll)安装到bin目录,将库文件安装到lib,并将头文件目录安装在include下。其他文件的安装位置可变,但是这三种已经涵盖了项目将要安装的一些重要的文件类型。

Windows上,将可执行文件和DLL放在基本安装位置,而不是bin目录下。虽然这是一种常见的做法,但可能导致基目录特别臃肿,使用户很难找到其他组件。另一种是将启动脚本放在cmd的子目录中,这使它们可以与bin等其他位置的DLL分隔开。

找到适用于大多数平台的目录结构不太现实,因为它最小化了由项目源代码实现特定于平台的逻辑。如果项目在所有平台上使用相同的相对布局,那么程序在运行时更容易找到需要的东西。在没有任何其他需求的情况下,CMake的GNUInstallDirs模块提供了非常方便的方法来使用标准目录布局。与上面提到的常见情况一致,并且还提供了各种符合GNU编码标准和FHS的标准位置。撇开与基本安装路径相关的部分(下一节将介绍),甚至可以在Windows上使用。

使用GNUInstallDirs模块是相当简单:

# Minimal inclusion, but see caveat further below
include(GNUInstallDirs)

这会创建CMAKE_INSTALL_<dir>缓存变量,其中<dir>表示特定的位置。模块的文档给出了定义位置的完整细节,但一些常用方式包括:

BINDIR

直接运行的可执行文件、脚本和符号链接的位置。默认为bin。

SBINDIR

与BINDIR相似,不过是针对有系统管理权限的情况。默认为sbin。

LIBDIR

库和编译文件的路径。根据主机/目标平台,默认设置为lib或其他(可能包括特定于体系结构的子目录)。

LIBEXECDIR

不直接由用户调用的可执行文件,但可以通过启动脚本或位于BINDIR中的符号链接的方式运行。默认为libexec

INCLUDEDIR

头文件目录。默认为include。

DATAROOTDIR

只读与结构无关的数据点。为了避开DOCDIR的警告,通常不直接引用。

DATADIR

与结构无关的只读数据,如图像和其他资源。默认值与DATAROOTDIR相同,用于覆盖项目数据位置的首选方法。

MANDIR

man格式文档的路径。默认为DATAROOTDIR/man。

DOCDIR

通用文档路径。默认值为DATAROOTDIR/doc/PROJECT_NAME(参见下面的注释,了解为什么依赖这个默认值是不安全的)。

每个位置都定义为缓存变量,因此可以重写它们。开发人员通常不会更改它们,因为安装位置应该在项目的控制之下。对于项目来说,更改默认位置通常也不可取,但如果项目希望遵循标准布局,需要做一些调整,那么更改位置就是没问题的。

DOCDIR位置需要特别提及,因为它默认包含一个值组成了PROJECT_NAME变量。PROJECT_NAME通过对project()的调用进行更新,因此可以在项目结构中变化。GNUInstallDirs模块仅在尚未定义缓存变量时才设置它们,因此CMAKE_INSTALL_DOCDIR的值将由GNUInstallDirs模块决定。为了防止这种情况,允许默认的文档目录遵循项目的结构,项目可能需要在每次包含模块时显式地设置DOCDIR的位置(非缓存变量将覆盖缓存变量):

# Explicitly set DOCDIR location each time
include(GNUInstallDirs)
set(CMAKE_INSTALL_DOCDIR ${CMAKE_INSTALL_DATAROOTDIR}/doc/${PROJECT_NAME})

本章的剩余部分中,示例中将使用CMAKE_INSTALL_<dir>变量作为大多数的安装路径。

25.1.2. 安装位置

确定了安装文件的布局之后,必须确定该布局的基本安装位置。有许多因素会影响这一决定,首先要回答安装是否可重定位。这只是意味着可以使用任何安装基点,只要保留相对布局,已安装的项目仍将按预期工作。可重定位是非常重要,并且应该是大多数项目的目标,例如:

  • 可同时安装多个版本。

  • 重定位包可以安装到共享驱动器上,这些共享驱动器在不同终端用户的机器上可能有不同的挂载点。

  • 一组自包含的可重定位文件,可以更容易地打包成系统包。

  • 非管理员用户可以可重定位项目在自己本地目录进行安装。

不是所有的项目都可以重定位,需要把他们的文件放在非常特定的位置(例如内核包)。除了少数配置文件外,有些项目是可以重定位的,有时是将这些特定的文件作为安装后的步骤进行处理(下一章将针对特定的打包系统进行讨论)。

基本安装位置的选择与目标平台密切相关,每个平台都有自己的通用实践和指导原则。Windows上,基本安装位置通常是C:\Program Files目录,而在大多数其他系统上,它是/usr/local或/opt目录。CMake提供了许多用于管理安装位置的控件,以抽象出这些平台差异。最重要的是CMAKE_INSTALL_PREFIX变量,它控制用户构建安装目标时的基本安装位置(该目标可以通过一些生成器类型调用install)。CMAKE_INSTALL_PREFIX的默认值是C:\Program Files${PROJECT_NAME},在基于Unix的平台上是/usr/local。

Linux上安装时,默认值不符合文件系统层次结构标准。FHS要求系统包使用/或/usr为基本位置,后者更可能是理想的选择。对于附加包,应该安装到/opt/或/opt/,建议使用/opt/。如果使用的是,那么在形式上应该是LANANA-注册的名称,或者仅提供包的小写域名。这是为了避免使用相同基本安装位置的不同包之间的冲突。对于大多数项目,建议显式地在非windows平台上设置CMAKE_INSTALL_PREFIX以兼用FHS路径/opt/…,但是这通常只在顶层CMakeLists.txt中完成,并检查项目是否源码树的顶层(支持分级项目安排)。

if(NOT WIN32 AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
 set(CMAKE_INSTALL_PREFIX "/opt/mycompany.com/${PROJECT_NAME}")
endif()

对于交叉编译的场景,可以定义CMAKESTAGING_PREFIX变量来为安装规则提供安装到的位置。这可以许安装到文件系统,同时保留CMAKE_INSTALL_PREFIX的其他效果,比如在已安装的二进制文件中嵌入路径(在25.2.2节“RPATH”中介绍)。CMAKE_STAGING_PREFIX还会影响大多数find…()命令的搜索路径。

对于一些打包场景和位置测试安装过程,CMake支持用于非Windows平台的常见DESTDIR功能。DESTDIR不是CMake变量,而是传递给构建工具的变量,或者作为构建工具的环境变量。它允许将安装基础位置放置在任意位置,而不是文件系统的根目录。当直接调用构建工具时,通常在命令行上使用,例如:

make DESTDIR=/home/me/staging install
env DESTDIR=/home/me/staging ninja install

DESTDIR功能在概念上有点类似于CMAKESTAGING_PREFIX,但是DESTDIR只在安装时指定,不会影响find…()命令。CMAKE_STAGING_PREFIX保存为缓存变量,而DESTDIR是环境变量,构建工具调用之间不保存。

CMAKE_INSTALL_PREFIX、CMAKE_STAGING_PREFIX和DESTDIR的组合为项目和开发人员提供了设置基本安装位置的灵活性,并可以测试安装,而不必实际接触最终的安装位置。但是要注意,不同的打包格式可能有它们自己的默认安装位置,并且可能会忽略这三个变量,而不是特定于包的变量。

25.2. 安装目标

确定了安装区域的目录结构后,就可以将注意力转到安装内容本身了。项目使用install()命令来定义安装内容,以及放置位置等等。这个命令有许多不同的形式,每个形式都作用于第一个参数指定的实体目标。其中一种用于安装的方式:

install(TARGETS targets...
 [EXPORT exportName]
 [CONFIGURATIONS configs...]
 # One or more blocks of the following
 [ [entityType]
 DESTINATION dir
 [PERMISSIONS permissions...]
 [NAMELINK_ONLY | NAMELINK_SKIP]
 [COMPONENT component]
 [NAMELINK_COMPONENT component] # CMake 3.12 or later only
 [EXCLUDE_FROM_ALL]
 [OPTIONAL]
 [CONFIGURATIONS configs...]
 ]...
 # Special case
 [INCLUDES DESTINATION incDirs...]
)

提供一个或多个目标,entityType指定如何安装目标的各个部分。每个目标都必须与install()命令在相同的目录范围内,并且entityType必须是以下类型之一:

RUNTIME

安装可执行的二进制文件。在Windows上,还会安装库目标的DLL。苹果捆绑包不包含在其中。

LIBRARY

除Windows和苹果框架之外的所有平台上安装动态库。

ARCHIVE

除了苹果框架外,安装静态库(所有平台)。在Windows上,这还会安装动态库的导入库(即.lib)部分。

OBJECTS

安装与对象库关联的对象(仅适用于CMake 3.9或更高版本)。

FRAMEWORK

Apple平台上,安装框架(动态或静态),包括复制到框架中的任何内容(例如,通过POST_BUILD自定义规则)。

BUNDLE

Apple平台上,安装捆绑包,包括复制到捆绑包中的内容。

PUBLIC_HEADER

非Apple平台上,这将安装框架库目标的PUBLIC_HEADER属性中列出的文件。Apple平台上,这些头文件看作为框架的一部分,但是对于非Apple平台,这些目标视为普通的动态库,并且头文件需要单独安装。

PRIVATE_HEADER

类似于PUBLIC_HEADER,但会受PRIVATE_HEADER属性的影响。

RESOURCE

非Apple平台上,这将安装框架或捆绑目标的目标RESOURCE 属性中列出的文件。Apple平台上,这些文件作为FRAMEWORK或BUNDLE的一部分。

entityType之后,可以列出各种选项。例如,下面展示了如何在所有平台(假设不是Apple框架)上,以某种方式安装库,将各自的部分放在期望的位置上:

install(TARGETS mySharedLib myStaticLib
 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
 LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
 ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

上面的示例展示了DESTINATION选项如何为同一个目标的不同部分指定不同的位置。该命令足够灵活,可以同时处理多个不同类型的目标。

  • 对于mySharedLib,在Windows上,DLL将转到RUNTIME,导入库转到ARCHIVE 。其他平台上,动态库将安装到LIBRARY 。

  • myStaticLib目标的静态库将安装到ARCHIVE。

如果目标没有对应的entityType,CMake通常会发出警告或错误(例如,其中一个目标是静态库,但没有提供ARCHIVE)。entityType可以省略,目标列表后面的选项将应用于所有实体类型。通常只有当列出的目标只能有一种实体类型时,才会这样做:

# Targets are both executables, so specifying the entity type isn't needed
install(TARGETS exe1 exe2
 DESTINATION ${CMAKE_INSTALL_BINDIR}
)

后面的选项可以指定的不仅仅是目标。还可以使用权限选项覆盖默认权限,指定一个或多个与18.2节“复制文件”中的文件(复制)命令相同的值:

对于文件(复制),平台不支持的权限将忽略。请注意,CMake在默认情况下会为所有目标设置适当的权限,所以通常只需要明确地提供权限。如果安装的位置需要比正常情况下更多的权限,或者需要添加SETUID或SETGID权限,类似如下情况:

# Intended to only be run by an administrator, so only allow the owner to have access
install(TARGETS onlyOwnerCanRunMe
 DESTINATION ${CMAKE_INSTALL_SBINDIR}
 PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
)

# Install with set-group permission
install(TARGETS runAsGroup
 DESTINATION ${CMAKE_INSTALL_BINDIR}
 PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
 GROUP_READ GROUP_EXECUTE SETGID
)

对于LIBRARY类型,当为库目标提供了版本信息时,一些平台支持创建符号链接(参见20.3节,“动态库版本化”)。动态库中可能存在的文件和符号链接通常是这样的:

libmyShared.so.1.3.2 ①
libmyShared.so.1 --> libmyShared.so.1.3.2 ②
libmyShared.so --> libmyShared.so.1 ③

① 由项目构建的实际二进制文件。 ② 符号链接文件,其名称是实际库的名称。当遵循语义版本控制时,将只在其名称中包含版本的主要部分。 ③ 文件名中没有嵌入版本信息的Namelink。当链接器命令行有-lmyShared这样的选项时,就可以通过该文件找到实际的库文件。

安装库实体文件时,可以提供NAMELINK_ONLY或NAMELINK_SKIP选项。NAMELINK_ONLY选项将只安装namelink文件,而NAMELINK_SKIP将安装除了namelink之外的所有内容。如果库目标没有版本信息,或者平台不支持namelinks,这两个选项的行为就会改变。NAMELINK_ONLY将不安装任何东西,而NAMELINK_SKIP将安装真正的库文件。当创建单独的运行时和开发包时,这些选项特别有用,namelink部分安装入开发包后,其他文件/链接会安装入运行时包。当给出NAMELINK_ONLY选项时,CMake不会警告install()命令中缺少的内容。之所以需要这样做,是因为不能在同一个install()调用中同时使用NAMELINK_SKIP和NAMELINK_ONLY,因此必须在不同的调用中对两者进行拆分(参见下面的示例)。

每个entityType部分可以指定COMPONENT选项。组件用于打包的逻辑,将在下一章详细讨论,现在可以看作是分离不同安装集的一种方式。上面提到的独立运行时和开发包场景,可以设置成如下方式:

install(TARGETS myShared myStatic
 RUNTIME
   DESTINATION ${CMAKE_INSTALL_BINDIR}
   COMPONENT MyProj_Runtime
 LIBRARY
      DESTINATION ${CMAKE_INSTALL_LIBDIR}
   NAMELINK_SKIP
   COMPONENT MyProj_Runtime
 ARCHIVE
   DESTINATION ${CMAKE_INSTALL_LIBDIR}
   COMPONENT MyProj_Development
)

# Because NAMELINK_ONLY is given, CMake won't complain about a missing RUNTIME block
install(TARGETS myShared
 LIBRARY
   DESTINATION ${CMAKE_INSTALL_LIBDIR}
   NAMELINK_ONLY
   COMPONENT MyProj_Development
)

CMake 3.12中,使用NAMELINK_COMPONENT选项可以更简单地将namelink拆分为不同的组件。此选项可以与COMPONENT一起使用,但只能在LIBRARY块中使用。使用这中方式,可以更简明地描述上述情况:

install(TARGETS myShared myStatic
 RUNTIME
   DESTINATION ${CMAKE_INSTALL_BINDIR}
   COMPONENT MyProj_Runtime
 LIBRARY
   DESTINATION ${CMAKE_INSTALL_LIBDIR}
   COMPONENT MyProj_Runtime
   NAMELINK_COMPONENT MyProj_Development # Requires CMake 3.12 or later
 ARCHIVE
   DESTINATION ${CMAKE_INSTALL_LIBDIR}
   COMPONENT MyProj_Development
)

如果没有为块提供任何COMPONENT,那么将与一个默认组件相关联,该组件的名称由变量CMAKE_INSTALL_DEFAULT_COMPONENT_NAME提供。如果未设置该变量,则将未指定的变量用作默认组件名称。在第三方子项目不使用任何安装组件的情况下,更改默认组件名可能会有用。为了使子项目的安装构件与主项目分离,可以在调用add_subdirectory()将子项目拉入主构建之前更改默认名称。

可以使用EXCLUDE_FROM_ALL选项仅安装特定的组件。默认情况下,安装不指定组件,并且会安装所有组件,但是打包实现可以单独安装特定的组件。CMake 3.12中还添加了文档来演示如何从命令行执行此操作。对于大多数项目,可能不太需要EXCLUDE_FROM_ALL。

OPTIONAL关键字也很少使用。如果目标存在,但文件缺失(例如,Windows DLL中ARCHIVE类型的导入库),而CMake不会认为它是错误的。因为它可以隐藏构建/安装逻辑的错误配置,所以请谨慎使用此选项。

还可以通过添加CONFIGURATIONS选项使用特定的配置。只有当前构建类型是所列出的类型之一时,才会安装该实体类型。对于单个install()命令,实体类型不能列出多次,因此如果不同的配置需要不同的详细信息,则需要多次调用。下面的例子展示了如何在不同的目录中安装调试版本和发布版本的静态库:

install(TARGETS myStatic
 ARCHIVE
   DESTINATION ${CMAKE_INSTALL_LIBDIR}/Debug
   CONFIGURATIONS Debug
)
install(TARGETS myStatic
 ARCHIVE
   DESTINATION ${CMAKE_INSTALL_LIBDIR}/Release
   CONFIGURATIONS Release RelWithDebInfo MinSizeRel
)

CONFIGURATIONS关键字还可以位于所有参数的前面,并作为那些不提供自己配置的默认值。下面的示例中,除了为调试和发布安装的ARCHIVE块外,所有的块都只为发布版安装。

install(TARGETS myShared myStatic
 CONFIGURATIONS Release
 RUNTIME
      DESTINATION ${CMAKE_INSTALL_BINDIR}
 LIBRARY
      DESTINATION ${CMAKE_INSTALL_LIBDIR}
 ARCHIVE
   DESTINATION ${CMAKE_INSTALL_LIBDIR}
   CONFIGURATIONS Debug Release
)

25.2.1. 接口属性

如果目标导出(将在25.3节中讨论,后面的“安装导出”中讨论),那么就有机会设置接口属性,供其他项目的目标使用。各种接口目标属性将自动将已安装的目标细节导出,但是需要特殊处理,以满足构建目标和使用已安装目标的需求。考虑以下代码示例:

add_library(foo STATIC ...)
target_include_directories(foo
 INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/somewhere
 ${MyProject_BINARY_DIR}/anotherDir
)
install(TARGETS foo
 DESTINATION ...
)

构建中,任何链接到foo的内容都会有某个位置的绝对路径,并且在头文件搜索路径中添加一个anotherDir 。当foo安装时,可以打包并部署到完全不同的机器上。显然,到某个位和anotherDir的路径将不再有意义,但上面的例子将添加它们到目标的头文件搜索路径中。需要提一下的是,“构建时使用路径xxx,安装时使用路径yyy”的方式,这正是BUILD_INTERFACE和INSTALL_INTERFACE生成器表达式所做的:

include(GNUInstallDirs)
target_include_directories(foo
 INTERFACE
   $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/somewhere>
   $<BUILD_INTERFACE:${MyProject_BINARY_DIR}/anotherDir>
   $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

$<BUILD_INTERFACE:xxx>将构建树扩展为xxx,在安装时扩展为空,而$<INSTALL_INTERFACE:yyy>则相反,确保yyy只针对已安装目标添加。INSTALL_INTERFACE中,yyy通常是相对路径,可视为相对于基本安装位置的路径。

虽然构建树中的头文件搜索路径可能因目标而异,但在安装之后,目标通常都共享相同的头文件搜索路径。上面的示例中,使用了CMAKE_INSTALL_INCLUDEDIR,并且可能会对每个可安装的目标重复使用,但是为每个目标分别指定并不是最方便的方法。可以使用install()命令的include选项为一组目标指定相同的信息。在INCLUDES DESTINATION之后给出的所有目录都添加到列出目标的INTERFACE_INCLUDE_DIRECTORIES属性中。这使得头文件搜索路径的描述更加简洁。

add_library(myStatic STATIC ...)
add_library(myHeaderOnly INTERFACE ...)

target_include_directories(myStatic
 PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/static_exports>
)
target_include_directories(myHeaderOnly
 INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}>
)

install(TARGETS myStatic myHeaderOnly
 ARCHIVE
     DESTINATION ${CMAKE_INSTALL_LIBDIR}
 INCLUDES
     DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

与其他参数不同,如果需要,可以为INCLUDES DESTINATION列出多个目录,尽管在实践中这可能不太常见。还要注意,INCLUDES块不支持其他entityType块支持的信息,它只指定一个DESTINATION关键字,之后可跟一个或多个位置。

25.2.2. RPATH

当操作系统加载库或可执行文件时,必须找到二进制文件链接到的所有动态库。不同的平台有不同的处理方式,Windows依赖于PATH环境变量中的位置,以及二进制文件所在的目录来查找所有需要的库。其他平台使用专门用于此目的的环境变量,比如LD_LIBRARY_PATH,以及其他机制,比如在conf文件中列出的库。环境变量的缺点是,它依赖于加载二进制文件的人或进程正确地配置了环境。

许多情况下,提供二进制文件的包已经知道在哪里可以找到依赖库,因为它们可能是在同一个包里。大多数非Windows平台都支持二进制文件将库搜索路径直接硬编码到文件中。此特性的通用名称是run path或RPATH,实际名称可能与平台有关。通过嵌入RPATH信息,二进制文件可以是自包含的,并且不必依赖于环境或系统配置提供的任何路径。此外,RPATH可以包含某些占位符,这些占位符允许地定义只在运行时解析为绝对路径的相对路径。占位符允许基于二进制文件的位置进行解析,因此可重定位包,可以定义RPATH细节,这些信息只能基于包的相对布局对路径进行硬编码。

与上一节中接口属性的情况一样,构建中的RPATH与已安装的二进制文件存在需求冲突。构建中,开发人员需要二进制文件能够找到需要链接的动态库,这样可执行文件才能运行(例如调试、测试执行等等)。支持RPATH的平台上,CMake将默认嵌入所需的路径,从而为开发人员提供最方便的体验,而不需要进行任何设置。但是,RPATH只适合于特定的构建,因此在安装目标时,CMake会用替换路径重写它们(默认替换成空的RPATH)。

使用RPATH默认值还是合理的,但是不太适合需要安装的目标。项目将会覆盖默认行为,以确保构建树和安装目标得到适当的满足。CMake允许单独控制构建和安装RPATH位置,因此项目可以实现适合自己的策略。以下目标属性和变量可以用于影响RPATH:

BUILD_RPATH

此目标属性可用于嵌入到构建树的二进制文件中的搜索路径。这是CMake为二进制文件的链接依赖,自动添加的附加路径,因此,应该指定CMake的路径,而不是使用外部路径。只有当二进制文件在运行时使用dlopen()或其他等效机制加载非链接库时(比如加载可选插件模块时),才需要这个属性。这个属性在add_library()或add_executable()创建目标时由CMAKE_BUILD_RPATH变量的值初始化。BUILD_RPATH属性和CMAKE_BUILD_RPATH变量在CMake 3.8中添加。

INSTALL_RPATH

此目标属性指定安装二进制文件时的RPATH。与BUILD_RPATH不同,CMake在默认情况下不提供INSTALL_RPATH,因此项目应该将此属性设置为已安装的路径列表。下面将讨论如何做到这一点。当创建目标时,该属性由CMAKE_INSTALL_RPATH变量值初始化。

INSTALL_RPATH_USE_LINK_PATH

当此目标属性设置为true时,目标链接到的每个库的路径将添加到安装RPATH位置集中,当该路径指向项目的源目录和二进制目录之外的位置时才如此。这主要用于将绝对路径嵌入到不属于项目的外部库,这些库应该位于项目将部署到的位置。要谨慎使用这个属性,这样的假设可能会降低已安装包的健壮性(路径可能会随着外部库的版本而改变,系统管理员可能会选择非默认的安装配置等等)。这个属性在创建目标时由CMAKE_INSTALL_RPATH_USE_LINK_PATH变量值初始化。

BUILD_WITH_INSTALL_RPATH

有些项目的安装布局与构建布局是一样的,在这种情况下,安装RPATH也适合于构建树。通过将这个目标属性设置为true,就不会使用BUILD_RPATH,而是在构建时将INSTALL_RPATH嵌入到二进制文件中。注意,当使用加载器支持而链接器不支持的占位符(后面将进一步讨论)时,这可能会导致链接过程中的构建问题。这个属性在创建目标时由CMAKE_BUILD_WITH_INSTALL_RPATH变量初始化。

SKIP_BUILD_RPATH

当此目标属性设置为true时,不设置BUILD_RPATH。将忽略BUILD_RPATH,CMake不会自动为目标链接到的库添加RPATH。请注意,如果依赖库链接到其他库,可能导致构建失败,因此请谨慎使用。创建目标时,该属性由CMAKE_SKIP_BUILD_RPATH变量值初始化。如果BUILD_WITH_INSTALL_RPATH属性设置为true,也会重载。

CMAKE_SKIP_INSTALL_RPATH

在安装时相当于CMAKE_SKIP_BUILD_RPATH。将其设置为true会直接忽略INSTALL_RPATH目标属性,并可能导致已安装的目标在运行时无法找到依赖库,因此有用性值得怀疑。注意,这里没有SKIP_INSTALL_RPATH目标属性,只有CMAKE_SKIP_INSTALL_RPATH变量。

CMAKE_SKIP_RPATH

将该变量设置为true将禁用所有RPATH支持,并忽略上述所有属性和变量。除非项目以其他方式管理运行时库本身的加载,否则通常不希望这样做。

安装RPATH位置理想情况下应该基于相对路径。这在大多数基于Unix的平台上是通过使用$ORIGIN占位符,来表示嵌入RPATH的二进制文件的位置。例如,下面是定义安装RPATH细节的常用方法,该方法适用于与默认GNUInstallDirs模块定义布局类似的项目:

set(CMAKE_INSTALL_RPATH $ORIGIN $ORIGIN/../lib)

要使其更加健壮并考虑到默认布局的潜在变化,还需要做一些工作。需要计算出从可执行文件目录到库文件目录的相对路径,实现如下:

include(GNUInstallDirs)
file(RELATIVE_PATH relDir
 ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}
 ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_INSTALL_RPATH $ORIGIN $ORIGIN/${relDir})

上面定义的所有目标都有一个INSTALL_RPATH,引导加载程序查找与二进制文件相同的目录,以及类似于../lib或二进制文件的位置。因此,对于安装到bin的可执行文件和安装到lib的动态库,这将确保两者都能找到项目提供的任何其他库。在第一次向项目添加RPATH支持时,强烈建议将此作为起点。注意,Apple的目标工作方式略有不同,可能会有不同的布局,因此上面的内容需要进一步调整以覆盖该平台(下一节将讨论)。

需要注意的一个问题是,加载器可以理解$ORIGIN,而链接器很可能不能。当链接到某个库,而该库本身又链接到另一个库时,这可能会出问题。第一级的链接不会出现问题,因为库会直接在链接器命令行上列出,但是第二级的库依赖关系必须由链接器找到。当链接器不理解$ORIGIN时,它无法通过RPATH详细信息找到第二层库。因此,除非路径也由其他选项(如-L)指定,否则就算第一级库技术上包含了所需的所有信息,链接也会失败。这是一个众所周知的问题,并不是CMake特有的,它是链接器(尤其是GNU ld链接器)的一个弱点。

根据上面提到的各种属性和变量,可能需要CMake在安装目标时更改嵌入的RPATH信息。有两种方法可以做到这一点。如果二进制文件是ELF格式的,默认情况下CMake使用内部工具在安装的二进制文件中直接重写RPATH。ELF头文件中的RPATH值是固定大小的,但是CMake在必要时填充BUILD_RPATH,需要确保有足够的空间用于INSTALL_RPATH。除了构建时链接器命令行上一些奇怪的选项外,开发人员基本上不知道实现的细节。对于非ELF平台,CMake在安装时重新链接二进制文件,而不是指定INSTALL_RPATH信息。这有时会使开发人员感到困惑,他们想知道为什么构建的东西需要再次链接,但最终重新链接是获得预期结果的一种方法。ELF平台也可以通过将CMAKE_NO_BUILTIN_CHRPATH变量设置为true,来强制重链接行为,但是除非内部RPATH重写由于某种原因失败,否则通常不应该使用这种方法。

交叉编译时,其他一些变量可以修改嵌入到二进制文件中的RPATH位置。任何以CMAKE_STAGING_PREFIX开始的RPATH位置都自动的将前缀替换为CMAKE_INSTALL_PREFIX。对于构建和安装RPATH位置都是如此。任何以CMAKE_SYSROOT开头的安装RPATH位置都将去掉这个前缀。

25.2.3. Apple平台上的特定目标

Apple的加载器和链接器与其他Unix平台的工作方式略有不同。Linux等平台上的库只将库名编码为动态库(例如soname),而Apple平台则编码库的完整路径。这个完整路径称为install_name,而install_name的路径部分有时称为install_name_dir。任何对该库的链接都会将完整的install_name编码为要搜索的库。当所有的东西都安装到预期位置时,这样没问题,但是对于可重定位包(包括大多数应用程序包),这就太不灵活了。作为一种处理方法,Apple支持类似于$ORIGIN的相对基点,但是占位符不同:

@loader_path

这相当于苹果的$ORIGIN,但链接器能够理解它,因此不会遇到其他链接器无法解码$ORIGIN的问题。

@executable_path

这个变量会使用正在执行的程序的位置。对于依赖项引入的库,这种方法的帮助不大,因为它要求库知道使用它们的可执行文件的位置。这通常是不确定的,因此@loader_path通常是更好的选择。

@rpath

可以用作install_name_dir部分的占位符,也可以完全替换install_name_dir。

@loader_path和@rpath的组合可以用于实现与其他支持$ORIGIN的Unix平台相似的行为。CMake提供Apple特定的控制项,以便在Apple平台进行适当的设置:

MACOSX_RPATH

这个目标属性设置为true时,CMake在为Apple平台构建时自动将install_name_dir设置为@rpath。这是CMake 3.0之后默认的行为。该变量可以使用INSTALL_NAME_DIR覆盖。如果在创建目标时设置了CMAKE_MACOSX_RPATH变量,则使用初始化MACOSX_RPATH属性的值。

INSTALL_NAME_DIR

这个目标属性用于显式设置库的install_name中的install_name_dir部分。默认的install_name的形式通常是@rpath/libsomename。但是对于@rpath不合适的情况,INSTALL_NAME_DIR可以指定一个替代方案。该属性在创建时使用CMAKE_INSTALL_NAME_DIR变量的值初始化。该属性会在非Apple平台上无效化。

对于非包布局,$ORIGIN行为也可以扩展到Apple平台:

if(APPLE)
 set(basePoint @loader_path)
else()
 set(basePoint $ORIGIN)
endif()
include(GNUInstallDirs)
file(RELATIVE_PATH relDir
 ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}
 ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}
)
set(CMAKE_INSTALL_RPATH ${basePoint} ${basePoint}/${relDir})

当使用了Apple包或框架,Apple的布局就会与其他平台完全不同,上面的策略就没用了。对于这种情况,有不同的策略来定义运行时搜索路径。例如,一个macOS应用包在安装了相关目标后,或者在构建后复制了框架(只显示了包结构的相关部分),最终可能会形成如下结构:

上面RPATH的信息可以通过将myApp的INSTALL_RPATH目标属性设置为@executable_path/..来实现。fmwk1和fmwk2会设置为@loader_path/../../。要支持构建后框架复制的情况,还可以在构建时使用安装RPATH信息。省略了构建后框架复制和代码签名的细节,这样的安排可能看起来像这样:

set(CMAKE_BUILD_WITH_INSTALL_RPATH YES)
set(CMAKE_BUILD_WITH_INSTALL_NAME_DIR YES)

add_executable(myApp MACOSX_BUNDLE ...)
add_library(fmwk1 SHARED ...)
add_library(fmwk2 SHARED ...)
target_link_libraries(myApp PRIVATE fmwk1) # Only needs fmwk1 directly...
target_link_libraries(fmwk1 PRIVATE fmwk2) # ... but fmwk1 needs fmwk2

set_target_properties(myApp PROPERTIES
 INSTALL_RPATH @executable_path/../Frameworks
)
set_target_properties(fmwk1 fmwk2 PROPERTIES
 FRAMEWORK TRUE
 INSTALL_RPATH @loader_path/../../..
)

如果项目的策略是只在安装时嵌入框架,那么像下面这样就行了:

install(TARGETS fmwk1 fmwk2 myApp
 BUNDLE DESTINATION .
 FRAMEWORK DESTINATION myApp.app/Contents/Frameworks
)

另一方面,如果项目希望在构建时嵌入框架,可以相对容易地实现构建后步骤,如下一个示例所示。但是请注意,TARGET_BUNDLE_DIR和TARGET_BUNDLE_CONTENT_DIR生成器表达式仅在CMake 3.9或更高版本中可用。

add_custom_command(TARGET myApp POST_BUILD
 COMMAND rsync -a
   $<TARGET_BUNDLE_DIR:fmwk1>
   $<TARGET_BUNDLE_DIR:fmwk2>
   $<TARGET_BUNDLE_CONTENT_DIR:myApp>/Frameworks/
)

上面的复制步骤有健壮性问题,比如不能删除旧内容,但是对于某些情况来说,它已经足够好了,至少是一个好的起点。

如果包需要签名,那么CMake通常不会很好地支持嵌入框架。如在第22章中强调的那样,Apple假设代码签名是由Xcode作为构建过程的一部分来处理的,而不是作为安装后的步骤,CMake在签名过程中提供的帮助很少。当前,如果项目希望使用嵌入式框架签署包,必须使用自己的逻辑实现。

如果项目希望为iOS创建通用二进制文件(有时也称为宽二进制文件),则会出现另一个复杂问题。构建可以是设备端的,也可以是模拟器端的。通常只安装一种架构,可以通过CMake 3.5及以后版本提供了IOS_INSTALL_COMBINED目标属性。如果此属性为true,那么当为设备构建安装目标时,它还将构建模拟器体系结构,安装时将两者合并为单个二进制文件。反之亦然,安装模拟器构建也会构建和安装。如果该目标属性有关,则该特性依赖于项目自己实现的代码签名逻辑。

涉及到在框架中嵌入头文件时,CMake提供了更多的帮助。如22.3节所述,目标可以在PUBLIC_HEADER和PRIVATE_HEADER目标属性中列出公共和私有头文件。在安装框架本身的过程中安装它们,不需要进一步配置。当这些相同的目标在非Apple平台上构建时,不会有任何框架结构来容纳头文件(目标文件将视为动态库),但头文件仍然可以安装到指定位置:

install(TARGETS myShared
 FRAMEWORK # Apple framework case
     DESTINATION ...
 LIBRARY # Non-Apple case
     DESTINATION ...
 PUBLIC_HEADER
     DESTINATION ...
 PRIVATE_HEADER
     DESTINATION ...
)

25.3. 安装导出

安装目标时,可以使用带有EXPORT选项的install(TARGETS),EXPORT选项会指定导出集的名称。然后可以使用命令的另一种形式安装该导出集:

install(EXPORT exportName
 DESTINATION dir
 [FILE name.cmake]
 [NAMESPACE namespace]
 [PERMISSIONS permissions...]
 [EXPORT_LINK_INTERFACE_LIBRARIES]
 [COMPONENT component]
 [EXCLUDE_FROM_ALL]
 [CONFIGURATIONS configs...]
)

安装导出集会在指定的目标目录中创建具有指定名称的name.cmake文件(必须以.cmake结尾)。如果没有提供FILE选项,则使用基于exportName的默认文件名。生成的文件将包含CMake命令为每个导入目标的出口集合。此文件的目的是让其他项目包含它,以便可以引用此项目的目标,并拥有关于接口属性和目标间关系的完整信息。通过一些限制,项目可以像对待自己的常规目标一样对待导入的目标。这些导出文件通常不能直接包含,它们用于配置包,然后其他项目使用find_package()命令来找到配置包(这在25.7节中有更详细的介绍)。

如果指定了NAMESPACE选项,那么在创建与之关联的导入目标时,每个目标都将在其名称前加上命名空间。考虑下面的例子:

add_library(myShared SHARED ...)
add_library(BagOfBeans::myShared ALIAS myShared)

install(TARGETS myShared
 EXPORT BagOfBeans
 DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(EXPORT BagOfBeans
 DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/BagOfBeans
 NAMESPACE BagOfBeans::
)

上面的示例遵循了16.4节的建议,其中每个常规目标有一个与之关联的命名空间别名。当为非别名myShared目标安装导出时,可以使用与别名目标相同的名称空间(即BagOfBeans::)。这允许导出引用目标的详细信息,就像这个项目引用别名一样(BagOfBeans::myShared)。项目可以选择通过add_subdirectory()直接添加这个项目,或者通过find_package()拉入导出文件,不管选择了哪个方法,仍然可以使用相同的BagOfBeans::myShared目标名。在CMake社区中,这个重要的模式正在成为一个相当普遍的方式,因此尝试遵循它,能更好的兼容大多数项目。

EXPORT关键字之后给出的导出集的名称不必与NAMESPACE相关。命名空间通常与项目名称紧密关联,但是可以使用一系列不同的策略来命名导出集。例如,项目可以定义多个导出集,其目标共享单个命名空间,并且导出集可能对应于整个安装逻辑单元。这些导出集可能与安装COMPONENT一一对应,也可能收集多个组件。以下是这些案例的演示:

# Single component export
install(TARGETS algo1 EXPORT MyProj_algoFree
 DESTINATION ... COMPONENT MyProj_free
)
install(EXPORT MyProj_algoFree
 DESTINATION ... COMPONENT MyProj_free
)
# Multi component export
install(TARGETS algo2 EXPORT MyProj_algoPaid
 DESTINATION ... COMPONENT MyProj_licensed_A
)
install(TARGETS algo3 EXPORT MyProj_algoPaid
 DESTINATION ... COMPONENT MyProj_licensed_B
)
install(EXPORT MyProj_algoPaid
 DESTINATION ... COMPONENT MyProj_licensed_dev
)

在上面的示例中,导出集只包含algo1目标,它是. 0MyProj_free 组件的成员。导出文件也是MyProj_free组件的成员,因此在安装该组件时,库和导出文件将一起安装。对于多组件导出,情况有所不同,其中导出集包含来自MyProj_licensed_A组件的algo2和来自MyProj_licensed_B组件的algo3,但导出文件在它自己的单独组件中。因此,可以使用或不使用导出文件安装目标,这取决于是否安装MyProj_licensed_dev组件。

上面的多组件导出案例,强调了如何安装导出集和组件。安装导出文件,而不同时安装导出文件指向的实际目标是错误的。因此,如果用户安装了MyProj_licensed_dev组件,那么也必须安装MyProj_licensed_A和MyProj_licensed_B组件。

install(EXPORT)命令的其余选项中,一些选项具有与install(TARGETS)的效果类似。PERMISSIONS、EXCLUDE_FROM_ALL和CONFIGURATIONS选项应用于已安装的导出文件,而不是目标本身,但在其他方面是等效的。install(EXPORT)使用的目标由项目决定,但是遵循一些约定可能会有用。这样做的动机与导出的文件用作配置包的一部分的方式有关,所以关于这个主题的讨论会在25.7节之后进行。

EXPORT_LINK_INTERFACE_LIBRARIES选项用于支持CMake 3.0之前的行为,并与链接接口库相关。不鼓励使用它,建议项目至少以3.0作为CMake的最低版本。

有一个非常类似的install()命令的形式,专门用于导出Android ndk-build项目的目标:

install(EXPORT_ANDROID_MK exportName
 DESTINATION dir
 [FILE name.mk]
 [NAMESPACE namespace]
 [PERMISSIONS permissions...]
 [EXPORT_LINK_INTERFACE_LIBRARIES]
 [COMPONENT component]
 [EXCLUDE_FROM_ALL]
 [CONFIGURATIONS configs...]
)

install(EXPORT)创建一个供其他CMake项目使用的文件,install(EXPORT_ANDROID_MK)创建一个Android.mk,ndk构建可以包含该文件。Android.mk提供了附加到导出目标的所有使用要求,因此ndk-build项目将知道所有编译器定义、头文件搜索路径等需要的信息。可以使用FILE选项更改导出文件的名称,但名称必须以.mk结尾。所有其他选项与install(EXPORT)方式具有相同的行为。install(EXPORT_ANDROID_MK)需要CMake 3.7或更高版本,但项目至少需要3.11版本,以避免受私有依赖关系静态库的bug的影响。

某些情况下,无法导出文件进行安装。示例场景包括针对主构建的不同平台编译的子构建,或者由于目标名称冲突、滥用CMAKE_SOURCE_DIR等变量而不能直接添加到主构建中的第三方项目。对于这些情况,CMake提供了export()命令,可以直接将导出文件写入到构建树中:

export(EXPORT exportName
 [NAMESPACE namespace]
 [FILE fileName]
)

除了要编写导出文件外,上面的操作基本上相当于简化的install(EXPORT)命令。尽管文件名可以包含路径(仍然以.cmake结尾),但可用选项的简化与用于install(EXPORT)时具有相同的含义。其他形式的export()命令允许导出单个目标,如果定义了导出集,那么上面的方式是最容易使用和维护的。

25.4. 安装文件和目录

与目标相比,安装单个文件和目录不那么复杂。文件安装可以使用以下形式:

install(<FILES | PROGRAMS> files...
 DESTINATION dir
 [RENAME newName]
 [PERMISSIONS permissions...]
 [COMPONENT component]
 [EXCLUDE_FROM_ALL]
 [OPTIONAL]
 [CONFIGURATIONS configs...]
)

大多数选项已经很熟悉了,它们的含义与install(TARGETS)相同。install(FILES)和install(PROGRAMS)之间的区别是,如果没有PERMISSIONS,后者会在默认情况下添加执行权限。这是为了安装像shell脚本需要可执行,但不是CMake的目标。RENAME选项只能操作单个文件,允许在安装时给该文件一个新名称。

某些情况下,项目可能希望安装与导入的目标相关联的二进制文件,但是install(TARGETS)不允许直接安装导入目标。解决这个问题的一种方法是将与导入的目标关联的文件安装为普通文件。与目标相关联的使用需求将不会保留,但至少可以安装二进制文件。使用时, $<TARGET_FILE:…>生成器表达式和其他类似的表达式特别有用。这样做的缺点是,将处理所有平台差异的责任放回到项目身上,这对于导入的库目标来说是有问题的。

# Assume myImportedExe is an imported target for an executable not built by this project
install(PROGRAMS $<TARGET_FILE:myImportedExe>
 DESTINATION ${CMAKE_INSTALL_BINDIR}
)

安装目录遵循与文件类似的模式,扩展了支持的选项集:

install(DIRECTORY dirs...
 DESTINATION dir
 [FILE_PERMISSIONS permissions... | USE_SOURCE_PERMISSIONS]
 [DIRECTORY_PERMISSIONS permissions...]
 [COMPONENT component]
 [EXCLUDE_FROM_ALL]
 [OPTIONAL]
 [CONFIGURATIONS configs...]
 [MESSAGE_NEVER]
 [FILES_MATCHING]
 # The following block can be repeated as many times as needed
 [ [PATTERN pattern | REGEX regex]
   [EXCLUDE]
   [PERMISSIONS permissions...] ]
)

没有任何可选参数的情况下,对于每个dirs位置,从该点开始的整个目录树将安装到目标目录中。如果源名称以斜杠结尾,则复制源目录的内容,而不是复制源目录本身。

# Results in somewhere/foo/...
install(DIRECTORY foo DESTINATION somewhere)

# Results in somewhere/...
install(DIRECTORY foo/ DESTINATION somewhere)

COMPONENT、EXCLUDE_FROM_ALL、OPTIONAL和CONFIGURATIONS选项与其他install()命令具有相同的含义。MESSAGE_NEVER选项禁止为每个已安装文件提供日志消息,但是也有人可能会说,不应该使用它来确定与其他已安装内容的一致性。

支持选项来分别控制文件和目录的权限。如果给定了USE_SOURCE_PERMISSIONS,则安装的每个文件将保留与其源文件相同的权限。FILE_PERMISSIONS将覆盖该权限并使用指定的权限。如果两个选项都没有给出,文件将具有相同的默认权限,就像使用了install(FILE)命令一样。对于安装过程中创建的目录,可以使用DIRECTORY_PERMISSIONS选项来覆盖默认值,除了添加执行权限之外,默认值与文件操作相同。

其余选项允许根据一个或多个通配符模式,或正则表达式,对文件集进行筛选。针对每个文件和目录的完整路径(总是用斜杠指定,即使在Windows上也是如此)测试每个模式或正则表达式。通配符模式必须匹配完整路径的末端,而不仅仅是中间的某个部分,而正则表达式可以匹配路径的任何部分,因此更加灵活。如果模式或正则表达式后面跟着EXCLUDE关键字,将不会安装所有匹配的文件和目录。这是一种只从目录树中排除少数特定内容的方法,也可以通过在模式或REGEX块之前提供FILES_MATCHING关键字来实现相反的方法,这意味着将只安装那些确实匹配模式或正则表达式的文件和目录。如果既没有给出FILES_MATCHING,也没有给出EXCLUDE,那么模式或正则表达式的唯一效果就是使用PERMISSIONS覆盖权限。

例子应该有助于澄清以上几点。下面的示例改编自CMake文档,安装src目录及其以下的所有头文件,并保留目录结构。

install(DIRECTORY src/
 DESTINATION include
 FILES_MATCHING
 PATTERN *.h
)

下一个例子复制样本代码和一些脚本,覆盖后者的权限,以确保它们是可执行的:

install(DIRECTORY src/
 DESTINATION samples
 FILES_MATCHING
 REGEX "example\\.(h|c|cpp|cxx)"
 PATTERN *.txt
 PATTERN *.sh
 PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
             GROUP_READ GROUP_EXECUTE
             WORLD_READ WORLD_EXECUTE
)

安装文档,跳过一些隐藏文件:

install(DIRECTORY doc/ todo/ licenses
 DESTINATION doc
 FILES_MATCHING
 REGEX \\.(DS_Store|svn) EXCLUDE
)

下一个示例忽略了任何FILES_MATCHING或EXCLUDE选项,这样模式和正则表达式只会修改权限,而不过滤文件和目录列表:

install(DIRECTORY admin_scripts
 DESTINATION private
 PATTERN *.sh
   PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
                               GROUP_READ GROUP_EXECUTE
)

所有情况下,install(DIRECTORY)都保留了源文件的目录结构。若在安装区域中创建一个空目录,源列表可以为空,但仍将创建DESTINATION。

install(DIRECTORY DESTINATION somewhere/emptyDir)

25.5. 自定义安装逻辑

某些情况下,仅仅将内容复制到安装区域是不够的。可能需要在安装过程中执行处理,例如重写文件的某些部分或以编程方式生成内容。对于这些情况,CMake提供了向安装步骤添加自定义逻辑的能力。

install(SCRIPT fileName | CODE cmakeCode
 [COMPONENT component]
 [EXCLUDE_FROM_ALL]
)

CODE形式可将CMake命令直接作为单个字符串嵌入,而SCRIPT形式将使用include()在安装时读取脚本。请注意,在安装过程中调用自定义代码的位置是未指定的,但目前的行为是install()命令通常在目录范围内处理它们(但这并不扩展install()调用内嵌套的子目录)。

多个SCRIPT和/或CODE可以组合在一个命令中,将按照指定的顺序执行。COMPONENT和EXCLUDE_FROM_ALL选项不能重复给出。

install(CODE [[ message("Starting custom script") ]]
        SCRIPT myCustomLogic.cmake
        CODE [[ message("Finished custom script") ]]
        COMPONENT MyProj_Runtime
)

25.6. 安装的依赖关系

创建包时,常见的是自包含。这可以扩展到项目构建的构件中,还包括外部依赖项,如编译器运行时库。CMake提供了一些模块,可以使这个任务更容易。

InstallRequiredSystemLibraries模块旨在为项目提供主要编译器的相关运行时库的详细信息。包括Intel(所有主要平台)和Visual Studio(仅适用于Windows)。使用模块相当简单,项目可以选择让模块定义install()命令,也可以请求填充相关的变量,以便创建必要的命令。最简单的情况下,尽管建议为install()命令设置组件,但项目也可以依赖默认值。

set(CMAKE_INSTALL_SYSTEM_RUNTIME_COMPONENT MyProj_Runtime)
include(InstallRequiredSystemLibraries)

默认安装位置是Windows的bin,和其他平台的lib。这可能与大多数项目的典型安装布局相匹配,这里可以用CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION变量覆盖:

include(GNUInstallDirs)
if(WIN32)
 set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION ${CMAKE_INSTALL_BINDIR})
else()
 set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION ${CMAKE_INSTALL_LIBDIR})
endif()
set(CMAKE_INSTALL_SYSTEM_RUNTIME_COMPONENT MyProj_Runtime)
include(InstallRequiredSystemLibraries)

如果项目希望自己定义install()命令,则需要在包含模块之前将CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP设置为true。然后项目可以使用CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS变量访问运行时库列表:

set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE)
include(InstallRequiredSystemLibraries)
include(GNUInstallDirs)
if(WIN32)
 install(FILES ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS}
 DESTINATION ${CMAKE_INSTALL_BINDIR}
 )
else()
 install(FILES ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS}
 DESTINATION ${CMAKE_INSTALL_LIBDIR}
 )
endif()

当使用Intel编译器时,默认的install()命令安装的不仅仅是CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS的内容。还通过文档变量安装了一些没有提供给项目的目录。对于那些有兴趣研究这些附加内容是否合适的开发人员,可以在模块的实现中搜索CMAKE_INSTALL_SYSTEM_RUNTIME_DIRECTORIES,看看这些附加内容是如何构造的。

使用Visual Studio编译器安装其他运行时组件(如Windows Universal CRT、MFC和OpenMP库)时,还可以使用其他一些控件。还可以强制安装运行时库的调试版本。这些在模块文档中都有清楚的描述,有兴趣的读者可以参考那里了解更多的细节。

另一对模块也可以用于安装项目的运行时依赖项。BundleUtilities和GetPrerequisites模块采用不同的方法,使用特定于平台的工具直接查询已安装的二进制文件,并递归地复制丢失的库。这些模块非常难以使用,并且通常不适合处理编译器运行时依赖关系。有时候,它们可以有效地查找和安装不可预测的依赖项,例如对于复杂的跨平台工具包,如Qt (DeployQt4模块广泛地使用这两个模块)。大多数项目最好花精力找出它们的实际依赖关系,并直接安装它们,以确保构建过程更可控和可靠,也可以选择使用InstallRequiredSystemLibraries来处理编译器运行时的依赖关系。

25.7. 编写配置包文件

安装的项目要想让其他CMake项目使用它,首选的方法是提供一个配置包文件。这个文件是通过使用find_package()命令的项目找到的,正如在23.5节中介绍的那样。配置文件的名称必须匹配以下两种形式之一:

  • <packageName>Config.cmake

  • <lowercasePackageName>-config.cmake

上面的第一种形式可能更常见一些,并且与后面讨论的CMake提供的其他功能一致,但在其他方面两者是等价的。该文件将为安装的项目提供的库和可执行文件提供导入目标。如果安装的基点添加到CMAKE_PREFIX_PATH变量中,配置文件安装到的目录应该是find_package()搜索的默认位置之一。这样可以确保配置文件很容易找到。在第23.5节中,将搜索的位置为:

<prefix>/
<prefix>/(cmake|CMake)/
<prefix>/<packageName>*/
<prefix>/<packageName>*/(cmake|CMake)/
<prefix>/(lib/<arch>|lib*|share)/cmake/<packageName>*/
<prefix>/(lib/<arch>|lib*|share)/<packageName>*/
<prefix>/(lib/<arch>|lib*|share)/<packageName>*/(cmake|CMake)/
<prefix>/<packageName>*/(lib/<arch>|lib*|share)/cmake/<packageName>*/
<prefix>/<packageName>*/(lib/<arch>|lib*|share)/<packageName>*/
<prefix>/<packageName>*/(lib/<arch>|lib*|share)/<packageName>*/(cmake|CMake)/

在Apple平台上,还可以搜索以下子目录:

<prefix>/<packageName>.framework/Resources/
<prefix>/<packageName>.framework/Resources/CMake/
<prefix>/<packageName>.framework/Versions/*/Resources/
<prefix>/<packageName>.framework/Versions/*/Resources/CMake/
<prefix>/<packageName>.app/Contents/Resources/
<prefix>/<packageName>.app/Contents/Resources/CMake/

显然,这是一组很大的备选项,但最佳选择在某种程度上取决于项目希望如何安装。在打包到Linux发行版中时,发行版本身可能有关于这些文件应该放在哪里的策略。与其强迫每个发行版向项目提供自己的补丁,以确保配置文件根据其策略安装,不如项目提供一种将所需的详细信息传递到构建的方法。缓存变量是实现这一目的的理想选择,因为项目可以指定默认值,但是可以在不更改项目的情况下重写它。在没有任何其他约束的情况下,两个非常简单且常用的位置是<prefix>/cmake<prefix>/lib/cmake/<packageName>,后者对多体系结构的部署更友好一些(参见下面的示例)。

install(EXPORT_ANDROID_MK)命令会为项目提供一个Android.mk文件,CMake对其位置没有特定约定。合理的安排是在包布局中使用专用的ndk-build目录,但这最终取决于项目。

25.7.1. CMake项目的配置文件

对于简单的CMake项目,只使用单一的导出集,没有依赖,install(EXPORT)命令可以直接用来创建一个基本的配置文件:

include(GNUInstallDirs)
install(EXPORT myProj
 DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProj
 NAMESPACE MyProj::
 FILE MyProjConfig.cmake
)

请注意目标如何使用由GNUInstallDirs模块定义的CMAKE_INSTALL_LIBDIR缓存变量,来增加Linux发行版不需要进行任何更改的可能性。GNUInstallDirs模块已经解决了常见的情况,通过定义缓存变量,允许在需要时进行简单的定制。

实践中,配置文件通常不是这样直接生成。更常见的情况是,准备单独的配置文件,通过include()命令引入导出文件。一个稍微扩展的示例使用两个导出集演示了该方案:

MyProjConfig.cmake

include("${CMAKE_CURRENT_LIST_DIR}/MyProj_Runtime.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/MyProj_Development.cmake")

CMakeLists.txt

# Define targets, etc...

# Create two separate export sets installed to the same place
# and a manually written config file that will include them
include(GNUInstallDirs)
install(EXPORT MyProj_Runtime
 DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProj
 NAMESPACE MyProj::
 FILE MyProj_Runtime.cmake
 COMPONENT MyProj_Runtime
)
install(EXPORT MyProj_Development
 DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProj
 NAMESPACE MyProj::
 FILE MyProj_Development.cmake
 COMPONENT MyProj_Development
)
install(FILES MyProjConfig.cmake
 DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProj
)

MyProjConfig.cmake非常简单,不需要外部提供依赖关系,并且配置文件假设运行时和开发组件总是都安装的。然后考虑一个场景,其中运行时组件依赖于其他名为BagOfBeans的包。配置文件负责确保BagOfBeans中所需的目标可用,通常通过调用find_package()来实现。方便起见,有时候可以使用CMakeFindDependencyMacro模块中的find_dependency()宏作为find_package()的包装器,处理QUIET和REQUIRED关键字。find_dependency()宏还有一个附加行为,如果它没有找到所请求的包,配置文件的处理将立即停止,控制权返回给调用者。这就好像是在find_dependency()失败后立即进行了return()。在实践中,这是简单、干净的依赖项规范,以及对依赖项失败的优雅处理。

MyProjConfig.cmake

include(CMakeFindDependencyMacro)
find_dependency(BagOfBeans)
include("${CMAKE_CURRENT_LIST_DIR}/MyProj_Runtime.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/MyProj_Development.cmake")

项目作者应该知道,find_dependency()包含一个优化,如果它检测到请求的包已经在前面找到,就会绕过调用。除非以后的调用需要请求不同的包组件,否则这种方法可以正常工作。find_dependency()第一次成功时,会锁定找到的组件集。如果稍后调用find_dependency()传递不同的组件,则忽略。因此,如果依赖项支持包组件,项目应该直接调用find_package(),并自己处理QUIET和REQUIRED选项。这些选项作为变量${CMAKE_FIND_PACKAGE_NAME} _find_quiet${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED传递到配置文件。始终使用${CMAKE_FIND_PACKAGE_NAME}而不是硬编码包名,因为可能存在大小写差异。

unset(extraArgs)
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_QUIETLY)
 list(APPEND extraArgs QUIET)
endif()
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED)
 list(APPEND extraArgs REQUIRED)
endif()
find_package(BagOfBeans COMPONENTS Foo Bar ${extraArgs})

如果项目希望自己的组件是可选的,那么配置文件的复杂性就会增加。支持这种功能所涉及的步骤可归纳如下:

  • 构建需要找到的项目组件集。从find_package()中的必需和可选组件集开始,添加满足项目依赖关系的组件。

  • 计算出该项目组件集所需的外部依赖集。有些是强制的,有些是可选的,因此需要派生两个独立的外部依赖集。

  • 查找外部依赖项,如果任何必需的依赖项未能加载,项目查找操作也必须失败,控制应该立即返回一个适当的错误消息。缺少可选的外部依赖项不会导致失败或错误消息。

  • 更新项目组件集,以删除依赖于缺少可选外部依赖项的组件。如果删除的组件本身是其他组件的依赖项,则可能需要进一步的筛选项目组件集。

  • 加载剩余的项目组件。

如果没有指定组件,项目需要决定要做什么。这可以视为所有组件都指定为可选组件或是必需组件。另一种策略是加载基本组件的最小集合,并忽略其他组件。最合适的策略将取决于项目组成部分的性质。请求组件集将在${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS变量中可用,如果组件指定为必需的,而不是可选的,那么${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED_<comp>对该组件的值为true。

配置文件不应该使用message()报告错误,而是应该将错误消息存储在一个名为${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE的变量中。然后,find_package()将获取该错误,并将其包装为关于错误在项目何处产生的详细信息。${CMAKE_FIND_PACKAGE_NAME}_FOUND应该设置为false以表示失败。这允许find_package()正确地实现不使用REQUIRED关键字的调用。如果包配置文件使用了message(FATAL_ERROR…),那么调用者永远不能将包视为可选的。

# Work out the set of components to load
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS)
 set(comps ${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS)
 # Ensure Runtime is included if Development was specified
 if(Development IN_LIST comps AND NOT Runtime IN_LIST comps)
 list(APPEND comps Runtime)
 endif()
else()
 # No components given, look for all components
 set(comps Runtime Development)
endif()

# Find external dependencies, storing comps in a safer variable name.
# In this example, BagOfBeans is only needed by the Development component.
set(${CMAKE_FIND_PACKAGE_NAME}_comps ${comps})
if(Development IN_LIST ${comps})
 find_dependency(BagOfBeans)
endif()

# Check all required components are available before trying to load any
foreach(comp IN LISTS ${CMAKE_FIND_PACKAGE_NAME}_comps)
 if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED_${comp} AND
 NOT EXISTS ${CMAKE_CURRENT_LIST_DIR}/MyProj${comp}.cmake)
 set(${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE
 "MyProj missing required dependency: ${comp}")
 set(${CMAKE_FIND_PACKAGE_NAME}_FOUND FALSE)
 return()
 endif()
endforeach()

foreach(comp IN LISTS ${CMAKE_FIND_PACKAGE_NAME}_comps)
 # All required components are known to exist. The OPTIONAL keyword
 # allows the non-required components to be missing without error.
 include(${CMAKE_CURRENT_LIST_DIR}/MyProj${comp}.cmake OPTIONAL)
endforeach()

上面的示例演示了,检查是否能够满足所需的组件之前不创建任何导入目标。

与配置文件密切相关的是版本文件。如果提供了版本文件,名字应该符合两种形式之一,并且必须和配置文件在同一目录下:

  • <packageName>ConfigVersion.cmake

  • <lowercasePackageName>-config-version.cmake

版本文件名的形式通常遵循与其相关配置文件相同的形式(例如,FooConfigVersion.cmake会和FooConfig.cmake搭配,而foo-config-version.cmake通常与foo-config.cmake配对)。版本文件是通知find_package()包是否满足指定的版本要求。find_package()在加载版本文件之前会设置一些变量:

  • PACKAGE_FIND_NAME

  • PACKAGE_FIND_VERSION

  • PACKAGE_FIND_VERSION_MAJOR

  • PACKAGE_FIND_VERSION_MINOR

  • PACKAGE_FIND_VERSION_PATCH

  • PACKAGE_FIND_VERSION_TWEAK

  • PACKAGE_FIND_VERSION_COUNT

这些变量包含作为findpackage()的VERSION参数的版本信息。如果没有给出这样的参数,那么PACKAGE_FIND_VERSION将为空,而其他PACKAGE_FIND_VERSION*变量将为0。PACKAGE_FIND_VERSION_COUNT对版本组件进行了计数,其余变量有其明显的含义。版本文件需要检查请求的信息与实际版本的包,然后设置以下变量:

PACKAGE_VERSION

这是实际的包版本,通常是major.minor.patch.tweak(不是所有的组件都是必需的)。

PACKAGE_VERSION_EXACT

只有在包版本和请求版本完全匹配时才设置为true。

PACKAGE_VERSION_COMPATIBLE

只有在包版本与请求版本兼容时才设置为true。如何决定兼容性取决于包本身。对于20.3节中提到的语义版本化原则的项目,变量将按照以下规则设置:

  • 如果缺少任何版本组件,将其视为0。

  • 如果主版本号不同,则结果为false。

  • 如果主版本号相同,次版本号小于所需的版本,则结果为false。

  • 如果主版本号和次版本号相同,包的补丁版本号小于所需的版本,则结果为false。

  • 如果主版本号、次版本号和补丁版本号相同,软件包的微调版本号小于所需的版本,则结果为false。

  • 对于所有其他情况,结果为true。

PACKAGE_VERSION_UNSUITABLE

只有在版本文件需要表明包不能满足任何版本要求时才设置为true(基本上包没有版本号,所以任何版本要求都应该视为失败)。

find_package()命令将使用此信息将以下变量返回给调用者(这里的返回值将实际的包的版本,而不是版本需求传递到find_package()命令中):

  • <packageName>_VERSION

  • <packageName>_VERSION_MAJOR

  • <packageName>_VERSION_MINOR

  • <packageName>_VERSION_PATCH

  • <packageName>_VERSION_TWEAK

  • <packageName>_VERSION_COUNT

虽然项目可以自由创建版本文件,但更简单、更健壮的方法是使用CMakePackageConfigHelpers模块提供的write_basic_package_version_file()命令:

write_basic_package_version_file(outFile
 [VERSION requiredVersion]
 COMPATIBILITY compat
)

如果给定了VERSION参数,那么requiredVersion应该是major.minor.patch.tweak形式,但只有主部分是强制性的。如果没有给出VERSION选项,则使用PROJECT_VERSION变量(由project()命令设置)。兼容性选项指定了如何确定兼容性的策略。compat参数必须是下列值之一(注意,大多数名称有一点误导):

AnyNewerVersion

包版本必须等于或大于指定的版本。

SameMajorVersion

包版本必须等于或大于指定的版本,并且包版本号的主部分必须与requiredVersion中的相同。这与语义版本化的兼容性要求相同。

SameMinorVersion

包版本必须等于或大于指定的版本,并且包版本号的主部分和次部分必须与requiredVersion中的相同。只有CMake 3.11或更高版本才支持这种选择。

ExactVersion

软件包版本号的主要、次要和补丁部分必须与requiredVersion中的相同。调整部分被忽略了。这一策略尤其具有误导性,目前正在进行讨论,会将其废弃,以支持一个新的、更清晰的策略。

CMakePackageConfigHelpers模块还提供了另一个有用的命令。configure_package_config_file()命令的目的是为了给路径处理提供一些便利,使项目更容易定义可重定位的包。大多数项目通常不需要它,但是当包配置文件需要相对于基本安装位置,而不是配置文件本身的位置来引用安装文件时,它提供了一种更简单的方法。该命令的形式如下:

configure_package_config_file(inputFile outputFile
 INSTALL_DESTINATION path
 [INSTALL_PREFIX prefix]
 [PATH_VARS var1 [var2...] ]
 [NO_SET_AND_CHECK_MACRO]
 [NO_CHECK_REQUIRED_COMPONENTS_MACRO]
)

应该使用该命令替换configurefile(),以复制<Project>Config.cmake.in,并将其替换。它将用做转换为绝对路径的<somevar>的内容,替换为`@PACKAGE <somevar>@中的变量。原始内容视为相对于基本安装位置的内容。需要用PATH_VARS选项,以这种方式转换的每个变量。要使此功能发挥作用,要在替换的变量之前使用,输入文件的顶部或附近必须有@PACKAGE_INIT@`。

相对于INSTALL_PREFIX, INSTALL_DESTINATION是outputFile将安装到的目录。当省略INSTALL_PREFIX时,默认为CMAKE_INSTALL_PREFIX。INSTALL_PREFIX通常只在outputFile用于构建树而不是安装的情况下提供(例如,它与export(EXPORT)命令一起使用)。

NO_SET_AND_CHECK_MACRO和NO_CHECK_REQUIRED_COMPONENTS_MACRO选项阻止@PACKAGE_INIT@定义辅助函数。在导入目标成为提供包目标的首选方式之前,需要使用变量。为了实现这点,configure_package_config_file()提供了一个set_and_check()宏,只会在尚未定义变量的情况下设置变量。提供导入目标的项目不需要这个宏,可以添加NO_SET_AND_CHECK_MACRO来防止定义。同样,当所有信息都通过变量提供时,在返回之前检查是否在末尾设置了所有必需的变量。为此定义了一个名为check_required_components()的宏,但是提供导入目标的项目本身应该执行这些检查,并且只有在找到所有组件时才定义导入目标。这使得check_required_components()宏变得多余。

一个例子应该有助于阐明这个命令的用法:

CMakeLists.txt

include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
set(cmakeModulesDir cmake)
configure_package_config_file(MyProjConfig.cmake.in MyProjConfig.cmake
 INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProj
 PATH_VARS cmakeModulesDir
 NO_SET_AND_CHECK_MACRO
 NO_CHECK_REQUIRED_COMPONENTS_MACRO
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/MyProjConfig.cmake
 DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProj
 COMPONENT ...
)

MyProjConfig.cmake.in

@PACKAGE_INIT@

list(APPEND CMAKE_MODULE_PATH "@PACKAGE_cmakeModulesDir@")

# Include the project's export files, etc...

25.7.2. 非cmake项目的配置文件

配置文件机制并不仅限于CMake构建的项目,它也可以用于非CMake的项目(尽管这还不是很常见)。虽然CMake项目可以利用各种CMake功能更容易创建所需的文件,非CMake项目必须手动定义它们。对于这样的项目,保持文件的简单也很重要,因为它们很可能由不太熟悉CMake的人维护。第一步是放弃对组件的支持,只将包作为导入目标的简单集合提供。对于只需要提供库的项目,下面的例子展示了一个小的配置文件,可以作为一个很好的起点:

# Compute the base point of the install by getting the directory of this
# file and moving up the required number of directories
set(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_DIR}")
foreach(i RANGE 1 NumSubdirLevels) ①
 get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH)
 if(_IMPORT_PREFIX STREQUAL "/")
 set(_IMPORT_PREFIX "")
 break()
 endif()
endforeach()

# Use a prefix specific to this project
set(projPrefix MyProj)

# Example of defining a static library imported target
add_library(${projPrefix}::myStatic STATIC IMPORTED)
set_target_properties(${projPrefix}::myStatic PROPERTIES
 IMPORTED_LOCATION "${_IMPORT_PREFIX}/lib/libmyStatic.a" ②
)

# Example of defining a shared library imported target with version details
add_library(${projPrefix}::myShared SHARED IMPORTED)
set_target_properties(${projPrefix}::myShared PROPERTIES
 IMPORTED_LOCATION "${_IMPORT_PREFIX}/lib/libmyShared.so.1.6.3" ③
 IMPORTED_SONAME "libmyShared.so.1" ④
)

# Another example of a shared library, this time for Windows
add_library(${projPrefix}::myDLL SHARED IMPORTED)
set_target_properties(${projPrefix}::myDLL PROPERTIES
 IMPORTED_LOCATION "${_IMPORT_PREFIX}/bin/myShared.dll"
 IMPORTED_IMPLIB "${_IMPORT_PREFIX}/lib/myShared.lib" ⑤
)

①NumSubdirLevels是该配置文件在基本安装点以下的子目录级别数。例如,如果文件在lib/cmake/Foo/FooConfig.cmake中找到,那么NumSubdirLevels是3。

②指定库相对于基本安装点的路径,在安装点之前找到,并存储在_IMPORT_PREFIX中。

③这个示例展示了Linux等平台,动态库版本号如何放在文件名的末尾。

④对于支持sonames的平台,IMPORTED_SONAME实际上是将链接嵌入到这个目标的二进制文件中的名称。Apple平台上,通常会有一个包含@rpath和一些子目录的组件。

⑤还必须提供与DLL关联的导入库的位置,以便能够链接到。如果目的只是提供DLL(例如,它在运行时可用,但不用于直接链接),IMPORTED_IMPLIB可以省略,但这种情况不太常见

上面的内容是基本的,各种IMPORTED_…属性需要为每个平台进行定制,但是非CMake项目可以自由地使用它认为方便的机制来生成安装配置文件的内容。为了增加健壮性,每个导入库只有在不存在的情况下才会添加,如下所示:

if(NOT TARGET ${projPrefix}::myStatic)
 add_library(${projPrefix}::myStatic STATIC IMPORTED)
 set_target_properties(${projPrefix}::myStatic PROPERTIES
     IMPORTED_LOCATION "${_IMPORT_PREFIX}/lib/libmyStatic.a"
 )
endif()

25.8. 总结

安装是一个重要的主题,需要良好的计划和对预期部署平台的理解。对于一个项目来说,最初只关注一个平台,或者只关注最终打算使用的一组平台的子集是很常见的,但是任何安装和部署计划会导致在项目发布周期的后期不得不处理意外的复杂性和平台差异。项目应该清楚地了解所安装的文件和目录结构,以及最终将支持的一整套打包方案。这在很大程度上影响了项目的结构,包括如何在库之间划分功能,以及二进制文件中需要显示哪些符号等基本内容。

项目应该尽可能遵循标准的布局。使用像GNUInstallDirs这样的模块可以极大地简化这个任务,即使对于Windows上的包也是如此。如果不可能,或者不希望使用,项目仍然可以考虑相同的目录结构是否可以在不同的平台上使用,以简化应用程序开发。

强烈鼓励项目使其包可重定位。除非软件包需要安装到一个非常特定的位置,可重定位软件包有显著的优势。为最终用户提供了更大的灵活性,更容易支持广泛的打包系统,并且在开发期间更容易测试。

默认安装点的选择是特定于平台的,CMake提供的默认设置并不总是理想的,但是包的创建通常会覆盖它们。避免在安装路径中包含包版本号,特别是对于可重定位的包。由于不同的使用场景会调用不同的目录结构,而这些目录结构可能与特定于版本的路径不兼容,因此倾向于让用户自行决定安装。项目也应该倾向于遵循适当的标准,比如Linux的文件系统层次标准(通常也适用于大多数其他基于Unix的平台,Apple除外)。

当定义目标有使用需求时,使用$<BUILD_INTERFACE:…>生成器表达式来正确地表示构建使用的头文件搜索路径。对于任何安装的库目标,对于install(TARGETS)命令最好使用INCLUDES DESTINATION来设置头文件搜索路径,而不是在目标本身上使用$<INSTALL_INTERFACE:…>生成器表达式,并确保INCLUDES DESTINATION使用相对于安装点的相对路径。

add_library(foo ...)

# Not ideal: embeds build paths in installed export files
target_include_directories(foo PUBLIC ${CMAKE_CURRENT_BINARY_DIR})

# Better: separate paths for build and install, with the latter
# added as part of the install() command rather than with the target
include(GNUInstallDirs)
target_include_directories(foo PUBLIC
 $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
)
install(TARGETS foo ...
 INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

始终为已安装的目标分配组件,并使用特定于项目的组件名称。当项目作为大型项目层次结构的一部分使用时,允许父项目控制子组件的处理方式。一个好模式的例子是<ProjectName>_<componentname>,例如MyProj_Runtime。安装导出集时,使用相同的项目名称作为命名空间,并附加两个冒号(即MyProj::)。遵循这些命名约定将使已安装的项目更加直观。更重要的是,还可以避免与其他项目包的名称冲突。

如果项目提供了其他项目需要链接的库,那么最好为运行时支持和开发定义单独的组件。这允许父项目重用运行时组件来打包动态库和执行所需的内容,并避免打包仅用于开发的目标,如静态库、头文件等。还减少了包维护人员(例如Linux发行版)的工作,因为包经常分解为运行时包和开发包。

包配置文件中,始终没有创建导入的目标,除非find_package()调用成功。这意味着在创建任何导入的目标之前,所有必需的组件都必须可用,所有必需的目标依赖项都应该存在。要引入依赖项,使用CMakeFindDependencyMacro模块中的find_dependency(),而不是从包配置文件中调用find_package(),除非依赖项支持包组件。如果调用find_package()引入依赖项,请确保将QUIET和REQUIRED选项正确地传递到依赖项的find_package()调用中。还可以使用适当的变量来定义成功/失败,并将错误消息报告给原始的find_package()命令,而不是调用message(FATAL_ERROR…)或类似的命令。

请使用InstallRequiredSystemLibraries模块来处理编译器运行时依赖项的安装,这使得项目避免了为不同的Visual Studio版本、SDK、工具集选择等寻找合适的文件,而重复所有复杂的逻辑。如果对Intel编译器的支持很重要,请理解此模块默认情况下安装的各种库,并决定是否需要这些库。特别是使用OpenMP的项目,很可能希望使用默认的安装命令,而不是定义自己的安装命令,这样就不必手动定义所需的库。

Last updated