RFC-0154:子软件包

RFC-0154:子软件包
状态已接受
领域
  • 软件交付
说明

允许软件包声明对特定于版本的子软件包的依赖项,以提高封闭性和可再定位性。

问题
  • 89252
Gerrit 更改
作者
审核人
提交日期(年-月-日)2022-01-25
审核日期(年-月-日)2022-03-23

摘要

此方案用于在 Fuchsia 中添加对定义和解析子软件包的支持。子软件包是从一个软件包(“A”)对另一个软件包(“B”)的命名引用,指定一个单向依赖项(从“A”到“B”)。子软件包还可以包含子软件包,从而根据顶级软件包中的版本固定相关软件包创建有向无环图 (DAG)。对于子软件包,软件包“A”中的资源可以通过其子软件包名称(由包含的软件包定义)引用软件包“B”。如果“B”是“A”的子软件包,则任何提供“A”的系统(例如,Fuchsia 归档、Blob 存储区)也必须让“B”可用。因此,子软件包支持两个重要的系统属性:

  • 封闭性 - 将程序(或测试)与特定软件包依赖项打包在一起。
  • 原子性 - 系统应保证,如果包含子软件包的软件包成功解析,则其所有子软件包都将成功解析。(本 RFC 中提议的方法将通过在从软件包解析请求返回成功结果之前快速解析整个子软件包层次结构来保证这一点。)
  • 可再定位性 - 软件包可随所有必需资源一起分发到单个归档文件(例如用于下载)或备用 Fuchsia 软件包服务器中。

设计初衷

此 RFC 提出了一种方法来表达和支持软件包间嵌套依赖项(子软件包)的概念。虽然目前不允许软件包依赖于其他软件包,但此 RFC 修改了限制,规定软件包不得依赖于其他软件包,除非通过遏制。

请注意,虽然本 RFC 中讨论的初始激励用例都与测试相关,但子软件包可能在其他场景中也很有用。子软件包将先用于测试,因为我们将能够采用在生产环境中无法接受的方式进行测试。安排注意事项除外,这样做的目的是在测试和生产场景中完全支持子软件包。

为何此时推荐?

目前最主要的用例是测试场景,使得需要软件包间依赖项。测试需要在封闭环境中与被测组件一起执行特定组件(通常是虚构组件或模拟组件)。为防止全局系统状态(即恰好安装的软件包集)影响测试的正确性,依赖和包含依赖项的确切版本非常重要。我们将此属性称为“封闭封装”。

我们通过将所有组件依赖项包含在一个软件包中,在树内对测试进行封闭式打包。但是,树外 (OOT) 的现状是依赖于绝对软件包网址。这些测试未进行封闭式打包,这会导致平台软件包网址成为隐式 ABI。

例如,目前 Flutter 集成测试通过组件网址 fuchsia-pkg://fuchsia.com/scenic#meta/scenic.cm 声明了对 Fuchsia Scenic 组件的依赖。这只是声明 Fuchsia 平台将通过该网址引用提供一些 View 实例。如果 Views 或其某个依赖项(测试开发者还必须发现和声明这些依赖项)更改了其接口、行为或已被移除,那么测试可能会在运行时失败。这已经是测试中断的常见原因,随着我们的规模扩大,情况只会变得更糟。

相比之下,使用子软件包时,Flutter 集成测试作者可以:

  1. 在构建时获取包含特定景观版本的软件包。(具体机制不在范围内。)
  2. 将该软件包作为其测试软件包的子软件包包含在内。
  3. 在其测试的组件清单中使用网址 scenic#meta/scenic.cm(而不是 fuchsia-pkg://fuchsia.com/scenic#meta/scenic.cm)声明子组件。

此版本的测试更为封闭,无论设备上运行的是哪个版本的“Sense”安装,它的行为始终相同。

注意:子软件包支持像这样的 OOT 测试场景,但分发 Fuchsia 平台软件包以在测试中重复使用的过程不在 RFC 的范围内。但是,在 RFC 获得批准后,我们可能会在后续工作中定义此流程,作为 Fuchsia SDK 的扩展。

我们致力于在树内组件和 OOT 组件之间提供一致的体验:单个测试软件包明确声明其所有封闭依赖项,这些依赖项可作为原子组提供给测试。

我们建议以子软件包嵌套的形式支持软件包间依赖项,而不是从树内重新应用现有策略(这样做需要构建包含多个组件的新软件包)。这样,我们就可以独立打包和依赖组件,而无需考虑复杂的命名空间。

利益相关方

教员:hjfreyer@google.com

审核者

名称 聚焦区域
wittrock@google.com 瑞典克朗
jsankey@google.com 瑞典克朗
geb@google.com 组件框架
shayba@google.com Build
etryzelaar@google.com 工具
aaronwood@google.com 组装/包装移动
cgonyeo@google.com RealmBuilder
ampearce@google.com 安全性

咨询:computeddruid@google.com

社交

在发布此 RFC 的初稿之前,SWD 团队参与了该草稿的制定。 组件框架团队及时了解相关工作的进展,并在每周会议上就范围和功能提供反馈,包括 CF 团队成员(包括 RFC 作者)。

设计

受子软件包影响的用例

  • 嵌套软件包依赖项的软件包内声明
    • 软件包可以包含列出其子软件包的元数据文件,并且构建系统必须能够根据软件包目标的依赖项填充此元数据文件。
  • 相对组件分辨率
    • 例如,开发者可以将对组件 fuchsia-pkg://servername/some-package#meta/child_component.cm 的引用替换为相对引用 child_package#meta/child_component.cm;其中 child_package 是对 some-package 的指定引用,在构建时是版本锁定的(通过软件包哈希)。
  • 软件包依赖项树遍历
    • 例如,通过发布单个顶级软件包,软件包服务器可以使用嵌入式软件包引用来自动定位和加载软件包依赖项。
  • 软件包树归档
    • 从指定的顶级软件包构造包含该软件包和所有依赖项的单个文件。

子软件包表示法

软件包通过子软件包的软件包作用域名称声明对子软件包的引用,并将其映射到在同一软件包存储区和同一软件包集中定义的软件包的软件包哈希值。(例如,对于从“universe”软件包集解析的父软件包,其子软件包也必须通过“universe”软件包集进行解析)。RFC 的其余部分将这些声明的概念表示形式描述为一个名为 meta/subpackages 的文件,该文件与 meta/contents 当前使用的格式 (=).

此类文件(如果存在)必须至少包含子软件包的名称及其软件包哈希(引用的子软件包的 meta.far 的“内容地址”)。

子软件包名称在单个 meta/subpackages 文件中必须是唯一的,但在不同的软件包中不必是唯一的。因此,软件包可完全控制其子软件包的命名,并且多个软件包可能会以不同的本地名称包含同一子软件包。

子软件包引用被视为引用软件包的“私有数据”。例如,父级软件包 A 可能包含两个子软件包:软件包 BC(通过其各自的软件包哈希引用)。软件包 B 还可能包含对同一软件包哈希 C 的子软件包引用。顶级父级 A 及其子软件包 B 都不知道常见依赖项。(换句话说,父级软件包只能解析它的子软件包,一个级别。父级无法直接将软件包网址解析为子软件包的子软件包。实际上,此 RFC 没有定义用于表示子软件包子软件包的语法。)

注意:单个子软件包也可以独立发布并作为顶级软件包提供。(例如,可以将生产组件发布为由 fuchsia-pkg 网址标识的顶级组件,但该组件的软件包也可以作为子软件包包含在一个或多个封闭测试中。)通过 Fuchsia 软件包 URI 解析顶级软件包时,默认行为是检索该软件包的最新发布版本。另一方面,子软件包始终通过“软件包哈希”指代特定版本。

使用子软件包构建软件包

向软件包中添加子软件包的方式与添加常规文件的方式类似,但需注意一些特殊事项。

各种 Fuchsia SDK 构建系统(目前为 GN 和 Bazel)通常支持创建“软件包”目标,该目标可以依赖于其他目标来纳入软件包中。在这些构建系统中,添加对软件包的依赖项会通知构建系统也生成相关软件包,但目前不会在生成的软件包中对该依赖项进行编码。例如,以下 GN 代码仅会在生成软件包“A”时生成软件包“B”。

# GN format
fuchsia_package("A") {
  deps = [
    ":B",
    other_deps,
    ...
  ]
}

fuchsia_package("B") {
  ...
}

通过对 fuchsia.git 中现有的软件包间依赖项进行粗略调查,我们发现,在大多数情况下,当目标软件包依赖于另一个软件包时,目标软件包中的组件应加载依赖的软件包。在这些情况下,依赖软件包可以与目标软件包捆绑为子软件包。但是,由于现有 GN 规则并不强制执行这种解释,因此我们无法推断出这种解释。

因此,此 RFC 建议使用显式变量 --subpackages--- 来声明子软件包目标。subpackages 列表中的每个目标都必须在生成的 fuchsia_packagemeta/subpackages 文件中产生相应的条目。

此列表中的目标还会推断 build 依赖项,因此 subpackages 中的目标也不需要显示在 deps 中。如需将包含已声明软件包依赖项的软件包转换为包含子软件包的软件包,请将所有(或选定)软件包目标从 deps 移至 subpackages。概念上,对 fuchsia_package 的更改可能如下所示:

# GN format
fuchsia_package("A") {
  subpackages = [ ":B" ]

  deps = [
    other_deps,
    ...
  ]
}

fuchsia_package("B") {
  ...
}

构建系统应将子软件包名称默认设置为包含的目标软件包的名称,但可以支持使用常见习语替换此名称。

为了方便使用现有工具构建包含子软件包的软件包,必须在 Fuchsia 树内构建系统中更新构建规则和脚本,并且应在 Fuchsia GN SDK 和下游构建环境中更新。(如需了解受影响的构建环境以及所需的更改,请参见实现部分。)

解析子软件包

子软件包解析会解析对已知软件包的已命名子软件包的相对引用。

fuchsia.pkg.PackageResolver 协议的 Resolve 方法将修改为返回 ResolutionContextResolve 只会解析绝对软件包网址。系统会额外添加一个 ResolveWithContext 方法,用于接受额外的 context 参数(ResolutionContext),还会为已解析的软件包返回新的 ResolutionContextResolveWithContext 会解析绝对软件包网址(忽略 context)或相对软件包网址。这些更改由以下概念性 FIDL 代码段表示:

   library fuchsia.pkg;

   ...

   const MAX_RESOLUTION_CONTEXT_LENGTH = <TBD>;
   type ResolutionContext = bytes:MAX_RESOLUTION_CONTEXT_LENGTH;

   protocol PackageResolver {

     /// Resolves an absolute component URL.
     /// ...
     Resolve(resource struct {
         package_url string;
         dir server_end:fuchsia.io.Directory;
     }) -> (struct {
         resolved_context ResolutionContext;
     }) error ResolveError;

     /// Resolves a component URL, which may be absolute or relative. If
     /// relative, the component will be resolved relative to the supplied
     /// `context`.
     ResolveWithContext(resource struct {
         package_url string;
         context ResolutionContext;
         dir server_end:fuchsia.io.Directory;
     }) -> (struct {
         resolved_context ResolutionContext;
     }) error ResolveError;

     ...
   }

注意:在选择用于上下文的字节数组时,存在一些限制条件、一些 FIDL 限制以及仔细考虑的意见。如需了解其他背景信息,请参阅“替代方案”部分:使用特定于上下文的 Resolver替代方案:为解析上下文值使用 FIDL 类型表示法

对于 package_url 中声明了子软件包的软件包(“父”软件包),在解析子软件包时,返回的 resolved_context 必须作为 context 输入参数传递给对 ResolveWithContext 的后续调用。(如果调用方不解析已解析软件包的子软件包,则可以忽略返回的 resolved_context。)

子软件包实现不得降低现有服务和组件的稳健性和存续性。换言之,子软件包不得导致非关键(可重启)组件需要被标记为关键,并且无状态协议(例如 fuchsia.pkg.Resolve(),还包括建议的 ResolveWithContext,它将取代对 Resolve 的一些调用)必须保持无状态。

context 值的实现可能会受到此限制条件的影响。只要父软件包在系统中处于活跃状态,ResolveWithContext 就必须接受 ResolveResolveWithContext 返回的任何 context。(从概念上讲,父软件包的软件包哈希值足以从同一 Merkle-hash-indexed 软件包存储区中无状态解析子软件包。)

注意:确定父级是否仍处于“活跃使用”状态是软件包垃圾回收的一个问题,需要在软件包解析器中实现子软件包期间解决。

对于绝对 package_url,系统会忽略 ResolveWithContextcontext 参数。

注意:由于此 RFC 将子软件包解析限制为父级(下一级)已声明的子软件包,因此相对路径不得包含斜杠 (/)。

从子软件包加载组件

组件解析器负责解析组件网址,将其转换为随后经过解析的软件包网址,然后从已解析的软件包加载组件清单。加载的清单和软件包内容用于创建新组件,其中包括用于按相对路径解析子软件包的上下文。

fuchsia.component.resolution.Resolver 协议的 Resolve 方法只会解析绝对的组成部分网址。系统会额外添加一个方法 ResolveWithContext,它接受额外的 context 参数(fuchsia.component.resolution.Context)。ResolveWithContext 会解析绝对的组成部分网址(忽略 context)或相对的组成部分网址。这些更改由以下概念性 FIDL 代码段表示:

   library fuchsia.component.resolution;

   ...

   // Note the context length, in bytes, must be at least the size of
   // fuchsia.pkg.MAX_RESOLUTION_CONTEXT_LENGTH, plus the size required to
   // accommodate additional component context information, if any.

   /// The maximum number of bytes for a `Context`.
   const MAX_RESOLUTION_CONTEXT_LENGTH uint32 = 8192;

   /// A byte array that persists a value used by the target `Resolver` to locate
   /// and resolve a component by relative URL (for example, by a subpackage
   /// name).
   alias Context = bytes:MAX_RESOLUTION_CONTEXT_LENGTH;

   protocol Resolver {

     /// Resolves a component with the given absolute URL.
     /// ...
     Resolve(struct {
         component_url string;
     }) -> (resource struct {
         component Component;
     }) error ResolverError;

     /// Resolves a component with the absolute or relative URL. If relative, the
     /// component will be resolved relative to the supplied `context`.
     ///
     /// `component_url` is the unescaped URL of the component to resolve, the
     /// format of which can be either:
     ///
     ///   * a fully-qualified absolute component URL
     ///   * a subpackaged-component reference, prefixed by a URI relative
     ///     path to its containing subpackage (for example,
     ///     `child_package#meta/some_component.cm`); or
     ///   * a URI fragment to a component in the current package (for
     ///     example,`#meta/other_component.cm`)
     ///
     /// `context` is the `resolution_context` of a previously-resolved
     /// `Component`, providing the context for resoving a relative URL.
     ResolveWithContext(struct {
         component_url string;
         context Context;
     }) -> (resource struct {
         component Component;
     }) error ResolverError;
   }

返回的 Component 类型会被修改以包含 resolution_context,以便在解析相对于此 Component 的其他组件时使用。

   type Component = resource table {

       ...

       /// The context used to resolve `component_url`s relative to this
       /// component.
       5: resolution_context Context;
   };

例如,在解析名为 parent 的组件后,可以通过后续调用 ResolveWithContext(subpackaged_url, parent.resolution_context) 来解析子打包组件。

对于绝对 component_url,系统会忽略 ResolveWithContextcontext 参数。

对于相对 component_url

  • 客户端必须使用 ResolveWithContext。使用相对组件网址调用 Resolve 将返回错误代码 ResolveError::INVALID_ARGS

对于相对的子打包组件网址:

  • component_url 以相对路径(子软件包名称,后跟特定组件基于 # 的 fragment,例如 child_package#meta/some_component.cm)开头。
  • 如果软件包解析器返回 PACKAGE_NOT_FOUND(或某些依赖于软件包商店的等效错误),组件解析器必须返回 ResolveError:PACKAGE_NOT_FOUND

对于相对资源组件网址(与另一个组件相同的软件包):

  • component_url 是一个 URI 片段(例如 #meta/other_component.cm,如 RFC-0104:相对组件网址中所述):
  • fragment 必须引用与先前已解析的另一个组件位于同一软件包中的组件(即“对等”组件)。
  • context 值必须引用与其对等方相同的软件包版本(相同的软件包哈希)。

注意:使用 context 解析相对资源组件网址(通过 URI 片段)会改变相对解析器的当前行为。目前,相对解析器会根据其父组件构造绝对软件包网址,并附加相对 fragment 部分。此行为不能保证从相同的软件包版本检索父组件和子组件。使用子软件包时,并非总是能通过父组件或对等组件构建绝对软件包网址。另一方面,将软件包固定到用于解析软件包中捆绑的所有组件的特定软件包哈希可能是用户所希望的行为;在这种情况下,使用上下文是一种改进。

代表现有组件(“父组件”)使用相对 URI(路径或片段)解析组件网址时,组件管理器必须将解析委托给用于解析父组件的同一 Resolver

上下文值应被视为组件解析器内部的实现细节,并且对客户端是不透明的。Resolver 可以将 resolved_context 值(从 PackageResolver::Resolve...() 返回)转发为组件分辨率 context 值。(API 不会阻止 Resolver 返回不同或经过扩充的上下文值,但这可能不是必要的。)

以下是解析具有子打包子项的组件的概念性示例(假设启动后环境):

  1. 如需从顶级软件包 P 中加载组件 A,请执行以下操作:

    a. 组件管理器获取为 fuchsia-pkg 方案注册的 Resolver capability,并调用 Resolver::Resolve("fuchsia-pkg://fuchsia.com/package-p#meta/component-a.cm")

    b. Resolver 会提取软件包网址并调用 PackageResolver::Resolve("fuchsia-pkg://fuchsia.com/package-p", ...),返回从中加载组件的 fuchsia.io.Directory 以及 resolved_context 值。

    c. Resolver::Resolve() 使用 resolution_context 构造并返回已解析的 Component,组件管理器会将它缓存在该组件的状态中。

  2. 如需从子软件包 child-package 中加载子组件 B(相对于上面的组件 A),请使用以下代码:

    a. 组件管理器获取用于解析 component-aResolver capability,并调用 Resolver::ResolveWithContext("child-package#meta/component-b.cm", component_a.resolution_context)

    b. Resolver 会从组件网址中提取相对软件包路径(子软件包名称),并从组件 context 输入参数中提取 package_context,然后调用 PackageResolver::ResolveWithContext("child-package", package_context),返回从中加载组件的 fuchsia.io.Directory 以及子软件包自己的 resolved_context 值。

    c. Resolver::ResolveWithContext() 使用新组件的 resolution_context 构造并返回已解析的 Component,组件管理器会将该组件缓存在该组件的状态下。

后续工作

  • 整合 subpackagescontents 文件 - 如果建议的永久性 FIDL 软件包内容的 RFC 获得批准后,应更新子软件包文件,使其与替换 contents 的新格式匹配,或者合并文件(使用扩展字段来区分内容条目和子软件包条目)。
  • 使用摘要统计信息注释 subpackages 文件条目 -“永久性 FIDL 软件包内容”RFC 提供了一种机制,可让每个子软件包到软件包哈希的映射包含其他数据。包含摘要统计信息可能会很有帮助,例如内容大小(包括每个子软件包及其嵌套依赖项的总览大小)。例如,在软件包提取操作期间、加载具有大量依赖项的软件包时,可以使用此属性进行进度报告。
  • 改进运行时依赖项解析 - 另一个正需要考虑的概念(适用于未来的 RFC)是支持具有绝对路径(即以斜杠“/”为前缀)的相对软件包网址,以便从提供“当前”软件包(或当前组件)的“同一软件包服务器”请求顶级软件包。(正如此 RFC 中所述,从当前软件包中解析子软件包网址同样可了解当前软件包服务器。)
  • 用于解析和加载资源包内容的通用 FIDL 服务 - 目前,Fuchsia 软件从文件包加载内容的唯一方式是在文件包中添加一个组件。自定义组件将负责路由目录或提供 FIDL 协议,以预配软件包中的资源。我们正在考虑未来的方案,在其中纳入一个更易于访问的 FIDL API,用于加载该软件包中的子软件包和/或资产。例如,这可用于将视觉资源或可选择的界面“主题”作为资源包分发,而无需执行额外的步骤来实现组件中介。
  • 延迟加载子软件包 - 此 RFC 建议,在加载(即快速加载)顶级软件包时(以递归方式)加载软件包及其所有子软件包。未来提议的扩展将允许将给定的子软件包声明为“可延迟加载”。在明确请求给定软件包之前,这可用于避免将软件包从远程服务器导入存储空间受限的设备的费用。(如需了解详情,请参阅 Eager 软件包加载。)
  • 针对子打包组件的热重载 - 此 RFC 提出了一种严格的软件包间版本控制实现方法:软件包哈希根据其内容的哈希值(包括其子软件包哈希)计算得出。因此,如果子软件包发生变化(通过子软件包软件包哈希值的变化来指示),则父软件包也会发生变化。您可能希望重新加载子软件包而不重新加载父软件包(例如,在开发时“热重载”组件),反之亦然。允许此类行为可能会降低子软件包的某些功能,包括可靠性和安全性。此外,在组件管理器如何解析和重新加载新版本的组件方面,存在一些已知的漏洞(请参阅 https://fxbug.dev/42145182)。如果这些已知问题得到解决,则可以修改子软件包依赖项限制(在未来的方案中),以允许父级软件包根据较宽松的协定(例如行为、API 和/或 ABI 保证)声明某些依赖项。

实现

Fuchsia 树内构建系统(GN 规则和脚本)

必须更新 build 规则、脚本和文件格式(包括用于生成 Fuchsia 软件包的中间文件格式),以便在软件包构建和/或归档的每个阶段转发 subpackages 列表,并最终生成新的 meta/subpackages 文件与软件包 contents 文件。

对于 subpackages 中的每个软件包目标,meta/subpackages 文件必须将声明的子软件包名称(默认为子软件包目标名称)映射到子软件包目标的软件包哈希值(即子软件包 meta.far 的 blob 哈希值)。

可能需要针对同一阶段更改格式(以添加子软件包引用)或需要补充文件(例如,如何将 contentssubpackages 配对)的一些文件格式包括:

  • 软件包“build 清单”(或 archive_manifest),通常以 .manifest 扩展结尾
  • package_manifest.json

Fuchsia 树外构建环境(GN、Bazel 和支持脚本)

应以与 Fuchsia 树内构建规则和脚本类似的方式更新构建规则和脚本,以便在树外代码库中启用子软件包。受影响的代码库包括 Chromium 和 Flutter/engine。(请注意,flutter/engine 目前实现的是经过修改的 GN SDK 构建规则和脚本副本,因此,需要在 GN SDK 代码库和 Flutter/engine 中均进行类似的更改(如果不完全相同)。

  • package.gni 使用 --manifest-path 调用 prepare_package_inputs.py,以生成 ${package_name}.manifest(在脚本中称为 archive_manifest,或在 pm CLI 文档中称为“build 清单”)
  • pm_tool.gni 使用 -m ${archive_manifest}-output-package-manifest 调用 pm build 以生成 package_manifest.json

软件包管理器软件包命令行界面 (CLI)

将通过更改 ffx package 来修改软件包管理器命令。

注意:pm 命令将被废弃,取而代之的是 ffx package。只有在工作流需要时才对 pm 进行更改,并且无法更新为使用 ffx package 替换命令。

  • ffx package build(原为 pm build
    • 此命令需要更改为接受其他参数和文件,或接受修改后的输入和输出文件格式,以包含额外的子软件包声明。
  • ffx package export(原为 pm archive
    • 此命令会读取 contents 文件,并生成包含所有引用的 blob 的 .far 文件。
    • 对于包含 subpackages 文件的软件包,将扩展 export 行为,以(递归方式)将这些子软件包捆绑到生成的归档文件中(请参阅捆绑软件包依赖项以进行分发
  • 其他 ffx package 命令可能还需要进行更改以适应 subpackages(例如 downloadimport)。系统会在实现时调查对这些命令的影响。如 RFC-0124:分散式产品集成:工件说明和传播中所述,这些更改可能会影响或受其他计划中的更改的影响。

捆绑软件包依赖项以进行分发

子软件包的一个关键用例是支持软件包分发,方法是确保软件包及其所有直接和间接依赖项可以作为一个软件包重新分配。与开发以递归方式遍历子软件包依赖项的过程相比,软件包格式并不重要。

ffx package 工具是一个逻辑工具,可实现一种识别和定位每个子软件包的内容(扩展的软件包目录或 .far 归档)的方法。

此 RFC 建议将软件包工具扩展为可感知子软件包,以支持分发软件包及其子软件包依赖项,而不是将实现用于封闭打包的自定义脚本的负担放在每个树外用户身上。例如,ffx package 将扩展为将软件包及其依赖项打包到单个文件归档文件中。

封闭软件包归档采用单个文件的形式,它将是所有贡献软件包的展开内容,从而生成一个索引且涵盖所有软件包中的所有 blob 的扁平化集合。

软件包解析器

必须更新 fuchsia.pkg.PackageResolver 协议的实现,才能实现上述设计部分中所述的行为。

Eager 软件包加载

软件包解析器必须以递归方式在内部解析所有子软件包,直到提取完所有子软件包,然后再返回根软件包的解析结果。采用这种方法是为了简化原子性属性的实现,因此所有必需的子软件包都保证在组件启动之前已解决。

强制执行 Eager 软件包解析可能也支持已获批准的 RFC-0145:即时软件包更新中的一些要求。值得注意的是,软件包及其子软件包树可用于实现 Eager 软件包更新 RFC 的“软件包组”前提条件。

组件解析器

必须更新 fuchsia.component.resolution.Resolver 协议的实现,才能实现上述设计部分中所述的行为。

RealmBuilder

必须更新 RealmBuilder,使其能够声明子软件包以及响应在组件网址中使用相对路径的组件解析请求。使用 RealmBuilder 的封闭测试目前无法访问软件包解析器。必须实现对通过子软件包声明加载封闭式捆绑软件包的支持,并使其可供封闭测试和系统测试使用。

性能

遍历子软件包可能会增加识别要下载的所有 blob 的延迟时间(通过遵循多级间接控制),尽管这在实践中不太可能重要。例如,比较加载具有 N 个 blob 的单个软件包与加载包含 N 个唯一 blob 的子软件包的软件包:

  • 父级
    • N 个 blob

  • 父级
    • N_0 个 blob
    • 子级 1
      • N_1 个 blob
      • Child2
      • N_2 个 blob
      • ...

在前一种情况下,所有 N 个 blob 都是预先已知的,可以并行加载。 在第二种情况下,软件包解析器必须连续跟踪父项和子项之间的链接,以累积要加载的全部 N 个 blob。这种遍历会产生以树深度为边界的额外延迟时间。如果递归 blob 加载的延迟成为问题,则可以使用多种实现选项来减少延迟。例如,可以在构建软件包时并行遍历子软件包链接和加载 blob,或者包含完整的子软件包内容地址集。

向后兼容性

相对资源组成部分网址的解析行为

解析现有软件包网址和组件网址的行为保持不变,但有一个例外:相对资源组件网址(由 #meta/child.cm 等 URI 片段表示)将需要使用组件 context(建议的实现将保证这一点),但此行为略有变化。当前的相对解析器会将 fragment 连接到父组件的软件包网址,然后重新解析软件包和组件。但是,解析器可能正在加载新版本的软件包。这被认为是原始实现的潜在风险。context 将保证从解析“父级组件”的同一软件包中解析相对组件。

解读已解析组件的 fuchsia.component.resolution.Package

解析组件后,返回的 fuchsia.component.resolution.Component 类型会包括 fuchsia.component.resolution.Package 类型的 package 字段,其中包含对包含所返回的 Component 的软件包的 package_url 的引用。

   type Package = resource table {
       /// The URL of the package itself.
       1: url string;

       /// The package's content directory.
       2: directory client_end:fuchsia.io.Directory;
   };

由于子软件包始终是相对的,因此 package_url 的任何现有使用都会受到影响。本 RFC 中描述的子软件包解析过程不会使用 Package 类型中存储的任何信息,因此,对 Package 或该类型的解读方式所做的任何更改都不会对子软件包 RFC 设计产生重大影响。

实现时需要考虑的替代方案包括:

  1. 存储仅引用软件包服务器和软件包哈希值的 url(例如 fuchsia-pkg://fuchsia.com?hash=123456789),这足以重新解析子软件包的内容,但不会保留有关父组件或其子软件包名称的任何信息。
  2. 将子软件包路径存储在 url 中,并向 Package 添加可选的解析上下文字段(解析组件软件包的上下文)。

FIDL 方法变更

fuchsia.pkg.PackageResolverfuchsia.component.resolution.Resolver 中,Resolve() 方法都将更改为返回已解析的新上下文,并且系统将添加名为 ResolveWithContext() 的其他方法以支持 context 输入参数。这可能不需要软转换。

软件包表示法变更

向 Fuchsia 添加子软件包不会改变没有子软件包的软件包的表示方式。现有的软件包网址和组件网址不会受到影响。开发者可以选择将完全限定的组件网址(例如,使用 fuchsia-pkg://fuchsia.com/top-level-package#...)替换为对其某个子软件包的引用(使用 child-package#...,将 child-package 映射到 top-level-package 的软件包哈希值)。

工具变更

较低版本的 Fuchsia、软件包服务器和某些主机端工具(如 pmffx package)不支持使用子软件包发布的软件包,或使用软件包相对路径发布的软件。Fuchsia 系统和主机端工具将需要更新并重新编译。

软件包名称语法

子软件包名称的作用域限定为父级软件包,实际上是具有相同软件包哈希的任何顶级软件包名称的别名。这意味着子软件包名称的语法不需要镜像软件包名称的语法。

不过,可能会经常使用相同的名称来指代独立软件包并将其称为子软件包。

由于子软件包是分层的,因此可以自然地将嵌套的子软件包视为可命名,可能会使用斜杠作为分隔符。例如,fuchsia-pkg://fuchsia.com/parent/child/grandchild#gc-component.cmchild/grandchild#gc-component.cm。请注意,此 RFC 既不制裁也不禁止此类表示法,但使用斜杠分隔符会暴露一个潜在问题。

目前,软件包名称可以包含单个斜杠。其后的内容被视为“变体”,例如 /0,这在 Fuchsia 中目前很常见。

值得注意的是,/0 已被弃用,如果从常见用途中移除,或许可以为子软件包释放 / 的含义(尽管目前并未提议这样做,并且必须在未来的 RFC 中遵循)。

如果软件包命名层次结构存在竞争用例,则可以使用备用分隔符(例如,:)进行子软件包嵌套,或者子软件包可以使用 /,而备用分隔符可以实现其他含义。

此 RFC 建议从允许的子软件包名称语法中保留至少 /:

安全注意事项

可审核性和可执行性

提议的设计考虑到了 Fuchsia Security 负责人迄今为止发表的以下意见:需要尽可能减少对系统安全状况的更改,并简化对可能影响可审核性和可执行性的更改的审核。

例如,父级及其所有子软件包(以递归方式)必须源自同一软件包存储区,并被视为同一软件包集的一部分(例如,全部来自“base”或全部来自“universe”)。使用相同的软件包集意味着子软件包层次结构中的所有软件包都必须遵守相同的可执行政策。

基于软件包或组件网址的系统许可名单

目前在特权许可名单中标识的软件包或组件(由于其敏感功能)可作为子软件包包含在内,但需要一些额外的限制条件才能保留其所需的权限。在测试之外,父软件包不得包含需要比父组件更多权限的子组件的子软件包。在封闭测试中运行的组件可能需要遵守此规则的例外情况,但如果列入许可名单的软件包或组件在封闭测试中用作子软件包,则特权功能应替换为模拟或等效功能(如果可能)。

如果这一限制被证实是其他系统改进的障碍,则应咨询安全团队以寻找其他替代方案。

受控对特权软件包解析操作的访问

目前,鉴于平面软件包命名空间,PackageResolver 客户端能够读取 blobfs 中的所有内容;因此,从软件包服务器请求软件包并读取其 BLOB 的功能属于特权操作。

子软件包将仅提供指定的 blobfs 软件包子集(目前深度为一层),但如果软件包声明依赖于另一个软件包,则没有更高的权限来读取该软件包的内容。此 RFC 建议,在从子软件包请求功能或资源时,对非特权客户端可以执行的操作保持相同的限制。

在特定软件包中运行的组件可能无法查看其子软件包的内容,但它们可以查看 meta/subpackages 文件并手动解析其内容(如果它们具有 PackageResolver 功能)。

与目前在单个软件包中同时包含相互依赖组件相比,这种设计可以提高安全性。在该解决方案中,每个组件可以任意查看彼此的内容。由于组件可能只能看到其运行的软件包,而看不到子软件包的内容,因此此设计会隐藏这些实现细节。

植入风险:行头屏蔽

如果我们实现深度优先的软件包解析,嵌套的子软件包 DAG 可能会阻止对其他软件包的解析。这可能会导致解析器死锁,在以递归方式解析其子软件包时,会继续为父项保留阻塞资源。

由于 RFC 声明其将仅实现紧急解析,因此实现人员可以通过确保无需完全解析父级即可释放解析器资源来解析子软件包,从而降低这种风险。

隐私注意事项

我们预计隐私状况不会受到任何影响。

测试

我们将添加单元测试,以验证其他功能。现有测试将有助于识别意外回归情况。

主机端打包测试将在处理子软件包时验证 ffx package(和/或 pm,如有必要)以及相关工具的行为。

将编写封闭的集成测试来验证来自子软件包的组件解析。

文档

以下已知文档需要更新:

  • 描述 Fuchsia 软件包网址和组件网址的现有文档(用于说明如何引用子软件包)、软件交付文档以及组件框架概念文档。对于目前显示包含 fuchsia-pkg:// 网址引用的 CML 示例的示例,您可以使用子软件包来扩充示例。
  • 应更新关于相对组件参考的文档,以说明与子打包组件相比的异同。

缺点、替代方案和问题

在将范围缩小到此 RFC 的当前版本之前,请参阅未来工作子部分(在“设计”部分中),其中介绍了一些未来的增强功能,以及考虑的替代方法和其他功能。

此外,以下小节还介绍了所提议的计划的一些替代方案。

替代方案:在子组件网址中注入版本哈希值

我们可以添加工具,而不是向软件交付堆栈引入子软件包,而是通过向子组件网址添加软件包哈希限定符,使组件能够声明版本化依赖项。例如,在 parent.cmchildren: 列表中,以安全方式打包的依赖项将显示为 url: "fuchsia-pkg://fuchsia.com/some_package?hash=1234#meta/a_component.cm"

只要工具和基础架构可以保证软件包存储区中有 some_package 的版本,此引用即可与现有解析器搭配使用。您无需创建新文件(例如此 RFC 中描述的概念性 subpackages 文件)并更改 SWD 以解释该文件。

这种替代方案遭到了拒绝,因为与建议的解决方案相比,该方案需要工具和构建基础架构来提高此工作的复杂性,尤其是对工具和构建工作流。例如:

  • 这种替代方案假定软件包哈希未嵌入到开发者 CML 或在运行时解析组件网址的代码中,例如为了将它们添加到 collection 中。当开发者(以某种方式)指明希望组件网址在构建时固定到特定哈希时,需要添加某种机制来更改组件清单和源代码中静态引用的已编译表示法,从而在网址中注入 ?hash=<blobid> 限定符。
  • 组件清单中的封闭依赖项支持运行时解析,但需要一种单独的机制来支持使用依赖项重新定位打包。解析修改后的清单和代码可能是不切实际的,因此任何用于确定在何处进行这些修改的机制也需要生成一些额外的元数据来描述依赖关系图。然后,可将工具更改为读取该元数据,以便执行多种操作,例如将软件包及其所有依赖项归档,或将软件包及其所有经过哈希固定的依赖项发布到软件包存储区中。

替代方案:使用针对特定上下文的解析器

解析组件或软件包时,可以修改 Resolve 方法,以包含 context_resolver 请求句柄 (server_end) 作为新的输入参数,而不是传递并返回 resolution_context

从概念上讲,PackageResolver 和/或 Resolver 可以对此 FIDL 协议模式建模:

protocol Resolver {
  Resolve(struct {
      component_url string;
      context_resolver server_end:Resolver;
  }) -> (resource struct {
      component Component;
  }) error ResolverError;

  ...
}

在这种替代方案中,必须在后续调用中使用 context_resolver 的客户端,代表返回的 PackageComponent 解析组件引用(例如,在解析 CML 声明的 children 的组件网址时)。

此方法遭到拒绝的主要原因是,它增加了维护与解析器的实时连接的复杂性。如果发生了导致解析器服务器重启的情况,则其中任何或所有通道可能需要重新建立,这会增加不必要的复杂性。

上下文参数应对将给定子软件包名称映射到父级软件包的 meta/subpackages 文件中的固定软件包哈希值所需的信息进行编码,以允许 PackageResolverResolver 实现在解析器重启后继续有效(如有必要)。

替代方案:解析上下文值的 FIDL 类型表示法

我们考虑了几种关于如何表示解析上下文的方法,最终决定为 PackageResolverContextResolver 使用通用字节数组。这种方法的一个优点是,字节数组是编码值的常见类型,并且不会排除对任意内容(包括 null 字节)进行编码。

FIDL 团队建议使用字节数组,因为此模式没有其他显式 FIDL 类型(在网络浏览器中可以视为“Cookie 模式”)。

最终,我们总结了此类型的限制条件,如下所示:

  • 这些类型应可编组,而不会在 fuchsia.pkg 和 fuchsia.component.resolve 之间引入 API 依赖项。
  • 在组件管理器重启后,上下文值无需保留,但子软件包实现不应要求将现有“非关键”(即可重启)软件包提供组件转换为“关键”。(此限制条件似乎不建议使用服务句柄。)请注意,如果组件管理器重启,所有组件(当前)都会使用新上下文值重新解析并重新启动。上下文值在组件管理器重启后不需要保留,更不用说重启或重启。
  • 上下文在概念上可以很小(与软件包服务器名称和软件包哈希值大致相当)。这意味着对于每个子软件包,VMO 的句柄可能非常昂贵。

替代方案:组件管理器使用 resolution_context 获取解析器

在解析相对的子封装组件网址时,我们考虑将 URI 架构(例如 fuchsia-pkg)添加为 Component::resolution_context 的一部分,以允许组件管理器通过架构查找,通过解析器注册表获取 Resolver。被拒绝的原因是,这样做在理论上会允许 Resolver 返回具有不同架构的上下文,用于解析相关的子打包组件,这被认为存在架构风险。而是会跟踪用于解析组件的 Resolver。组件管理器将始终使用通过相对网址请求其他组件的组件的 Resolver

替代方案:使用相对组件,而不是相对软件包

在 Fuchsia 中,软件包是软件分发的单元和软件的安装单元(可执行代码和其他文件)。虽然 Fuchsia 组件提供了一些较为熟悉的用例,但可能不涉及组件或组件间依赖关系的软件包之间可能存在跨软件包依赖项(例如,Fuchsia shell 软件包可能依赖于另一个没有自己的组件的资源软件包)。此外,在 SDK 中独立分发预构建组件也不是方便的方式。

替代方案:使用特殊的 fuchsia-subpkg:// 架构引用子软件包

此 RFC 建议将 URI 相对路径(以路径段开头并省略 scheme 和授权方前缀的 URI)解释为对子软件包中的资源的引用。当前方案还规定,子软件包只能引用直接“子”软件包,因此路径中不得包含斜杠。(在子软件包引用中使用斜杠是保留的,以备将来使用。)

另一种方案是,在引用子软件包时需要特殊的 scheme 前缀(例如 fuchsia-subpkg://),以确保指定的字符串明确用于子软件包解析。

此外,使用架构前缀 fuchsia-subpkg 似乎意味着依赖于处理 fuchsia-pkg 架构的同一解析器,这可能会令人感到困惑。

URI 标准建议仅从相对路径开始。要求使用特殊的 scheme 前缀可能意味着依赖于特定的 scheme 处理程序,从而限制了一般性。无架构的相对路径实现广泛且易于理解(例如,在 HTML 中,<a href="sub-path/page"> 作为相对位置引用隐式处理,无需特殊架构)。

现有艺术和参考资料

标准

接受的 Fuchsia RFC

可能相关的 Fuchsia RFC 草稿