1. 简介
通过上一篇CMake 构建系统中的概念解析文章,相信大家对CMake中的基本概念有了深入的理解,本文将介绍如何使用CMake来完成软件的安装。这里所谓的安装类似于Windows平台上的各种应用的安装程序要完成的事情,只是没有UI界面罢了。当然,我们也可以使用CMake来制作那些安装程序,并且这部分内容也会在后续的文章中介绍。而本文则是制作安装程序的前置条件。本文基于Installing Files,增加了必要的解释便于理解。
通常情况下,通过构建得到的软件会安装在与源代码和构建树分开的目录中。这允许软件开发商以一种“干净”的形式将软件发布给用户,并且将用户与构建过程的细节隔离开来。CMake提供了install
命令来指定如何安装一个项目。该命令由CMakeLists文件中的项目调用,并告诉CMake如何生成安装脚本。这些脚本在安装时执行,以执行文件的实际安装。对于Makefile生成器(UNIX、NMake、MinGW等。),用户只需运行make install
(或者nmake install
),make工具将调用CMake的安装模块以完成软件的安装过程。基于GUI的系统(Visual Studio、Xcode等),用户只需构建名为INSTALL
的目标便可以得到相同的结果。
每调用一次install
命令,便会相应生成一些安装规则(而不是真正进行安装)。在一个CMakeLists文件(源目录)中,将按照调用相应命令的顺序来评估这些规则。在CMake 3.14中,跨多个目录的评估顺序发生了变化。
install
命令有多个签名,它们是为几种常见的安装用例而设计的。每次对命令的调用都需要将签名指定为第一个参数。可用的签名为TARGETS
, FILES
或者PROGRAMS
, DIRECTORY
, SCRIPT
, CODE
和EXPORT
,解释如下:
install(TARGETS …)
:安装与项目内构建的目标所相对应的二进制文件。install(FILES …)
:通用文件安装,通常用于软件所需的头文件、文档和数据文件。install(PROGRAMS …)
:安装并非由本项目构建的可执行文件,如shell脚本。该签名等同于install(FILES)
只是已安装文件的默认权限包含可执行位。install(DIRECTORY …)
:安装整个目录树。它可以用于安装带有资源(如图标和图像)的目录。install(SCRIPT …)
:指定安装期间要执行的CMake脚本文件。这通常用于为其他规则定义安装前或安装后的操作。install(CODE …)
:指定安装期间要执行的CMake代码。这类似于install (SCRIPT)
但是代码是作为字符串在调用中内联提供的。install(EXPORT …)
:生成并安装CMake文件,该文件包含将目标从安装树导入到另一个项目中的代码。
TARGETS
, FILES
, PROGRAMS
,以及DIRECTORY
签名都是为了创建针对文件的安装规则。要安装的目标、文件或目录在签名名称参数之后列出。可以使用特定关键字参数,并在其后跟相应的值来指定其他详细信息。大多数签名都支持的关键字参数如下。
DESTINATION
:此参数指定安装规则放置文件的位置,后面必须跟一个指示该位置的目录路径。如果目录被指定为完整路径,它将在安装时被评估为绝对路径。如果目录被指定为相对路径,它将在安装时追加安装前缀而形成最终的路径。前缀可以由用户通过缓存变量CMAKE_INSTALL_PREFIX
来设置。CMake提供了特定于平台的默认值:在UNIX中使用/usr/local,在Windows中使用<SystemDrive>/Program Files/<ProjectName>
,其中SystemDrive
类似于C:
,ProjectName
是project
命令给予的最顶层名称。PERMISSIONS
:此参数指定要在已安装的文件上设置的文件权限。可用于重写由特定的install
命令签名指定的默认权限。有效权限包括OWNER_READ
,OWNER_WRITE
,OWNER_EXECUTE
,GROUP_READ
,GROUP_WRITE
,GROUP_EXECUTE
,WORLD_READ
,WORLD_WRITE
,WORLD_EXECUTE
,SETUID
,以及SETGID
。有些平台并不完全支持以上列出的所有权限,在这样的平台上,这些权限名称会被忽略。CONFIGURATIONS
:此参数指定安装规则适用的构建配置列表(例如,调试、发布等)。对于Makefile生成器,构建配置由CMAKE_BUILD_TYPE
变量指定。对于Visual Studio和Xcode生成器,用户在install
目标构建时选择需要的构建配置。仅当当前安装配置与为此参数提供的列表中的条目匹配时,才会真正评估该安装规则。配置名称进行字符串匹配时不区分大小写。COMPONENT
:此参数指定安装规则适用的安装组件。一些项目将它们的安装分成多个组件进行单独打包。例如,一个项目可以定义一个包含运行工具所需文件的Runtime
组件;一个包含构建工具扩展所需文件的Development
组件;和一个包含手册网页和其他帮助文件的Documentation
组件。然后,项目可以通过一次只安装一个组件来单独打包每个组件进行分发。默认情况下,会安装所有组件。特定于组件的安装是一个高级特性,供软件包维护人员使用。它需要手动调用安装脚本,并带有一个参数来定义COMPONENT
变量以命名所需的组件。请注意,组件名称不是由CMake定义的。每个项目可以定义自己的组件集。OPTIONAL
:此参数指定如果要安装的输入文件不存在,这不是错误。如果输入文件存在,它将按照要求安装。如果它不存在,它将被静默不安装。
2. 安装目标文件
项目通常会安装在构建过程中创建的一些库和可执行文件。install
命令提供了TARGETS
签名用于达成此目的。
TARGETS
关键字后面紧跟一个使用add_executable
或者add_library
生成的目标列表。每个目标对应的一个或多个文件将被安装。
使用此签名安装的文件可以分为几类,例如ARCHIVE
, LIBRARY
,或者RUNTIME
。对文件进行分类的目的是希望在安装时可以根据类型的不同而将其安装至对应的典型目录。相应的关键字参数是可选的,但如果存在,请指定仅适用于该类型的其他参数。目标文件分类如下:(参看输出工件了解详情)
- 可执行文件-
RUNTIME
:由add_executable
创建(在Windows上为.exe,在UNIX上没有扩展名) - 可加载模块-
LIBRARY
:由带有MODULE
选项的add_library
创建(Windows上的.dll。在UNIX上的.so) - 共享库-
LIBRARY
:在类UNIX平台上,由带有SHARED
选项的add_library
创建(在大多数UNIX上为.so。Mac上为.dylib) - 动态链接库-
RUNTIME
:Windows平台上,由带有SHARED
选项的add_library
创建(.dll) - 导入库-
ARCHIVE
:一个可链接文件,由导出符号的动态连接库创建(大多数Windows上为.lib,Cygwin和MinGW上为.dll.a)。 - 静态库-
ARCHIVE
:由带有STATIC
选项的add_library
创建(Windows上的.lib。在UNIX、Cygwin和MinGW上为.a)
例如,有一个定义可执行文件的项目myExecutable
,它链接到一个共享库mySharedLib
。它还提供了一个静态库myStaticLib
和一个插件模块myPlugin
,它还被链接到了共享库。可执行文件、静态库和插件文件可以使用多个install
命令单独安装:
install(TARGETS myExecutable DESTINATION bin)
install(TARGETS myStaticLib DESTINATION lib/myproject)
install(TARGETS myPlugin DESTINATION lib)
只有在可执行文件链接到的共享库也被已安装后,可执行文件才能够在安装位置运行。为了支持所有平台,函数库的安装需要多加小心。它们必须安装到各个平台上可以被动态链接器搜索到的位置。在类UNIX的平台上,库通常安装在lib目录,而在Windows上,它应该放在可执行文件所在的bin目录中。另一个挑战是,与Windows上的共享库相关联的导入库应该像静态库一样对待,并安装到lib/myproject。换句话说,我们用同一个目标名称,在不同的平台上将会创建三种不同的文件,而且它们必须安装到三个不同的目的地!幸运的是,这个问题可以通过使用类型关键字参数来解决。可以使用以下命令来生成跨平台的安装规则:
install(TARGETS mySharedLib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib/myproject)
这将指示CMake:
RUNTIME
文件(.dll)应该安装到bin;LIBRARY
文件(.so)应该安装到libARCHIVE
(.lib)文件应该安装到lib/myproject。
在UNIX上LIBRARY
文件将被安装;在Windows上RUNTIME
和ARCHIVE
文件将被安装。
如果上面的示例项目被打包成多个组件,例如Runtime
和Development
组件,我们必须为每个安装的目标文件分配适当的组件。运行应用程序需要可执行文件、共享库和插件,因此它们属于Runtime
组件。同时,导入库(对应于Windows上的共享库)和静态库只需要用于开发应用程序,因此属于Development
组件。
组件分配可以通过COMPONENT
参数来完成。您还可以将所有安装规则合并到一个命令调用中,这相当于为上述所有命令添加了组件参数。每个目标生成的文件都将使用对应于其类别的规则进行安装。
install(TARGETS myExecutable mySharedLib myStaticLib myPlugin
RUNTIME DESTINATION bin COMPONENT Runtime
LIBRARY DESTINATION lib COMPONENT Runtime
ARCHIVE DESTINATION lib/myproject COMPONENT Development)
在某些平台上,多版本的共享库可能有一个符号链接,例如:
lib<name>.so -> lib<name>.so.1
lib<name>.so.1
是函数库的名字,lib<name>.so
是一个“名称链接”(namelink),它可以帮助链接器找到通过-l<name>
给定的库。NAMELINK_ONLY
选项使得在安装函数库目标时只安装名称链接。NAMELINK_SKIP
选项会使得在安装函数库目标时安装除名称链接之外的库文件。当两个选项都没有给出时,两个部分都被安装。在版本化的共享库没有名称链接的平台上,或者当一个库没有版本化时,NAMELINK_SKIP
选项将安装库,而NAMELINK_ONLY
选项不安装任何东西。
3. 安装普通文件
项目可能会安装并非由add_executable
或者add_library
创建的文件,如头文件或文档。通用目的的文件安装规则是使用FILES
签名来声明的。
FILES
关键字后面紧跟着要安装的文件列表,文件列表中的相对路径是相对于当前源文件目录而言的。文件将安装到给定的DESTINATION
目录。例如:
install(FILES my-api.h ${CMAKE_CURRENT_BINARY_DIR}/my-config.h
DESTINATION include)
该命令生成的规则将安装源代码树下的my-api.h文件和构建树下的my-config.h到相对于安装前缀的include目录。默认情况下,已安装的文件被赋予以下权限:OWNER_WRITE
, OWNER_READ
, GROUP_READ
,以及WORLD_READ
,但这可以通过指定PERMISSIONS
选项来改写。例如,用户希望在UNIX系统上安装一个只有其所有者(即执行安装过程的用户)可读的全局配置文件。我们可以用如下命令来完成这个任务:
install(FILES my-rc DESTINATION /etc
PERMISSIONS OWNER_WRITE OWNER_READ)
它将安装my-rc文件到绝对路径/etc下,并且只有拥有者才有对其进行读/写的权限.
RENAME
参数指定已安装文件的名称,该名称可以不同于原始文件名。仅当安装单个文件时,才允许重命名。例如:
install(FILES version.h DESTINATION include RENAME my-version.h)
该命令将源目录下的version.h文件安装到相对于安装前缀的include/my-version.h路径。
4. 安装程序文件
项目还可能安装预制的工具程序,比如shell脚本或Python脚本,这些程序并不是由CMake构建得到的。这些程序也可以使用FILES
签名进行安装,并且通过PERMISSIONS
选项添加可执行权限。然而,因为这种情况很常见,以至于提供这么一个更加便利的接口是有意义的,因此CMake提供了PROGRAMS
签名用于此目的。
PROGRAMS
关键字后面紧跟着要安装的脚本列表。该命令与FILES
签名相同,只是默认权限还包括OWNER_EXECUTE
, GROUP_EXECUTE
,以及WORLD_EXECUTE
。例如,我们可以使用以下命令安装一个Python脚本:
install(PROGRAMS my-util.py DESTINATION bin)
该命令将my-util.py程序安装到相对于安装前缀的bin目录,并授予它所有者、组、全局可读和可执行权限,以及所有者可写权限。
5. 安装目录
项目还可能发布一个完整的目录,其中包含了资源文件,如图标或html文档。可以使用DIRECTORY
签名安装整个目录。
DIRECTORY
关键字后面紧跟着要安装的目录列表,相对路径是相对于当前源文件目录而言的。列表中的每个命名目录都被安装到目标目录中。复制目标目录时,每个目录名中的最后一部分被附加到目标目录。例如:
install(DIRECTORY data/icons DESTINATION share/myproject)
这将data/icons的内容从源目录安装到安装前缀下的share/myproject/icons目录。如果输入目录以斜杠结尾,则表示将输入目录的内容安装到指定的目的地。例如:
install(DIRECTORY doc/html/ DESTINATION doc/myproject)
该命令将doc/html的内容从源目录安装到安装前缀下的doc/myproject目录中(而不是doc/myproject/html)。如果没有给出输入目录名,如:
install(DIRECTORY DESTINATION share/myproject/user)
这将创建目标目录,但不会在其中安装任何东西。
由DIRECTORY
签名安装的文件的权限与FILES
签名相同。用DIRECTORY
签名安装的目录则被赋予与PROGRAMS
签名相同的权限(因为目录需要执行权限)。FILE_PERMISSIONS
和DIRECTORY_PERMISSIONS
选项可用于覆盖这些默认值。考虑这么一个例子:一个包含shell脚本的目录将被安装到一个对所有者和组都可写的目录中。我们可以使用如下命令:
install(DIRECTORY data/scripts DESTINATION share/myproject
FILE_PERMISSIONS
OWNER_READ OWNER_EXECUTE OWNER_WRITE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE
DIRECTORY_PERMISSIONS
OWNER_READ OWNER_EXECUTE OWNER_WRITE
GROUP_READ GROUP_EXECUTE GROUP_WRITE
WORLD_READ WORLD_EXECUTE
)
这将data/scripts目录安装到share/myproject/scripts中,并设置所需的权限。在某些情况下,由项目创建的输入目录可能已经设置了所需的权限。那我们可以使用USE_SOURCE_PERMISSIONS
选项告诉CMake在安装过程中使用输入目录中的文件和目录的权限。如果在前面的示例中,我们已经为输入目录设置好了正确的权限,则可以使用以下命令:
install(DIRECTORY data/scripts DESTINATION share/myproject
USE_SOURCE_PERMISSIONS)
通常,输入目录中可能有您不希望安装的额外子目录,例如存放源代码版本管理元信息的目录(如.git等)。也可能有特定的文件不应该安装或安装不同的权限,而大多数文件获得默认值。PATTERN
和REGEX
选项可用于此目的。PATTERN
选项后首先是一个globbing模式,然后是一个EXCLUDE
或者PERMISSIONS
选项。REGEX
选项后面的参数首先是正则表达式,然后是EXCLUDE
或者PERMISSIONS
。EXCLUDE
选项声明在安装时,那些与前面的模式或表达式匹配的文件或目录需要被排除,而PERMISSIONS
选项则为它们分配特定的权限。
每个输入文件和目录都作为带正斜杠的完整路径与模式或正则表达式进行匹配。模式将只匹配出现在完整路径末尾的完整文件名或目录名,而正则表达式可能匹配任何部分。例如,模式foo*
将匹配.../foo.txt
,但不能匹配.../myfoo.txt
或者.../foo/bar.txt
;然而,正则表达式foo
会与前面的几个例子匹配。
回到上面安装图标目录的例子,考虑这样一种情况,输入目录由git管理,还包含一些我们不想安装的额外文本文件。那我们可以使用下列命令进行排除:
install(DIRECTORY data/icons DESTINATION share/myproject
PATTERN ".git" EXCLUDE
PATTERN "*.txt" EXCLUDE)
这个命令将安装图标目录,同时忽略任何包含.git的目录或所有文本文件。使用REGEX
选项的等效命令如下:
install(DIRECTORY data/icons DESTINATION share/myproject
REGEX "/.git$" EXCLUDE
REGEX "/[^/]*.txt$" EXCLUDE)
它使用“/”和“$”来实现与模式相同的匹配。考虑一个类似的情况,其中输入目录包含shell脚本和文本文件,我们希望在安装时为其设置与其他文件不同的权限。例如:
install(DIRECTORY data/other/ DESTINATION share/myproject
PATTERN ".git" EXCLUDE
PATTERN "*.txt"
PERMISSIONS OWNER_READ OWNER_WRITE
PATTERN "*.sh"
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)
该命令将data/other的内容从源目录安装到share/myproject,同时忽略.git目录并分别为.txt和.sh给予特定的权限。
6. 指定安装时需额外执行的脚本
项目安装可能需要执行一些额外的任务,而不仅仅是将文件拷贝到安装树中。第三方软件包可能会提供自己的机制来注册新插件,这些插件必须在项目安装期间调用。为此,CMake提供了SCRIPT
签名。
SCRIPT
关键字后面紧跟CMake脚本的名称,CMake将在安装过程中执行这些脚本。如果给定的文件名是相对路径,则是相对于当前源文件目录而言的。一个简单的用例是在安装过程中打印消息。我们首先编写一个message.cmake文件,包含下列内容:
message("Hello from ${CMAKE_CURRENT_LIST_FILE}")
然后使用以下命令引用该脚本:
install(SCRIPT message.cmake)
在主CMakeLists文件处理期间,并不会执行自定义安装脚本;它们是在安装过程中执行的。在包含install (SCRIPT)
的脚本中定义的变量和宏定义将无法从被包含的脚本访问。但是,在脚本执行期间,CMake会定义某些变量可被脚本使用,例如可用于获取安装信息。变量CMAKE_INSTALL_PREFIX
会被设置为实际的安装前缀。这可能与相应的缓存变量值不同,因为安装脚本可能由使用不同安装前缀的打包工具执行。环境变量ENV{DESTDIR}
可以由用户或打包工具设置。它的值被添加到安装前缀和绝对安装路径之前,以确定文件的安装位置。为了引用磁盘上的安装位置,自定义脚本可以使用$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}
作为路径的顶部。变量CMAKE_INSTALL_CONFIG_NAME
会被设置为当前正在安装的构建配置的名称(调试、发布等。)。在特定于组件的安装过程中,变量CMAKE_INSTALL_COMPONENT
会被设置为当前组件的名称。
7. 指定安装时需额外执行的代码
对于某些非常简单的指令,例如前面提到的安装脚本中使用的消息打印命令,使用内嵌在install
命令的代码将更加方便。CODE
签名可用于实现此目的。
CODE
关键字后面紧跟一个字符串,该字符串包含要放在安装脚本中的代码。可以使用以下命令创建安装时消息:
install(CODE "MESSAGE(\"Installing My Project\")")
其效果与message.cmake脚本相同,只不过是通过内联代码实现。
8. 安装必需的共享库
通常我们会在构建可执行文件时链接许多共享库。当您安装这样的可执行文件时,您还必须安装其必备的共享库,称为“必需的库”(prerequisites),因为可执行文件需要它们的存在才能正确加载和运行。共享库的三个主要来源分别是:操作系统本身、您自己项目的构建的函数库以及属于外部项目的第三方库。来自操作系统的那些函数库可能不需要安装任何东西就可以存在:因为它们常常作为运行可执行文件的基础平台而存在。您自己项目中的构建产品可能已经在CMakeLists文件中通过add_library
构建规则完成了构建,因此为它们创建CMake安装规则应该很简单。然而,第三个来源,即第三方库经常成为维护成本较高的部分,尤其是当项目依赖了很多第三方库时,或者这些库经常需要更换版本时。函数库可能被添加,代码可能被重组,第三方共享库本身实际上可能有其他额外的必需的库,而这些额外的库并不那么直观可见。
为解决这个问题,CMake提供了一个模块BundleUtilities
,以便更容易处理所需的共享库。本模块提供fixup_bundle
函数,通过指定相对于可执行文件的明确定义的位置来复制和修复必备的共享库。对于Mac bundle应用程序,它将函数库嵌入包中,用install_name_tool
形成一个独立的单元。在Windows上,它将库复制到可执行文件所在的目录中,因为可执行文件会在自己的目录中搜索所需的dll。
fixup_bundle
函数帮助您创建可重定位的安装树。Mac用户都喜欢自包含的bundle应用程序:这意味着你可以把它们拖到任何地方,双击它们,它们都可以工作。除了操作系统本身,它们不依赖于安装在某个位置的任何东西。类似地,没有管理权限的Windows用户喜欢可重定位的安装树,其中可执行文件和所有必需的dll都安装在同一个目录中,这样无论您将它安装在哪里,它都可以工作。你甚至可以在安装后移动它们,它仍然可以工作。
为了使用fixup_bundle
,我们需要首先声明安装一个可执行目标。然后,配置一个可以在安装时调用的CMake脚本。在配置好的CMake脚本中,include BundleUtilities
并使用适当参数调用fixup_bundle
函数。下面给出一个简单的例子。
在CMakeLists.txt中:
install(TARGETS myExecutable DESTINATION bin)
# To install, for example, MSVC runtime libraries:
include(InstallRequiredSystemLibraries)
# To install other/non-system 3rd party required libraries:
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/FixBundle.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/FixBundle.cmake
@ONLY
)
install(SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/FixBundle.cmake)
在主CMakeLists中,我们声明了一个可执行文件安装规则,然后通过include(InstallRequiredSystemLibraries)
指定了一些系统库的安装规则。然后,我们配置了一个CMake脚本FixBundle.cmake,该脚本将在安装时调用。最后,我们使用install (SCRIPT)
指定FixBundle.cmake
作为在安装阶段需要执行的额外脚本。
在FixBundle.cmake.in中:
include(BundleUtilities)
# Set bundle to the full path name of the executable already
# existing in the install tree:
set(bundle
"${CMAKE_INSTALL_PREFIX}/myExecutable@CMAKE_EXECUTABLE_SUFFIX@")
# Set other_libs to a list of full path names to additional
# libraries that cannot be reached by dependency analysis.
# (Dynamically loaded PlugIns, for example.)
set(other_libs "")
# Set dirs to a list of directories where prerequisite libraries
# may be found:
set(dirs
"@CMAKE_RUNTIME_OUTPUT_DIRECTORY@"
"@CMAKE_LIBRARY_OUTPUT_DIRECTORY@"
)
fixup_bundle("${bundle}" "${other_libs}" "${dirs}")
在这个例子中,我们使用@VAR@
语法来指定一些变量,这些变量在配置时将被替换为实际的值。这些变量包括CMAKE_INSTALL_PREFIX
,CMAKE_EXECUTABLE_SUFFIX
,CMAKE_RUNTIME_OUTPUT_DIRECTORY
和CMAKE_LIBRARY_OUTPUT_DIRECTORY
。fixup_bundle
函数接受三个参数:
bundle
参数指定需要查找依赖库的可执行文件的路径;other_libs
参数指定无法使用依赖分析找到的函数库,例如前面提到的插件(可动态加载的共享库);dirs
参数指定了一些目录,从这些目录中查找依赖库。
另外,您需要自行验证您是否有权限复制和分发可执行文件的必备共享库。一些函数库可能有限制性的软件许可证,对于这些函数库,禁止使用fixup_bundle
进行复制。
9. Reference
- Installing Files. 2023/07/08.