第26章:打包

发布包的创建是开发人员经常感到无力的部分。对于希望掌握跨平台创建健壮的、表现良好的包来说,各种打包系统、平台差异和约定都有一条非常陡峭的学习曲线。每个包管理系统总是使用它自己独特的输入规范,对包含的内容、应该如何安装、包组件之间的关联、如何与操作系统集成等等。平台之间,甚至同一平台的不同发行版之间的差异并不明显,通常只有在遇到不可预见的行为或约束时才会了解到(Windows路径长度限制和Linux上不同的系统库目录的名称就是很好的例子)。

尽管有这些差异,在使用的包方面,也是有共同点的。虽然每个系统或平台可能有不同的实现,但许多打包功能可以以通用的方式描述。CMake和CPack利用了这一点,并提供了定义良好的接口来指定这些共通方面,然后将其转换为系统输入文件或命令,以生成各种格式的包。这为开发人员提供了更短的学习曲线,从而为跨相关平台生成包提供了捷径。

CMake和CPack不仅抽象了打包的共同点,还简化了许多打包器的特性。通过为这些简单的接口,CMake和CPack使开发人员能够以更熟悉的方式开发更高级的打包特性。大多数情况下,这是通过设置几个相关的变量,或使用适当的参数调用函数来实现的,所有这些参数都在每个包程序的CMake模块文档中定义。

CPack打包在内部实现为一个或多个暂存区域的安装,然后使用该区域生成最终的包。这些安装是通过调用install()命令来控制的,上一章介绍了这个过程的第一部分,本章会介绍了这个过程的第二部分,描述了指定包元数据和包本身配置的变量和命令。

26.1. 打包的基础知识

打包设置和执行与测试的处理方式类似。cpack读取一个输入文件,并根据该文件的内容生成相应的包。如果命令行上没有显式地提供输入文件,cpack将使用当前目录中的CPackConfig.cmake。这个输入文件通常由CMake通过包含CPack模块生成,就像包含CTest模块为CTest生成输入文件一样。项目可以通过CMake变量和命令自定义打包输入文件的内容。

CPack模块支持基于目标平台的几种默认格式。要创建的格式集可以在cpack命令行上使用-G选项覆盖。如果需要建立多种格式,可以使用分号分隔,如下所示:

cpack -G "ZIP;RPM"

如果CMake配置为使用Xcode或Visual Studio这样的多配置生成器,cpack需要知道它应该打包哪个配置的可执行文件。可以通过给cpack一个-C选项来完成的(单配置CMake生成器会忽略-C选项):

cpack -C Release

cpack命令还支持其他选项,-G和-C是两个常用的选项,其他信息通过输入文件提供。这是因为开发人员可以构建包构建目标,而不是直接调用cpack,该目标将首先构建默认的all目标,然后使用最少的选项调用cpack。因此,对于项目来说,确保cpack输入文件定义了必需的设置,会更加方便一些。如果构建树的顶层包含名为CPackConfig.cmake的文件,CMake将自动创建包目标。

创建cpack输入文件的最简单方法是包含cpack模块,对于整个CMake项目只执行一次。这种包含通常在顶层CMakeLists.txt文件的末尾附近,直接或通过子目录的CMakeLists.txt完成。以项目是否有父项目作为包含条件,还可以确保项目仅在本身为顶层项目时,才会定义打包。例如:

cmake_minimum_required(VERSION 3.0)
project(MyProj)
# ...Define targets, add subdirectories, etc...

# End of the CMakeLists.txt file
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
 add_subdirectory(packaging) # include(CPack) will happen in here
endif()

包含CPack模块时,CPackConfig.cmake文件保存到构建树的顶层(即CMAKE_BINARY_DIR)。由于CMake只在处理完CMakeLists.txt文件后才检查这个文件。如果包含CPack模块,便会创建包的构建目标。

虽然为打包配置的大多数方面提供了默认值,但这些默认值并不总合适。大多数项目希望在包含CPack模块之前设置一些基本信息,以提供更好的替代方案。特别是,建议在调用include(CPack)之前设置以下变量:

CPACK_PACKAGE_NAME

包名是元数据中基本的部分之一。用作包的默认文件名的一部分,可能出现在UI安装程序的不同位置,并且很可能是用户用来引用项目的名称。理想情况下,不会包含空格,因为在某些上下文中,空格会替换成其他字符。如果没有显式设置该变量,则使用CMAKE_PROJECT_NAME作为默认值。

CPACK_PACKAGE_DESCRIPTION_SUMMARY

这个变量提供了关于项目的简短描述,适合在空间受限的包列表中显示,并且让终端用户了解该包的用处。也可能在其他情况下显示给用户,仅用于显示信息。CMake 3.9中,默认值取自CMAKE_PROJECT_DESCRIPTION,而在早期的CMake版本中,默认值是空字符串。

CPACK_PACKAGE_VENDOR

供应商通常只用于获取信息,而不会影响包的结构或行为。默认的Humanity通常不适合做任何事情,除非正确设置。这里可以使用真实的公司或组织名称,而不是域名。

CPACK_PACKAGE_VERSION_MAJOR, CPACK_PACKAGE_VERSION_MINOR, CPACK_PACKAGE_VERSION_PATCH

它们用于构造整个包版本,可作为包文件名的一部分出现在包的元数据和安装程序UI中。版本信息是打包的关键部分,项目应该始终显式地设置。默认值为0,1,1只作为占位符,不应该用于正式的发布包。一种方便的使用模式是,给project()命令提供相关的版本信息:

set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR})
set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH})

cpack根据这三个变量填充CPACK_PACKAGE_VERSION,但这只会在cpack运行时发生,因此CMake处理期间不会填充CPACK_PACKAGE_VERSION。CMake 3.12中,这些变量的默认值来自CMAKE_PROJECT_VERSION_MAJOR、CMAKE_PROJECT_VERSION_MINOR和CMAKE_PROJECT_VERSION_PATCH变量,这些变量在CMake 3.12中添加。这些变量是通过顶层CMakeLists.txt文件中project()命令的版本信息设置的,因此可以提供合理的默认值,而不是相当随意的0、1和1。也就是说,当项目为顶层项目,可以依赖这些变量来提供默认值,但实际情况可能并不总是这样。因此,始终显式地将它们设置为项目真正想要值则更为安全。

CPACK_PACKAGE_INSTALL_DIRECTORY

一些包程序会将其附加到基本安装中,以创建特定于包的目录。其默认值可以不同,可能包括包名和版本。默认值中不太合适出现版本号,例如对于能够升级项目的安装程序。为了确保更好的默认行为,项目可能将其设置为与CPACK_PACKAGE_NAME相同的值。

CPACK_VERBATIM_VARIABLES

应该始终显式地将该变量设置为true。它确保写入cpack配置文件的所有内容都正确地转义。默认值为false只是为了保持与早期CMake版本的兼容,但是旧的行为会导致格式不正确的配置文件,因此不推荐使用。

通常会设置更多的变量来改善用户体验,特别是对于UI安装:

CPACK_PACKAGE_DESCRIPTION_FILE

这是一个文本文件的名称,其中包含对项目较长的描述。文件的内容可能会在安装程序的介绍性页面中显示,或者添加到包的元数据中。始终使用文件的绝对路径。作为另一种选择,描述可以直接作为名为CPACK_PACKAGE_DESCRIPTION变量的内容提供。虽然在CMake 3.11或更早的版本中没有文档说明这一点,但在CMake的早期版本中就提供了支持。

CPACK_RESOURCE_FILE_WELCOME

安装程序在开始页面上会显示欢迎信息。此变量指定一个文件名,指定在欢迎页面上应该显示的内容。如果没有设置,那么对于那些显示欢迎消息的安装程序,CPack会提供默认值作为占位符,但这是一个相对较差的方案,不适合正式发布版本。如果发布有欢迎页面的安装程序,项目应该需要设置这个选项。并且始终使用文件的绝对路径。

CPACK_RESOURCE_FILE_LICENSE

大多数UI安装程序都会向用户显示许可页面,并要求用户在继续安装前接受许可。许可显示的文本取自这个变量命名的文件。如果没有设置变量,则使用一些通用的占位符文本,但是强烈建议项目提供自己的许可信息。始终使用文件的绝对路径。

CPACK_RESOURCE_FILE_README

一些UI安装程序会提供单独的页面,显示由该变量命名的文件的内容。可以在用户继续安装前向他们提供一些信息,并且默认情况下会使用通用的文本。如果想要创建显示这些页面的安装程序,项目应该通过这个变量提供一个包含更合适内容的文件。始终使用文件的绝对路径。

CPACK_PACKAGE_ICON

通常可以设置这个变量,但是请注意,大多数包生成器对于包内关联位置的图标的格式和使用都有自己的不同要求。有些生成器会忽略这个变量,而有些则以不同的方式。

遵循上述准则的示例:

set(CPACK_PACKAGE_NAME MyProj)
set(CPACK_PACKAGE_VENDOR MyCompany)
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "CPack example project")
set(CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})
set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR})
set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH})
set(CPACK_VERBATIM_VARIABLES YES)
set(CPACK_PACKAGE_DESCRIPTION_FILE ${CMAKE_CURRENT_LIST_DIR}/Description.txt)
set(CPACK_RESOURCE_FILE_WELCOME ${CMAKE_CURRENT_LIST_DIR}/Welcome.txt)
set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_LIST_DIR}/License.txt)
set(CPACK_RESOURCE_FILE_README ${CMAKE_CURRENT_LIST_DIR}/Readme.txt)
include(CPack)

为了方便在不带参数的情况下运行cpack,并使用包构建目标,应该将CPACK_GENERATOR变量设置为所需的格式。如果没有设置,将使用默认生成器支持的格式。由于并不是所有平台都支持或适合所有格式,因此设置此变量需要逻辑来指定格式。下面的示例为目标平台选择了一种通用的存档格式和一种原生的包格式:

if(WIN32)
 set(CPACK_GENERATOR ZIP WIX)
elseif(APPLE)
 set(CPACK_GENERATOR TGZ productbuild)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
 set(CPACK_GENERATOR TGZ RPM)
else()
 set(CPACK_GENERATOR TGZ)
endif()

CPack模块还定义了生成源码包的信息。创建了一个CPackSourceConfig.cmake,可以使用CPackConfig.cmake代替。当项目配置为使用Makefile或Ninja生成器时,还会定义一个package_source构建目标。生成源码包相对简单,下面两个命令都可以实现相同的效果。

# All build generators
cpack -G TGZ --config CPackSourceConfig.cmake

# Makefile and Ninja build generators only
cmake --build . --target package_source

源码包包含整个源目录树。可以使用CPACK_SOURCE_IGNORE_FILES变量过滤掉源树的某些部分,其中包含一个正则表达式列表,用于比较每个完整的文件路径,所有匹配的文件将从源包中删除。这个变量默认忽略.git、.svn等存储库目录,以及一些常见的临时文件。如果一个项目覆盖了CPACK_SOURCE_IGNORE_FILES,将需要指定了相关的模式。为了避免正则表达式中的转义和引号问题,强烈建议将CPACK_VERBATIM_VARIABLES设置为true。

set(CPACK_VERBATIM_VARIABLES YES)
set(CPACK_SOURCE_IGNORE_FILES
 /\\.git/
 \\.swp
 \\.orig
 /CMakeLists\\.txt\\.user
 /privateDir/
)

26.2. 组件

如果项目的install()命令中没有定义任何组件,那么所有包生成器都将生成一个包含所有已安装内容的单包。当项目定义了组件时,就为如何打包提供了更大的灵活性。还可以在组件之间指定关系,从而允许定义分层组件结构,并在安装时强制执行组件之间的依赖关系。每个包生成器都以不同的方式使用这些组件的信息,一些生成器为不同的组件创建单包,而另一些生成器则在单个UI安装程序中显示用户可选择的组件。一些安装程序甚至支持在安装时按需下载组件。

上一章演示了如何将组件定义为install()的一部分。这些命令只将内容分配给组件,而不定义任何组件信息。组件之间的关系是使用CPackComponent模块指定的,CPackComponent模块作为包含CPack模块的一部分会自动包含。这些命令还为组件提供额外的元数据,一些安装程序在安装期间使用这些组件向用户显示信息。

CPackComponent模块中最重要的命令是cpack_add_component(),它描述了单个组件:

cpack_add_component(componentName
 [DISPLAY_NAME name]
 [DESCRIPTION description]
 [DEPENDS comp1 [comp2...] ]
 [GROUP group]
 [REQUIRED | DISABLED]
 [HIDDEN]
 [INSTALL_TYPES type1 [type2...] ]
 [DOWNLOADED]
 [ARCHIVE_FILE archiveFileName]
 [PLIST plistFileName]
)

虽然所有的关键字都是可选的,但至少要提供DISPLAY_NAME和DESCRIPTION,以便在安装期间向用户提供有意义的信息,并使非UI安装程序有足够的元数据供用户理解包的用途。如果只在安装一个或多个组件时才安装该组件,应该将这些组件与DEPENDS选项一起列出。注意,并不是所有包类型都满足这些依赖关系。可以使用GROUP选项将组件放在特定的组下,可以使用cpack_add_component_group()命令进行描述(后面将进一步讨论)。

如果是必安组件,则应该给出REQUIRED关键字。用户将无法通过安装程序的UI禁用该组件。如果没有这个关键字,用户可以启用或禁用组件,默认状态是启用的。若要将此默认值更改为禁用,需要添加DISABLED关键字。无论组件是否必需,可以通过添加HIDDEN关键字对安装程序UI进行隐藏。非必需的隐藏组件通常会禁用,如果有另一个启用的组件对其有依赖,安装程序将安装该组件。

其余选项具有更专业的效果,只适用于少数包生成器。安装类型是组件的预置选择,可用于简化用户的安装过程。组件可以通过INSTALL_TYPES选项分配任意数量的安装类型,其中每个类型都是由cpack_add_install_type()命令定义,如下所示:

cpack_add_install_type(typeName [DISPLAY_NAME uiName])

如果typeName已经具有足够的描述性,可以省略DISPLAY_NAME选项,但是对于使用多个显示的安装类型,必须使用DISPLAY_NAME,而uiName将是带引号的字符串。没有预定义的安装类型,但是通常可以看到包提供了Full、Minimal或Default这样的安装类型。在CMake提供的维护的包生成器中,只有NSIS支持安装类型。

对于那些支持可下载组件的生成器,将DOWNLOADED关键字添加到cpack_add_component()可以按需下载组件,而不是直接包含在包中。ARCHIVE_FILE选项可用于定制下载组件的文件名。CMake提供的下载组件生成器是IFW,所以关于这个特性的讨论会在26.4.2节中进行。类似地,PLIST选项(仅在CMake 3.9或更高版本中可用)只能用于productbuild包生成器(参见26.4.6节)。

如果没有用GROUP定义组件的详细信息,那么在大多数UI安装程序中,组件会作为一个简单的列表。当使用分组时,允许定义任意深度的层次,可以包含组件和其他组。使用CPackComponent模块中的以下命令,可以定义一个组:

cpack_add_component_group(groupName
 [DISPLAY_NAME name]
 [DESCRIPTION description]
 [PARENT_GROUP parent]
 [EXPANDED]
 [BOLD_TITLE]
)

这个命令可以在引用groupName的cpack_add_component()之前或之后。DISPLAY_NAME和DESCRIPTION选项的作用与cpack_add_component()命令中的对应选项相同。PARENT_GROUP是组的等价选项,允许将其放在另一个组下,以支持任意组层次。当使用EXPANDED关键字时,组最初将在安装程序UI中展开,而BOLD_TITLE关键字将使该组以粗体显示。

理想情况下,组件名称应该特定于项目,以分层的项目方式来选择要打包的组件,以及如何在安装程序中显示它们(或者在非UI安装程序中,如何构造特定于组件的包)。组名的限制较少,因为它们可能包含来自不同项目的组件和组。组名不能与任何组件名相同。

cpack_add_component()和cpack_add_component_group()的作用是在当前范围内定义一组特定于组件的变量。CPackComponent文档列出了其中一些变量,并建议可以直接设置这些变量,但不建议这样做。命令提供了一种更健壮、可读性更好的定义组件和组信息的方式。它们应该与include(CPack)在相同的作用域中,最好是在include(CPack)之后立即调用。从技术上来说,约束没有这么严格,但是在不同的范围中定义组件会让关联性变得脆弱。

一个示例有助于说明上面的一些概念和讨论。

set(CPACK_PACKAGE_NAME ...)
# ... set other variables as per earlier example

include(CPack)

cpack_add_component(MyProj_Runtime
 DISPLAY_NAME Runtime
 DESCRIPTION "Shared libraries and executables"
 REQUIRED
 INSTALL_TYPES Full Developer Minimal
)
cpack_add_component(MyProj_Development
 DISPLAY_NAME "Developer pre-requisites"
 DESCRIPTION "Static libraries and headers needed for building apps"
 DEPENDS MyProj_Runtime
 GROUP MyProj_SDK
 INSTALL_TYPES Full Developer
)
cpack_add_component(MyProj_Samples
 DISPLAY_NAME "Code samples"
 GROUP MyProj_DevHelp
 INSTALL_TYPES Full Developer
 DISABLED
)
cpack_add_component(MyProj_ApiDocs
 DISPLAY_NAME "API documentation"
 GROUP MyProj_DevHelp
 INSTALL_TYPES Full Developer
 DISABLED
)
cpack_add_component_group(MyProj_SDK
 DISPLAY_NAME SDK
 DESCRIPTION "Developer tools, libraries, etc."
)
cpack_add_component_group(MyProj_DevHelp
 DISPLAY_NAME Documentation
 DESCRIPTION "Code samples and API docs"
 PARENT_GROUP MyProj_SDK
)
cpack_add_install_type(Full)
cpack_add_install_type(Minimal)
cpack_add_install_type(Developer DISPLAY_NAME "SDK Development")

项目生成器可以要求以三种方式的其中一种处理组件,选择由CPACK_COMPONENTS_GROUPING变量控制,可以设置为以下值:

ALL_COMPONENTS_IN_ONE

将创建单个包的所有请求组件。组件和组结构忽略。

ONE_PER_GROUP

每个顶层组件组应该创建一个包。那些不属于组的组件将创建自己的包。如果没有设置CPACK_COMPONENTS_GROUPING,这是默认行为,并且通常是理想的安排,但是对于UI安装程序,隐藏了项目想要显示的组件。

IGNORE

每个组件会创建自己的包,而不考虑任何组件组。此设置可能更适合UI安装程序,以确保没有隐藏组件,除非显式地配置为隐藏。

还有两个变量也会影响生成器解释组件的方式。如果CPACKMONOLITHIC_INSTALL设置为true,组件将完全禁用,所有组件将安装到一个包中。这是一个残酷的切换,因此要在所有相关平台上仔细测试结果,并特别注意寻找任何意外的情况。由于传统的原因,每个生成器有自己的设置,用于在默认情况下确定组件的支持程度。这个设置可以由`CPACK_COMPONENT_INSTALL`变量在每个生成器的基础上覆盖,该变量可以根据需要设置为true或false。

执行基于组件的安装时,项目不需要包括最终包中的所有组件。包含的组件集由CPACK_COMPONENTS_ALL变量控制,必须在include(CPack)之前设置该变量。如果没有设置,cpack将打包所有组件,项目可以显式地设置该变量,可以只列出希望打包的组件。例如,如果一个项目想要控制文档和代码是否应该打包,可以这样来实现:

if(NOT MYPROJ_PACKAGE_HELP)
 set(CPACK_COMPONENTS_ALL
 MyProj_Runtime
 MyProj_Development
 )
endif()
include(CPack)

项目可能希望安装除少数特定组件外的所有特定组件,而不是显式地列出要打包的所有组件。只读伪属性COMPONENTS中有完整的组件集,可以通过get_cmake_property()命令检索。项目可以从该组件列表删除不需要的条目。

if(NOT MYPROJ_PACKAGE_HELP)
 get_cmake_property(CPACK_COMPONENTS_ALL COMPONENTS)
 list(REMOVE_ITEM CPACK_COMPONENTS_ALL
   MyProj_Samples
   MyProj_ApiDocs
 )
endif()
include(CPack)

开始选择要安装的组件集,以及如何处理组件似乎有点复杂。造成困难的主要原因是要理解每个包生成器如何处理CPACK_COMPONENTS_GROUPING的值。本章后面的章节将解释每种生成器类型的行为,但在测试项目中会进行一些快速的实验,对于了解不同设置的效果通常有指导意义。

26.3. 多配置包

CPack本质上为单个构建配置生成包。大多数情况下,包为发布构建类型创建,但对于像SDK这样的项目,可能需要同时包含库的调试版本和发布版本。要将这两种配置都构建,并打包到一组包中,需要多做一些工作。

CPack提供了高级变量CPACK_INSTALL_CMAKE_PROJECTS,可以将多个构建树合并到一个打包运行中。预计每个包中会有四个部分,这四个部分分别是:

  • 构建目录

  • 项目名称(对多配置生成器很重要)。

  • 安装组件。ALL会列出CMAKE_COMPONENTS_ALL变量中的所有组件。其他值需要定义一个类似CMAKE_COMPONENTS_XXX的变量,该变量只保存组件名。例如,如果要安装的组件名为Runtime,则需要定义变量CMAKE_COMPONENTS_RUNTIME,并将其值设置为Runtime。

  • 软件包中的相对位置。安全的值是正斜杠(/),因为不同的包生成器会以不同的方式使用它。

项目的四部分中,一部分用于发布,其余部分用于调试。发行版的构建目录可以定义为CMAKE_BINARY_DIR,对于调试构建,需要创建并构建单独的构建目录。

调试组只需要添加两种构建配置之间不同的组件,但无论是使用默认的ALL还是使用特定组件,都需要特别小心,确保安装的文件不会相互覆盖。最后发布组件将确保所有具有相同名称和安装位置的文件,在打包时以发布版本进行。

set(CPACK_COMPONENTS_MYPROJ_RUNTIME MyProj_Runtime)
set(CPACK_COMPONENTS_MYPROJ_DEVELOPMENT MyProj_Development)

unset(CPACK_INSTALL_CMAKE_PROJECTS)
if(MYPROJ_DEBUG_BUILD_DIR)
 list(APPEND CPACK_INSTALL_CMAKE_PROJECTS
 ${MYPROJ_DEBUG_BUILD_DIR} ${CMAKE_PROJECT_NAME} MyProj_Runtime /
 ${MYPROJ_DEBUG_BUILD_DIR} ${CMAKE_PROJECT_NAME} MyProj_Development /
 )
endif()
list(APPEND CPACK_INSTALL_CMAKE_PROJECTS
 ${CMAKE_BINARY_DIR} ${CMAKE_PROJECT_NAME} ALL /
)
include(CPack)

当使用多配置生成器Xcode或Visual Studio, MYPROJ_DEBUG_BUILD_DIR目录在上面的例子中需要配置为只支持调试类型,而不是默认设置。这是唯一迫使其安装调试构建输出,而不是相同的配置为主要项目的方式。在该调试构建目录中运行cmake时,可以显式地将CMAKE_CONFIGURATION_TYPES缓存变量设置为Debug。

虽然可以只对多配置生成器使用一个构建目录,但这样更加脆弱和复杂。相反,上述技术适用于所有构建和包生成器类型。此外,还可以扩展合并不同体系结构的构建,甚至将完全独立的项目合并到统一的包或一组包中。

26.4. 包生成器

CPack支持创建各种格式的包,所有这些为以下类别之一:

简单的打包

归档文件可以是各种格式,比如zip、tarball、bz2等等。它们是包格式中最基本的,因为这只是用户在文件系统,某个位置文件的打包。它们是所有包格式中支持程度最好的,并且当最终用户想要多个不同版本可用或同时安装时,它们是简单易用的。

UI安装器

倾向于与支持的目标平台深度集成,提供诸如在安装后添加和删除组件、与桌面菜单集成等特性。通常为用户提供选择安装哪些组件的方法,而且非常直观,因此新手用户往往更喜欢。CMake支持Windows上的NSIS和WIX安装程序,Mac上的DragNDrop(比如DMG)和productbuild,以及Windows、Mac和Linux上的Qt安装程序框架(IFW)。Mac的一些旧的安装程序类型仍然支持,但应该认为是不适合的,后面的章节会简要提及。

非UI包

针对特定的包管理器。RPM和DEB在Linux上非常流行,FreeBSD和Cygwin的包在各自的平台上能很好的支持。

特定于产品的包

CMake 3.12为.NET添加了NuGet格式的支持。

无论生成哪种类型的包,每种情况下都使用的是相同的CPackConfig.cmake。通常不会出现问题,因为特定于生成器的配置通常是通过特定于生成器的变量实现的。如果需要为特定的生成器添加某些逻辑,并且CMake和CPack提供的现有变量不够,可以将CPACK_PROJECT_CONFIG_FILE变量设置为一个文件的名称,该文件将在调用每个包生成器时包含一次。每次读取时,CPACK_GENERATOR变量将保存生成器的名称,而不是整个生成器列表。在生成器需要时,可以通过该文件覆盖CPackConfig.cmake中的设置。完整的cpack按照以下伪代码的步骤运行:

include(CPackConfig.cmake)

function(generate CPACK_GENERATOR)
 # CPACK_GENERATOR is a single generator local to this function scope
 if(CPACK_PROJECT_CONFIG_FILE)
 include(${CPACK_PROJECT_CONFIG_FILE})
 endif()

 # ...invoke package generator
endfunction()

# Here CPACK_GENERATOR is the list of generators to be processed,
# as set by CPackConfig.cmake or on the cpack command line
foreach(generator IN LISTS CPACK_GENERATOR)
 generate(${generator})
endforeach()

上面的例子是将CPACK_PACKAGE_ICON设置为生成器特定的值,因为不同的生成器希望这个图标具有不同的格式,因此文件名需要是生成器特定的。

本章的其余部分将讨论CMake/CPack提供的包生成器。

26.4.1. 打包文件

CPack支持以多种不同格式创建归档文件。最受广泛支持的是ZIP和TGZ,前者在Windows平台上很常见,后者生成gzip压缩的tar包(.tar.gz或. TGZ),在其他平台也都受支持。其他可用的存档格式包括TBZ2 (.tar.bz2)、TXZ (.tar.xz)、TZ (.tar. z)和7Z (7zip archives . 7Z)。为了可移植性最大化,通常应该选ZIP和TGZ,但其他一些格式可能生成较小的存档,并且可能适合那些支持这些格式的平台。

cpack还支持解压打包格式。这可以使用STGZ生成器进行,它会生成一个Unix shell脚本,该脚本末尾嵌入了打包文件。这可以看作是一种基于控制台的UI安装程序,但实际上只提供非常基本的功能,用户可能更喜欢可以自己解包的打包文件。

由于历史原因,打包生成器在默认情况下禁用了组件。要启用基于组件的打包格式创建,必须将CPACK_ARCHIVE_COMPONENT_INSTALL设置为true,然后CPACK_COMPONENTS_GROUPING将确定要生成的打包文件集。

执行非组件安装时,可以使用CPACKARCHIVE_FILE_NAME变量控制包的文件名。对于基于组件的安装,每个组件包的名称由`CPACK_ARCHIVE_FILE_NAME`控制,其中是大写的组件名或组名。适当的存档扩展名将附加到指定的文件名后(例如 .tar.gz, .zip等).。

打包文件的一个常见约定是,将提取的目录结构的顶层与打包文件的名称相同,但不带文件扩展名(即与CPACK_PACKAGE_FILE_NAME相同)。对于非组件安装,这是打包生成器的默认行为,但是对于多组件包,默认情况下不使用顶层目录。通过将CPACK_COMPONENT_INCLUDE_TOPLEVEL_DIRECTORY设置为true,项目可以为组件打包指定一个公共顶层目录。由于这个变量是所有包生成器共享的,特定生成器的覆盖可以很容易做到这点:

CMakeLists.txt

set(CPACK_PROJECT_CONFIG_FILE
 ${CMAKE_CURRENT_LIST_DIR}/cpackGeneratorOverrides.cmake
)

cpackGeneratorOverrides.cmake

if(CPACK_GENERATOR MATCHES "^(7Z|TBZ2|TGZ|TXZ|TZ|ZIP)$")
 set(CPACK_COMPONENT_INCLUDE_TOPLEVEL_DIRECTORY YES)
endif()

开发人员应该注意,一些打包格式、平台和文件系统对文件名和路径长度有限制。例如,POSIX.2要求文件名不超过100个字符,对于扩展的tar交换格式路径不超过255个字符,而旧的tar格式可能将整个路径限制为100个字符或更少。当解压缩到eCryptFS文件系统时,根据经验,文件名有大概140个字符的限制。Windows上解压的路径长度可以限制为260个字符,这取决于某些设置和操作系统版本。UTF-8文件名和路径使情况更加复杂,并可能会缩短有效字符的长度。

考虑到这些约束,项目应该避免在安装包中使用长路径和文件名。这些限制在包类型中最为明显,由于其他非打包格式也在内部使用打包方式,并将其部署到具有这些限制的系统中,因此通常应该选较短的路径和文件名。

26.4.2. Qt安装程序框架(IFW)

IFW包生成器为CPack提供基于UI的打包格式,并且提供最广泛的平台支持。可以使用相同的配置细节为Windows、Mac和Linux安装程序,这使得当项目想要在主要桌面平台上拥有一致的UI安装程序时,这是一个很好的选择。还可以显示组件和组的本地化名称和描述,以及灵活的自定义能力。

使用UI外观和安装程序图标的默认,通常就足够了,但有些项目可能希望定制化的来改进品牌,特别是关于图标的使用。生成器会忽略CPACK_PACKAGE_ICON变量,依赖于三个独立的IFW变量来控制不同的图标:

  • CPACK_IFW_PACKAGE_ICON (Windows是.ico,Mac是.icns, Linux没有)

  • CPACK_IFW_PACKAGE_WINDOW_ICON (.png)

  • CPACK_IFW_PACKAGE_LOGO (最好是.png)

这些变量在不同平台之间无法进行一致的处理,因此很难正确地设置。简单起见,最好将这三个图像设置为相同的图像,尽管可能采用不同的格式和/或大小。建议在感兴趣的平台上进行测试,以确保安装程序按照预期的方式呈现。下面的示例展示了如何进行这样的配置:

# Define generic setup for all generator types...

# IFW-specific configuration
if(WIN32)
 set(CPACK_IFW_PACKAGE_ICON ${CMAKE_CURRENT_LIST_DIR}/Logo.ico)
elseif(APPLE)
 set(CPACK_IFW_PACKAGE_ICON ${CMAKE_CURRENT_LIST_DIR}/Logo.icns)
endif()
set(CPACK_IFW_PACKAGE_WINDOW_ICON ${CMAKE_CURRENT_LIST_DIR}/Logo.png)
set(CPACK_IFW_PACKAGE_LOGO ${CMAKE_CURRENT_LIST_DIR}/Logo.png)

include(CPack)
include(CPackIFW)
# Define components and component groups...

IFW生成器默认启用基于组件安装。生成单独的安装程序,但CPACK_COMPONENTS_GROUPING可以控制为用户显示层次的多少:

ALL_COMPONENTS_IN_ONE

不显示组件层次结构,始终安装默认启用的组件。

ONE_PER_GROUP

只显示第一级的组,以及不属于任何组的组件。任何组下的子组和组件将隐藏。

IGNORE

显示所有隐藏组件,而不管在组层次结构中的位置。这可能是大多数项目想要使用的选项。

命令可以提供组件和组的更详细配置:

cpack_ifw_configure_component(componentName
 [NAME componentNameId]
 [DISPLAY_NAME displayName...]
 [DESCRIPTION description...]
 [VERSION <version>]
 [DEPENDS compId1 [compId2...] ]
 [REPLACES compId3 [compId4...] ]
 # Other options not shown
)

# The cpack_ifw_configure_component_group() command supports all
# of the above options too

可以为每个组件或每组件组的DISPLAY_NAME和DESCRIPTION提供不同语言和地区的替代内容。这两个选项为一个对列表,其中第一个值是语言或区域ID,第二个值是该语言的文本内容。列表中的第一个值可以在没有前面的语言或语言环境ID的情况下给出,如果在安装时没有任何语言或语言环境ID与用户当前的设置匹配,则该值将当作默认文本。

cpack_ifw_configure_component(MyProj_Docs
 DISPLAY_NAME Documentation
 de Dokumentation
 pl Dokumentacja
)

cpack_ifw_configure_component_group(MyProj_Colors
 DISPLAY_NAME en Colors
 en_AU Colours
 DESCRIPTION en "Available color palettes"
 en_AU "Available colour palettes"
)

VERSION选项允许指定每个组件和每个组的版本号。在线安装程序使用它来确定更新是否可用(参见后面的部分)。如果没有给出VERSION,则默认为CPACK_PACKAGE_VERSION。

DEPENDS选项类似于cpack_add_component()的选项,不同的是compId1…项的形式不同。需要遵循QtIFW样式,这是一个层次字符串,而不是componentName。分组层次结构的每一层都是点分隔的,如下面的例子所示:

include(CPack)
include(CPackIFW).

cpack_add_component(foo GROUP groupA)
cpack_add_component(bar GROUP groupB)

cpack_add_component_group(groupA)
cpack_add_component_group(groupB)

cpack_ifw_configure_component(bar DEPENDS groupA.foo)

组件安装程序内部使用的名称可以用NAME选项覆盖。此名称将用于标识DEPENDS参数中的组件,也用于检查组件的新版本是否可用。顶层组名可以用CPACK_IFW_PACKAGE_GROUP变量设置,通常设置为反向域名,以确保组件名在大型、多供应商的安装程序中不会发生冲突。当列出与DEPENDS选项的依赖项时,必须包含这个顶层组名,如下面修改示例所示:

set(CPACK_IFW_PACKAGE_GROUP com.examplecompany.product)

include(CPack)
include(CPackIFW)

cpack_add_component(foo GROUP groupA)
cpack_add_component(bar GROUP groupB)

cpack_add_component_group(groupA)
cpack_add_component_group(groupB)

cpack_ifw_configure_component(bar
 DEPENDS com.examplecompany.product.groupA.foo
)

CPACK_IFW_PACKAGE_GROUP只是额外变量的一个,可以用来设置特定于IFW的配置。这些变量应该在include(CPackIFW)之前设置,并且可以以各种方式修改安装程序的显示和行为。CPackIFW模块文档提供了所有支持的变量,及变量的完整清单,其中许多变量在QtIFW产品的本机配置中有类似的设置。大多数变量都有合理的默认值,应该更多地将其视为可以定制的选项,而不是需要设置的东西。一个例外是(需要安装的)维护工具名称的相关变量,它允许用户修改已安装的组件集或完全删除产品。一些平台上,工具名可能出现在桌面或应用程序菜单中,默认名称可能会让用户感到困惑。因此,项目应该提供一个具体的名称,可以这样做:

set(CPACK_IFW_PACKAGE_MAINTENANCE_TOOL_NAME ${PROJECT_NAME}_MaintenanceTool)
set(CPACK_IFW_PACKAGE_MAINTENANCE_TOOL_INI_FILE
 ${CPACK_IFW_PACKAGE_MAINTENANCE_TOOL_NAME}.ini)
include(CPackIFW)

.ini文件用于提供安装程序的状态信息。.ini文件的名称选择性设置,最好与安装程序一致。通过设置,当工具出现在桌面或应用程序菜单中,用户将看到与项目相关的名称。

IFW生成器的一个特性是能够创建在线安装。部分或所有组件都可以按需下载,而不是打包成安装程序的一部分。如果可选组件非常大,这就特别方便。在线安装的另一个好处是,可以从在线存储库获得更新的版本,这提供了一个非常方便的升级方式。用户运行工具时,工具会联系在线存储库集,确定可用的组件及其版本,然后根据需要添加、删除或升级单个组件。

将项目配置为支持可下载组件的第一步,是指定安装程序从何处下载这些组件。主默认存储库使用cpack_configure_downloads()指定:

cpack_configure_downloads(baseUrl
 [ALL]
 [ADD_REMOVE | NO_ADD_REMOVE]
 [UPLOAD_DIRECTORY dir]
)

baseUrl是安装程序寻找可下载组件的位置。安装程序希望在该位置下找到一个名为Updates.xml的文件。如果存在ALL关键字,所有组件都视为可下载,而不管是否显式标记为可下载。这是制作在线安装程序的一种方法。因为不需要嵌入包,从而生成最小的安装程序。

关键字ADD_REMOVE引导安装程序,使用Windows的“添加/删除程序”功能,当用户通过Windows系统设置选择修改包时,将运行工具。ALL关键字意味着ADD_REMOVE,可以通过NO_ADD_REMOVE进行覆盖。

UPLOAD_DIRECTORY选项由支持可下载组件的CPack生成器类型(尽管这些组件都没有积极维护),IFW生成器会忽略该选项。当cpack运行时,在单独的目录中创建可下载包,这样整个目录的内容就可以上传到baseUrl位置(必须手动完成)。UPLOAD_DIRECTORY选项旨在允许项目覆盖此目录所在的位置,但IFW生成器总是创建一个名为repository的目录,该目录位于基_CPack_Packages目录下(多层深度)。

IFW生成器允许项目指定维护工具和安装程序访问的附加存储库。如果不同的组件是由不同的供应商提供的,或者某些组件与其他组件有不同的发布计划,那么这个功能会非常有用。

cpack_ifw_add_repository(repoName
 URL baseUrl
 [DISPLAY_NAME displayName]
 [DISABLED]
 [USERNAME username]
 [PASSWORD password]
)

repoName是一个内部跟踪名称,baseUrl与cpack_configure_downloads()有类似的含义。DISPLAY_NAME选项通常用于提供的名称,否则baseUrl将显示为存储库名称,这往往对用户不太友好。如果存储库需要用户名和密码,可以提供,但是请记住,密码将以未加密的方式存储是不安全的。DISABLED关键字表示存储库在默认情况下禁用,但是用户可以在安装程序或维护工具的UI中启用它。

用于发布包的主存储库和用于预览包的辅助存储库(默认禁用)的例子可以这样配置:

include(CPack)
include(CPackIFW)

cpack_configure_downloads(https://example.com/packages/product/release ALL)
cpack_ifw_add_repository(secondaryRepo
 DISPLAY_NAME "Preview features"
 URL https://example.com/packages/product/preview
 DISABLED
)

不幸的是,cpack_configure_downloads()命令目前不支持指定名称,因此主URL将始终显示为纯URL,而不是一个对用户更友好的名称。

包生成器的一个缺点是,生成的安装程序不能为用户提供触发无交互的命令行安装方法。这是Qt安装程序框架本身的限制,而不是CMake或CPack的限制。与大多数其他生成器类型相比,因为包含安装程序的接口、网络等所需的Qt支持,所以安装程序有额外的开销。与其他生成器的几百kB相比,这样会使一个普通安装程序的大小达到18Mb或更大。

上面的讨论只涵盖了IFW生成器的主要方面,还有相当多的可用功能允许项目定制安装程序和维护工具。对于许多项目来说,上面的功能允许创建灵活、健壮和跨平台的安装程序。如果需要进一步的裁剪,所提供的特性将作为扩展的基础。

26.4.3. WIX

WIX包生成器使用WIX工具集为Windows生成.msi安装程序。与IFW包生成器相比,具有类似程度的UI可定制性,有以下优势:

  • 通过msiexec工具的选项,可以直接支持命令行(即无人值守)安装。

  • 安装程序与Windows的添加/删除功能直接集成。

  • 默认的界面外观为大多数用户所熟悉。

与IFW相比,有以下缺点:

  • 没有简单、直接的方法来提供本地化的组件名称和描述。

  • 会忽略CPACK_WIX_COMPONENT_INSTALL和CPACK_COMPONENTS_GROUPING选项了。

  • 不支持可下载组件。

  • 不能同时安装具有相同升级GUID的多个版本。每个安装都会替换前一个,即使是在完全不同的目录下。

默认情况下,WIX生成器生成一个基于组件的包,会在UI中显示,就好像将CPACK_COMPONENTS_GROUPING设置为IGNORE一样。如果不希望使用基于组件的包,可以将CPACK_MONOLITHIC_INSTALL设置为true,安装所有已定义的组件。在独立的安装程序中只包含一些组件是不可能的,如果设置了CPACK_COMPONENTS_ALL, CMake会发出警告并忽略CPACK_COMPONENTS_ALL。

WIX安装程序的关键部分是包含产品GUID和升级GUID。如果其他已安装的包具有相同的升级GUID,则升级该包,而不是将新包作为单独的产品安装。如果升级GUID是相同的,但是产品的GUID是不同的,那么就认为是一次重大的升级,新的安装程序将完全替换旧的包。如果产品GUID也是相同的,只要安装程序报告的版本号比当前安装的包更新,那么新的安装程序会够执行小的升级。服务包就是这样一个示例,其中将相同的产品GUID应用到的基本版本进行维护。除非创建一个相当高级的安装程序或打包策略,否则项目通常需要在每个版本中更改产品GUID,因为Windows本身对于在不同包之间的产品GUID约束相当严格。

CPack提供了对设置产品和升级GUID的支持。CPACK_WIX_PRODUCT_GUID和CPACK_WIX_UPGRADE_GUID变量可以在include(CPack)之前设置(以手动控制),或者可以不设置,允许CPack在每次调用时生成新值。对于产品GUID,这种自动生成可能是期望的行为,但是升级GUID在理想情况下在产品的生命周期中应该永远不变。项目应该获取一个GUID,并将CPACK_WIX_UPGRADE_GUID设置为该值,最好不要再更改。这将确保所有未来版本都能够无缝地升级旧版本。实际的GUID可以通过各种方法获得,比如命令行工具、基于web的UUID生成器,甚至使用string(UUID)命令与CMake本身一起获得。对于某些产品,升级GUID随着每个主要版本的变化而变化,以允许较旧的主要版本与较新的版本共存。

产品GUID必须更改的条件之一是.msi文件名称的更改。安装程序的文件名通常会包含一些版本细节,这意味着每个版本都将视为一次重大升级。如果用户安装了新版本,将完全取代以前安装的版本。可以将新版本安装到不同的目录,并将旧的目录删除。使用包含版本号的默认安装目录(由CPACK_PACKAGE_INSTALL_DIRECTORY控制)可能很有诱惑力,但是用户很可能希望在升级时默认目录保持不变。理想情况下,只有当升级GUID更改时,默认目录才应该更改,因为GUID是提供从一个版本到另一个版本的标识符。

安装一个新包,并且已经安装了具有相同升级GUID的另一个包时,会在版本之间进行检查。只有当新包的版本更新时,升级才允许进行。在测试中只考虑了前三个版本号组件,因此版本2.7.4.3和2.7.4.9从升级的角度来看是相同的版本。因此,打算使用WIX生成器的项目应该避免使用超过三个版本号组件。如果允许从单独的cpackage_version_xxx版本部分自动设置为CPACK_PACKAGE_VERSION,则会强制执行。

对于基本的WIX包,UI大多数默认值都可以接受。项目可能希望提供一个产品图标来代替通用的MSI安装程序图标,用于在添加/删除区域显示品牌,但在其他方面,默认值是可以接受的。下面的示例展示了WIX安装程序的基本配置。

# Define generic setup for all generator types...

# WIX-specific configuration
set(CPACK_WIX_PRODUCT_ICON ${CMAKE_CURRENT_LIST_DIR}/Logo.ico)
set(CPACK_WIX_UPGRADE_GUID XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)

include(CPack)
# Define components and component groups...

26.4.4. NSIS

NSIS包生成器使用Nullsoft脚本安装系统,为Windows生成安装程序可执行文件。它与IFW和WIX生成器有许多相似的特性,包括一定程度的UI可定制性和对组件层次结构的支持。NSIS生成器的优点有:

  • 安装程序的可执行文件,可以通过专用的命令行选项直接支持无人值守的安装。

  • 是唯一积极维护的安装类型的CPack生成器。

  • 安装前/安装后和卸载前命令是直接支持的,尽管这些命令必须实现为NSIS命令。

NSIS生成器有几个缺点:

  • 忽略CPACK_NSIS_COMPONENT_INSTALL和CPACK_COMPONENTS_GROUPING选项。NSIS生成器与WIX生成器具有相同的限制。

  • 不支持可下载的组件。

  • 安装了产品后,用户只能能重新安装的情况下更改已安装的组件集。

  • 只支持基本的UI定制,不直接支持任何UI内容的本地化。这些是CPack生成器的限制,而不是NSIS本身的限制,NSIS通过本地脚本语言提供了一些工具。

  • 尽管可以将不同的版本安装到不同的位置,但它们会共享注册表信息,因此不能完全隔离。所以,也会只有一个版本会出现在Windows设置的添加/删除区域。

默认情况下,如果新包与旧包安装在同一目录下,这些安装程序将只执行对现有产品安装的升级。项目可以将CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL变量设置为true,强制安装程序首先检查注册表是否有包的现有安装。这种检查不依赖于安装位置,因此是检查要升级的现有安装的可靠方法。因此,建议对大多数项目将该变量设置为true。

NSIS安装程序可以覆盖默认外观。安装程序、卸载程序和产品本身在添加/删除区域显示的图标都可以设置,因为默认的显示太差强人意。显式地设置产品显示的名称,以避免CPack提供不适当的默认名称。下面的示例展示了一个覆盖的基本配置,以避免使用不合适默认值的情况。

# Define generic setup for all generator types...

# NSIS-specific configuration
set(CPACK_NSIS_MUI_ICON ${CMAKE_CURRENT_LIST_DIR}/InstallerIcon.ico) ①
set(CPACK_NSIS_MUI_UNIICON ${CMAKE_CURRENT_LIST_DIR}/UninstallerIcon.ico) ②
set(CPACK_NSIS_INSTALLED_ICON_NAME bin/MainApp.exe) ③
set(CPACK_NSIS_DISPLAY_NAME "My Project Suite") ④
set(CPACK_NSIS_PACKAGE_NAME "My Project") ⑤

set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL YES)

include(CPack)
# Define components and component groups...

① 安装程序的图标。Windows可能会有更多内容,以表明安装程序需要管理员权限。使用绝对路径确保NSIS在创建安装程序时能够找到图标。

② 用于安装\卸载程序的图标。同样,使用图标文件的绝对路径。

③ 这将控制在添加/删除区域中使用的图标。必须是图标文件(.ico)或有嵌入应用程序图标的可执行文件的路径。路径应该是相对于安装位置的基点。

④ 仅在添加/删除区域中显示的包的名称。

⑤ 在安装程序的UI和安装期间在标题栏中使用的名称。在某些上下文中,可能会加上Setup这个词。

26.4.5. DragNDrop

在Mac上,产品通常以.dmg文件的形式发布。作用类似于磁盘镜像,可以包含从单个应用程序到整个应用程序套件、文档链接等任何内容。/Applications区域的符号链接经常作为镜像的一部分,以便用户可以轻松地将应用程序拖放到镜像中进行安装,因此这种生成器类型的名称为DragNDrop。此生成器类型的配置变量在名称中使用DMG,而不是DRAGNDROP,但是请注意,cpack只将DRAGNDROP识别为生成器本身的名称。

dmg格式更接近于打包,而不是UI安装程序。组件用于控制创建一个或多个.dmg文件,以及每个.dmg文件包含什么,但是没有安装时UI来选择组件。用户需要打开.dmg文件并将内容拖到所需的位置来安装它们。CPACK_COMPONENTS_ALL控制安装了哪些组件,CPACK_COMPONENTS_GROUPING变量控制这些组件如何在.dmg文件中分布,如下所示:

ALL_COMPONENTS_IN_ONE

所有组件都包含在一个.dmg文件中。

ONE_PER_GROUP

每个顶层组件组和每个不在组中的组件,都将放在自己单独的.dmg文件中。

IGNORE

每个组件将放在自己单独的.dmg文件中,并忽略所有组件组。

除了默认值之外,这种生成器类型几乎不需要自定义。打开磁盘镜像时,Finder窗口显示的大小和布局可以通过提供一个自定义的.DS_Store文件来控制。该项目需要使用示例文件夹,需要手工准备这样一个文件,其中包含与最终磁盘映像相同的内容,或者可以使用AppleScript编程创建。CPACK_DMG_DS_STORE变量可以用来命名预先准备好的.DS_Store文件,或者CPACK_DMG_DS_STORE_SETUP_SCRIPT可以指向一个在包生成时运行的AppleScript文件。对于这两种情况,如果需要,可以使用CPACK_DMG_BACKGROUND_IMAGE变量设置背景图像,但是将背景保留为默认的空白是比较常见的。对于磁盘镜像不应该提供/Applications文件夹的符号链接,项目应该将CPACK_DMG_DISABLE_APPLICATIONS_SYMLINK设置为true。

通过将CPACK_PACKAGE_ICON设置为.icns格式的图标,可以为磁盘镜像指定一个图标。此图标仅用于挂载时表示.dmg文件,而不用于.dmg文件本身。指定的图标可能显示在Finder标题栏或某些Finder视图中。

语言本地化通过CPACK_DMG_SLA_DIR和CPACK_DMG_SLA_LANGUAGES变量提供。这些可用于提供在许可协议阶段,打开磁盘镜像时,使用的特定短语,并提供许可协议的本地化版本。请参阅CPackDMG模块的文档,了解如何使用这两个变量,以及需要提供的语言文件的相关要求。

Bundle生成器类型与DragNDrop生成器相关。使用相同的DMG变量集,加上自己的一些变量。Bundle生成器最初的目的是生成提交到Apple应用程序商店的应用程序包。如今,这样的应用程序包在使用CMake的Xcode生成器时,可以更好的构建,这更接近Apple的期望。请参阅第22章,了解此类应用程序包的推荐方法。

26.4.6. productbuild

DragNDrop生成器的替代方法是productbuild。它不生成.dmg磁盘镜像,而是生成用于macOS安装程序应用程序的.pkg包。生成器不应该将CPACK_MONOLITHIC_INSTALL设置为true,因为这样做会损坏的安装程序。安装程序类型不受支持,也很少能够定制UI。

与IFW生成器相比,productbuild的主要优点是能够为安装程序签名。通过将CPACK_PRODUCTBUILD_IDENTITY_NAME(如果需要还可以将CPACK_PRODUCTBUILD_KEYCHAIN_PATH)设置为签名细节,可以很容易地进行配置。通常只指定默认标识就足够了,可以这样做:

set(CPACK_PRODUCTBUILD_IDENTITY_NAME "Developer ID Installer")
include(CPack)

productbuild生成器缺乏对可下载组件的支持,因此无法创建在线安装程序。升级是通过替换现有安装的以前内容。与NSIS安装程序一样,只能在安装产品时,修改已安装的组件集。同时将多个版本安装到不同的目录也是不可能的。

默认情况下,productbuild生成器生成的安装程序可重定位。这意味着,当包安装到用户的机器上时,如果操作系统知道一个与包中提供的应用程序同名的应用程序包,则安装程序将覆盖现有的应用程序,无论在文件系统的哪个位置。这种情况下,应用程序不会安装到默认的/Applications区域,这通常意味着也不会出现在用户期望的地方。这种情况通常出现在开发人员用来构建和测试包的机器上。构建产生的应用程序包是操作系统所知道的,所以在安装包时,构建树的应用程序包会用作该应用程序的安装位置,而不是/Applications中的预期位置。构建树的_CPack_Packages级目录中也会有该应用程序的另一个副本,它可以产生类似的行为。为了正确地测试安装程序,在运行安装程序之前,需要先从开发人员的机器上删除应用程序包的所有副本。

解决上述重定位问题的方法是将组件标记为不可重定位。这就阻止了安装程序选择现有的应用程序包的位置,但代价是阻止了用户移动应用程序包。要使组件不可重定位,需要使用cpack_add_component()命令的PLIST选项为每个组件提供一个定制的plist文件。plist文件应该使用pkgbuild命令的--analyze选项来获得,其他选项可以通过查看项目的cpack命令的详细输出来找到:

cpack -G productbuild -V

常规的plist文件看起来是这样的:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
 <dict>
 <key>BundleHasStrictIdentifier</key>
 <true/>
 <key>BundleIsRelocatable</key>
 <true/>
 <key>BundleIsVersionChecked</key>
 <true/>
 <key>BundleOverwriteAction</key>
 <string>upgrade</string>
 <key>RootRelativeBundlePath</key>
 <string>Applications/MyApp.app</string>
 </dict>
</array>
</plist>

将BundleIsRelocatable字典项改为false,以防止操作系统在安装时重新定位应用程序。组件中的每个应用程序包将有一个节。当plist文件生成和更新时,可以这样使用:

cpack_add_component(MyProj_Runtime
 ... # Other options
 PLIST runtime.plist
)

应该将productbuild生成器视为旧的不再支持PackageMaker生成器的替代品。苹果不再提供PackageMaker应用程序,因此使用新版macOS的开发者必须使用productbuild。

26.4.7. RPM

Linux系统上,RPM是两种主要的包管理格式之一。RPM包没有自己的UI特性,本质上只是带有元数据集和一些脚本特性的打包文件。系统的包管理器使用这些来管理包之间的依赖关系,向用户提供信息,触发安装前/安装后脚本和卸载脚本等等。

由于包本身没有UI特性,因此不需要对UI进行定制,但是RPM生成器通过变量提供了元数据的可定制性。许多变量不需要显式地设置,因为大多数默认值都适合于不需要执行复杂操作的项目。对于不需要调用安装前/安装后或卸载脚本的包,并且可以由底层的包创建工具,自动确定包之间的依赖关系,定制的数量与其他包生成器类似。

RPM生成器支持组件安装,但默认情况下禁用组件。当组件禁用时,只生成一个.rpm,行为就好像CPACK_MONOLITHIC_INSTALL设置为true。这种情况下,所有组件都包含在包中。如果启用了组件,那么CPACK_COMPONENTS_GROUPING就有意义了,并且会创建多个.rpm文件。通过将CPACK_RPM_COMPONENT_INSTALL设置为true来启用组件,已安装的组件集通常由CPACK_COMPONENTS_ALL控制。

# Define generic setup for all generator types...
set(CPACK_COMPONENTS_GROUPING ONE_PER_GROUP)

# RPM-specific configuration
set(CPACK_RPM_COMPONENT_INSTALL YES)

include(CPack)
# Define components and component groups...

组件名或组名可能不适合用作包名,这些包名通常在RPM package manager UI应用程序中作为. RPM文件名的一部分对用户可见。可以使用CPACK_RPM_<COMP>_PACKAGE_NAME在每个组件的基础上设置这些名称,其中是大写组件名称。当创建禁用组件的包时,可以通过设置CPACK_RPM_PACKAGE_NAME覆盖单个包名。

add_executable(sometool ...)
install(TARGETS sometool ... COMPONENT MyProjUtils)

set(CPACK_RPM_MYPROJUTILS_PACKAGE_NAME myproj-tools)
include(CPack)

rpm文件的名称也可以定制,项目很可能希望这样做。每个组件的.rpm文件的名称是由CPACK_RPM_<COMP>_FILE_NAME变量控制(对于非组件打包,只需CPACK_RPM_FILE_NAME)。这些变量的默认值遵循以下模式:

<CPACK_PACKAGE_FILE_NAME>[-<component>].rpm

部分是原始组件的名称(大写/小写没有变化)。这种默认文件名的缺点是它不包含任何版本或架构细节,但是这些信息通常是必需的(或者至少是希望有的)。通常最好是cpack让底层的包创建工具选择一个更好的默认包名,这可以通过将CPACK_RPM_<COMP>_FILE_NAME设置为一个特殊的字符串RPM-DEFAULT来完成。下面给出了生成文件名的示例。

rpm默认的包文件名将自动包含架构。如果需要显式地指定架构,比如将一个包标记为noarch,表明不特定于架构,CPACK_RPM_<COMP>_PACKAGE_ARCHITECTURE变量可以设置为所需的值,如果没有设置特定于组件的覆盖,可以将CPACK_RPM_PACKAGE_ARCHITECTURE设置为默认值(它也用于集成包)。体系结构的默认值由cpack作为uname -m的输出,但是如果在64位主机上构建32位包,这将会导致错误,因此项目最好显式地设置体系结构。

RPM文件需要有版本信息。RPM生成器将默认使用CPACK_PACKAGE_VERSION,也可以使用CPACK_RPM_PACKAGE_VERSION设置RPM的版本号(这种情况应该很少)。注意,目前还不能指定每个组件的版本,CPack的 RPM生成器目前仅限于为所有组件设置相同的版本。除了包版本之外,RPM包还有单独的发布号,使用CPACK_RPM_PACKAGE_RELEASE指定的。这个发布号是包本身的发布,而不是产品的发布,所以如果发布号增加了(例如,为了修复一个打包问题),那么包版本通常会保持不变。如果包版本发生了变化,那么发布号通常会重新设置为1,这是没有指定CPACK_RPM_PACKAGE_RELEASE时的默认值。还可以通过CPACK_RPM_PACKAGE_EPOCH指定一个可选的epoch,它在某些系统或存储库中的使用可能比其他系统或存储库更常见。完整版本的格式是E:X.Y.Z-R,其中E是历元,如果提供的话必须是一个数字。如果没有设置历元,完整版本的格式为X.Y.Z-R。

除非知道需要的epoch值,否则项目通常不设置epoch。除非项目覆盖了CPACK_PACKAGE_VERSION和CPACK_RPM_PACKAGE_ARCHITECTURE的值,否则它们的值在CMakeLists.txt文件中是不可用的,因为这些变量的默认值只在cpack处理输入文件时会用到,而不是在CMake运行时。这意味着直接设置包文件名,要比使用RPM-DEFAULT做更多的工作。下面的例子展示了如何使用rpm默认特性:

set(CPACK_RPM_PACKAGE_RELEASE 5) # Optional, default of 1 is often okay
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
 set(CPACK_RPM_PACKAGE_ARCHITECTURE i686)
endif()

set(CPACK_RPM_MYPROJUTILS_PACKAGE_NAME myproj-tools)
set(CPACK_RPM_MYPROJUTILS_FILE_NAME RPM-DEFAULT)
include(CPack)

对于上面的例子,假设CPACK_PACKAGE_VERSION计算结果是一个X.Y.Z格式的字符串,这个例子通常会得到这样的包文件名:

myproj-tools-X.Y.Z-5.i686.rpm
myproj-tools-X.Y.Z-5.x86_64.rpm

正如前一章所讨论的,Linux系统上不太可能需要默认的基本安装点,这也扩展到了RPM包的创建。实际上,除了Windows系统之外,通常也应该通过显式地设置CPACK_PACKAGING_INSTALL_PREFIX变量来为打包设置一个合适的基准点。扩展前一章的示例,该项目可能需要做如下操作:

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

RPM包的独特性是可以包含重新定位路径。包可以指定一个或多个路径前缀,然后用户可以选择在安装时将其重新定位到文件系统的某处。要支持这个特性,必须将CPACK_RPM_PACKAGE_RELOCATABLE变量设置为true,然后CPACK_RPM_RELOCATION_PATHS可以包含允许用户重新定位的路径前缀列表。如果使用这个特性,开发人员应该参考CPackRPM模块文档,了解如何处理相对路径,以及适用于这两个变量的各种默认行为。还要注意的是,如果该项目是作为Linux发行版的一部分包含的,发行版维护人员可能需要重写安装前缀变量和重新定位目录。

RPM包创建工具通常会去除可执行文件和动态库中的调试符号。其基本原理是应该尽量减小发布二进制文件的大小,并且通常会隐藏实现细节,不提供调试工具。通常,剥离是由CPACK_STRIP_FILES变量控制,决定了在打包期间剥离是否作为阶段安装的一部分,在RPM生成器的情况下,RPM包创建工具通常在默认情况下执行自己的剥离操作。因此,即使CPACK_STRIP_FILES为false或未设置,剥离操作仍然可能发生。潜在的问题是包创建工具rpmbuild通常会在安装后进行一些操作,会在创建最终的.rpm包之前删除二进制文件并执行其他任务。c传统上,cpack提供的解决方案是通过设置 CPACK_RPM_SPEC_INSTALL_POST变量,内容通常是类似于/bin/true的路径。不过这种方式已经废弃,推荐使用CPACK_RPM_SPEC_MORE_DEFINE变量进行代替:

# Prevent stripping and other post-install steps during package creation
set(CPACK_RPM_SPEC_MORE_DEFINE "%define __spec_install_post /bin/true")

虽然上述的防剥离技术有效,但也抛弃了通常会使用的其他操作(例如,python文件的自动字节码编译,特定于体系结构的后处理)。更好的替代方案是允许剥离.rpm中的二进制文件,并生成一个单独的debuginfo包。最初对生成debuginfo包的支持是在CMake 3.7中添加的,在3.8和3.9中得到了进一步的改进。要启用这个特性,通常只需要将CPACKRPM_DEBUGINFO_PACKAGE或组件特定的`PACK_RPM_DEBUGINFO_PACKAGE`设置为true。生成的debuginfo包包含源文件和调试信息。默认情况下,源文件取自CMAKE_SOURCE_DIR和CMAKE_BINARY_DIR,也可以用CPACK_BUILD_SOURCE_DIRS变量覆盖。可以使用CPACK_RPM_DEBUGINFO_EXCLUDE_DIRS和cpack_rpm_debuginfo_exclude_dirs_add变量排除源目录层次结构的部分。前者通常用于排除系统目录,并具有适当的默认值。发行版维护人员可能希望独立于项目在CPACK_RPM_DEBUGINFO_EXCLUDE_DIRS_ADDITION中设置的内容重写CPACK_RPM_DEBUGINFO_EXCLUDE_DIRS_ADDITION,因此需要使用两个单独的变量。

生成debuginfo包时,有时会遇到如下错误:

CPackRPM: source dir path '/path/to/source/dir' is shorter
than debuginfo sources dir path
'/usr/src/debug/SomeProject-X.Y.Z-Linux/src_0'! Source dir path must be
longer than debuginfo sources dir path. Set
CPACK_RPM_BUILD_SOURCE_DIRS_PREFIX variable to a shorter value or make
source dir path longer. Required for debuginfo packaging. See
documentation of CPACK_RPM_DEBUGINFO_PACKAGE variable for details.

由于在debuginfo处理中重写路径的方式,到源树的路径需要比预期安装的源位置更长。注意,这会影响持续集成系统,其中源树的位置固定。这种对路径长度的需求可能与其他限制冲突,其中路径长度可能需要最小化,所以要仔细考虑这些限制是否适用于项目。

源RPM也可以由RPM生成器生成。它们类似于debuginfo包,但只包含源代码,不包含调试信息。生成方式与其他包生成器的源代码包相同,CPackRPM模块文档包括,如何从源代码RPM构建二进制RPM的基本说明(这是一个验证步骤)。

# Create source RPM
cpack -G RPM --config CPackSourceConfig.cmake

# Verify that a binary RPM can be produced from it
mkdir -p build_dir/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
rpmbuild --define "_topdir build_dir" --rebuild <source-RPM-filename>

CPackRPM模块提供了比上面讨论更多的变量。可以指定包提供相应的信息,或者指导包创建工具自动计算。如果包替换其他包或与其他包冲突,也可以指定这一点。可以提供在包安装和卸载之前或之后运行的脚本,或者需要完全控制,项目可以提供自己定制的.spec文件模板,而不是使用cpack提供的默认模板(这应该尽可能避免,因为否定了cpack提供的大部分功能)。

26.4.8. DEB

DEB格式是Linux系统的另一种主流包格式,DEB和RPM都有许多相似的特性。DEB包只包含相关元数据信息,系统的包管理器使用这些元数据来描述依赖关系、触发脚本等等。

DEB和RPM之间的区别是DEB包不需要特殊的工具。这使得DEB包可以在不使用DEB格式的系统上创建,这可以在基于RPM的系统(如RedHat、SuSE等)上同时生成RPM和DEB包。需要注意的是,当在非DEB系统上创建DEB包时,像dpkg-shlibdeps这样的工具不可用,因此像依赖关系这样的东西就无法计算了。

组件的处理方式与RPM类似,并具有类似的配置。组件是通过设置CPACK_DEB_COMPONENT_INSTALL为true来启用的(这个变量不遵循其他所有特定于DEB变量的命名,这些变量的名称前缀是CPACK_DEBIAN_而不是CPACK_DEB_)。包名类似于CPACK_DEBIAN_PACKAGE_NAMECPACK_DEBIAN_<COMP>_PACKAGE_NAME变量,文件名由CPACK_DEBIAN_FILE_NAMECPACK_DEBIAN_<COMP>_FILE_NAME变量控制。DEB和RPM存在相同的文件命名问题,只是应该使用DEB-DEFAULT,而不是RPM-DEFAULT。如果提供其他值,文件名必须以.deb或.ipk结尾。DEB的版本控制与RPM非常相似,即指定一个架构。提供了等效的DEB变量,在变量名中使用DEBIAN替换RPM。

与RPM相比,DEB包生成器没什么变量来影响依赖项的处理。如果打包是在基于DEB的主机上执行,则dpkg-shlibdeps工具可用,可以通过设置CPACK_DEBIAN_PACKAGE_SHLIBDEPS或组件特定的CPACK_DEBIAN_<COMP>_PACKAGE_SHLIBDEPS变量为true来自动计算动态库的依赖关系。手动指定的依赖项可以通过CPACK_DEBIAN_PACKAGE_DEPENDSCPACK_DEBIAN_<COMP>_PACKAGE_DEPENDS变量提供,如果同时使用手动和自动依赖项,则与确定的依赖项合并。请注意,如果设置了特定于组件的依赖变量,则不会为该组件使用非组件变量。如果启用依赖计算,则将填充特定的组件变量,因此如果只设置CPACK_DEBIAN_PACKAGE_DEPENDS,对于自动依赖的填充组件,会忽略该变量。因此,当自动依赖启用时,总会填充CPACK_DEBIAN_<COMP>_PACKAGE_DEPENDS,而不是CPACK_DEBIAN_PACKAGE_DEPENDS,这样会让项目更健壮。如果通过cpack_add_component()的DEPENDS选项指定组件间依赖关系,项目也应该将CPACK_DEBIAN_ENABLE_COMPONENT_DEPENDS设置为true,然后该选项将在生成的组件包中强制执行这些依赖关系。

与上面的相关,每个包还可以指定需要的动态库。在提供readelf工具的平台上,可以通过将CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS设置为true来自动确定这些库的依赖关系。然后使用readelf工具确定每个共享对象需要的动态库,并将这些信息添加到包中。CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS_POLICY变量控制是否执行精确(=)或最小(>=)需求。

CPackDeb模块文档详细介绍了许多前面没有提到特定于DEB的变量。特别是,一些变量可以用来指定包需要什么、提供什么、替换什么等等。还可以设置一些特定于DEB的元数据项,比如维护人员信息、包组或类别等。开发人员应该参考模块文档,以获得支持的更多信息。

26.4.9. FreeBSD

FreeBSD包生成器相对不成熟,在CMake 3.10中添加。不支持组件,总是生成一个.pkg文件。一些FreeBSD特定的变量可以指定包的元数据,还有一些和DEB或RPM特定变量相同。大部分包配置可以由通用的CPACK_…变量来指定,而不是由特定生成器变量来指定。建议项目开发人员查阅CPackFreeBSD模块文档,以了解可用的特性和限制。

26.4.10. Cygwin

基本的包生成器是针对Cygwin的。它本质上是一个BZip2归档文件的包装器,除了通用变量之外不提供任何配置。项目可能只希望使用一种简单的打包格式。

26.4.11. NuGet

CMake 3.12中增加了对NuGet包格式的支持。新模块支持许多与其他包生成器类似的选项,这些选项都在模块文档中列出。这些选项大部分是能自解释的,它们遵循与上面讨论的其他生成器模式。开发人员应该意识到,由于这个包生成器是CMake的新版本添加的,所以还没有进行广泛的测试,所以建议注意检查在以后的CMake版本的补丁和更新。

26.5. 总结

关于打包要做的第一个决定是项目为其发行版提供哪种包格式。一个好的出发点是考虑为每个目标平台提供至少一种简单的打包格式和一种本机格式。当终端用户希望同时安装产品的多个版本时,打包格式非常方便,因为可以将发布归档解压到不同的目录中。只要包是完全可重定位的,这是一个简单而有效的策略。为了获得最广泛的兼容性,建议对Windows使用ZIP归档,对基于unix的系统使用TGZ归档。

非打包格式取决于目标平台。如果UI安装程序适用于所有平台,考虑使用IFW生成器来获得一致的用户体验。这些安装程序还提供了最大的可定制性、本地化和可下载组件选项。如果更多本机安装程序是首选,选择将取决于项目需求。对于Windows来说,WIX或NSIS都挺好,而且功能相似。对于Mac,多组件项目可能更适合使用productbuild生成器来获得干净的安装体验,但对于非组件项目,用户更倾向于使用DragNDrop生成器,因为提供了更多的简单性和灵活性。Linux上,如果不使用IFW生成器实现跨平台一致性,可以考虑同时提供RPM和DEB包以供用户使用。

要特别考虑用户是否应该能够在“无头系统”上安装产品。这直接影响包格式的选择,以及需要定义和打包组件的方式。对于“无头系统”,非UI安装方法必须可用,包不应该需要与UI有依赖,这样UI组件需要与非UI组件分开。这对于RPM和DEB包格式尤其重要,因为包之间的依赖关系通常由包管理器强制执行,所以需要UI依赖关系的组件包可能会为“无头系统”安装大量不需要的UI相关包。

定义组件名称时,考虑到项目可能作为更大项目的子项目。在组件名称中包含项目名称,以防止项目之间的名称冲突。UI安装程序中显示组件名、包文件名等可以设置为不同的名称,而不是依赖于CMake项目内部使用的组件名。事实上,鼓励为组件设置自定义的显示名称和描述,包括为包格式提供本地化支持。

设置组件细节时,最好使用相关CMake模块定义的命令,而不是直接设置变量。诸如cpack_add_component()、cpack_add_component_group()等命令使用了命名的参数,这使得设置各种选项非常容易阅读和维护。因为参数名称中的任何错误都会被命令捕获,而如果变量名称拼写错误,则直接忽略掉

为各种生成器配置详细信息时,可能有大量的变量会影响打包的方式。许多情况下,默认值可以接受,但项目应该设置一些细节。项目应该显式地设置的变量有三个CPACK_PACKAGE_VERSION_MAJOR、CPACK_PACKAGE_VERSION_MINOR和CPACK_PACKAGE_VERSION_PATCH,因为默认的版本信息很少适合或者可能并不总是可靠的。包名、描述和供应商信息也应该设置。为了确保生成的输入文件中的变量值进行转义,应该显式地将CPACK_VERBATIM_VARIABLES设置为true。

大多数情况下,项目都希望避免在默认安装目录名称中包含版本号。许多安装程序都支持更新现有的安装,因此在产品升级之后,目录名中不适合放置版本号。用户也可能喜欢目录名在升级时保持不变,这样他们就可以编写跨版本的包装器脚本、启动器等等。简单的压缩包是例外,这就是为什么非组件打包生成的默认行为大多遵循常规,即将提取的内容放在适当的子目录下,该子目录包括包名和版本。对于基于组件的包,项目需要将CPACK_COMPONENT_INCLUDE_TOPLEVEL_DIRECTORY设置为true以获得类似的行为。

RPM和DEB包应该倾向于将包文件名设置为RPM-DEFAULT和DEB-DEFAULT。这确保了包文件名遵循通用的命名约定,而且将包版本和体系结构信息合并到包文件名中,是一种更简单的方法。不要依赖于CPack提供的默认RPM或DEB包文件名,因为它们忽略了版本和架构信息。

使用RPM生成器时应该为发布包保留调试信息,请考虑使用debuginfo功能,而不是阻止包创建的剥离步骤。防止剥离需要禁用包生成的其他需求,并需要将调试信息作为发布包的一部分。debuginfo功能允许提供适当的发布包,包括在单独的包中捕获的调试信息,这些包可以分发给用户,也可以不分发。

如果需要从单个cpack调用生成多架构或调试和发布包,请使用CPACK_INSTALL_CMAKE_PROJECTS变量来合并来自多个构建树的组件。使用这样的安排时,总是最后安装发布组件,以避免调试和发布组件安装到相同的文件名和目录中。理想情况下,无论如何都不应该发生这种情况。

探索和理解项目支持的每个UI安装程序提供的定制选项。强烈建议定义产品图标,以确保专业的外观。项目还应该提供自述文件、欢迎信息和许可详情,这样安装程序或包的元数据就不会使用CPack提供的占位符。

Last updated