RFC-0154:子软件包

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

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

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

摘要

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

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

设计初衷

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

请注意,虽然本 RFC 中讨论的最初动机用例都与测试相关,但子软件包在其他场景中也可能很有用。子软件包将首先可供测试,因为我们可以采用在生产环境中不可接受的简化方式来进行测试。除了时间安排注意事项之外,我们的目标是在测试和生产场景中全面支持子软件包。

为何此时推荐?

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

我们通过将所有组件依赖项包含在单个软件包中,以便在树中密封打包测试。不过,对于外部树 (OOT),现状是依赖于绝对软件包网址。这些测试未密封打包,这会导致平台软件包网址成为隐式 ABI。

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

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

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

此版本的测试更加密封,无论运行该测试的设备上安装了哪个版本的 Scenic,其行为始终都是相同的。

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

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

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

利益相关方

主持人:hjfreyer@google.com

审核者

名称 聚焦区域
wittrock@google.com SWD
jsankey@google.com SWD
geb@google.com 组件框架
shayba@google.com 构建
etryzelaar@google.com 工具
aaronwood@google.com 组件/包装盒移动
cgonyeo@google.com RealmBuilder
ampearce@google.com 安全

咨询对象:computerdruid@google.com

社交

在该 RFC 发布之前,SWD 团队参与了该 RFC 的初始草稿。组件框架团队会及时了解该工作进展,并在每周会议期间就范围和功能提供反馈,RFC 的作者中也有 CF 团队成员。

设计

受子软件包影响的用例

  • 嵌套软件包依赖项的软件包内声明
    • 软件包可以包含列出其子软件包的元数据文件,并且构建系统必须能够根据软件包目标的依赖项填充此元数据文件。
  • 相对组件分辨率
    • 例如,开发者可以将对组件 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 文件中生成相应的条目。

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

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

  deps = [
    other_deps,
    ...
  ]
}

fuchsia_package("B") {
  ...
}

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

为了便于使用现有工具构建包含子软件包的软件包,必须在 Fuchsia 树内构建系统中更新构建规则和脚本,并且应在 Fuchsia GN SDK 和下游构建环境中更新这些规则和脚本。(实现部分介绍了受影响的 build 环境和已知的必要更改。)

解析子软件包

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

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 哈希索引文件包存储区中无状态地解析子软件包。)

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

对于绝对 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(路径或 fragment)解析组件网址时,组件管理器必须将解析委托给用于解析父级组件的同一 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.Directoryresolved_context 值。

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

  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() 会构建并返回已解析的 Component,以及新组件的 resolution_context,后者会由组件管理器缓存在该组件的状态中。

后续工作

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

实现

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

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

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

以下文件格式可能需要更改格式(以添加子软件包引用)或为同一阶段提供补充文件(例如 contentssubpackages 的搭配方式):

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

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

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

  • package.gni 使用 --manifest-path 调用 prepare_package_inputs.py,以生成 ${package_name}.manifest(在脚本中称为 archive_manifest,在 pm CLI 文档中称为“build manifest”)
  • 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 协议的实现,以实现上文设计部分中所述的行为。

提前加载软件包

软件包解析器必须在内部递归解析所有子软件包,直到提取所有子软件包,然后才能返回根软件包的解析结果。之所以选择这种方法,是为了简化原子性属性的实现,这样一来,在组件启动之前,所有必需的子软件包都一定会解析完毕。

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

组件解析器

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

RealmBuilder

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

性能

遍历子软件包可能会增加识别要下载的所有 blob 的延迟时间,因为需要遵循多级间接,但这在实践中不太可能有显著影响。例如,将加载包含 N 个 blob 的单个软件包与加载包含子软件包且子软件包总共有 N 个唯一 blob 的软件包进行比较:

  • 父级
    • N 个 blob

  • 父级
    • N_0 blob
    • Child1
      • N_1 blob
      • Child2
      • N_2 blob

在第一种情况下,所有 N 个 Blob 都是事先已知的,并且可以并行加载。在第二种情况下,软件包解析器必须按序跟踪父级和子级之间的链接,以累积要加载的完整 N 个 blob。此遍历会产生额外的延迟时间,延迟时间受树的深度所限。如果递归 blob 加载的延迟时间成为问题,您可以使用多种实现选项来缩短延迟时间。例如,可以并行遍历子软件包链接和加载 blob,或者在构建软件包时包含完整的子软件包内容地址集。

向后兼容性

相对于资源组件网址的解析行为

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

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

解析组件后,返回的 fuchsia.component.resolution.Component 类型包含一个类型为 fuchsia.component.resolution.Packagepackage 字段,其中包含对包含返回的 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 既不批准也不禁止此类表示法,但使用斜线分隔符会暴露潜在的陷阱。

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

值得注意的是,/0 已废弃,如果从常规用途中移除,则可以为子软件包释放 / 的含义(不过目前尚未提出此建议,并且必须遵循未来的 RFC)。

如果软件包命名层次结构存在竞争用例,则可以使用其他分隔符(例如 :)来嵌套子软件包,或者子软件包可以使用 /,而其他分隔符可以用于其他用途。

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

安全注意事项

可审核性和可执行性

在设计提案中,我们考虑了 Fuchsia 安全主管迄今为止提出的关于需要尽量减少对系统安全状况的更改,以及简化对可能影响可审核性和可执行性的更改的审核的意见。

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

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

由于其敏感功能,目前在特许权限许可名单中标识的软件包或组件可以作为子软件包包含,但需要一些额外的约束条件才能保留其所需的特许权限。除了测试之外,父级软件包不得为需要比父级更高权限的子级组件包含子软件包。在密封测试中运行的组件可能需要对此规则进行例外处理,但如果已列入许可名单的软件包或组件在密封测试中用作子软件包,则应尽可能将特权功能替换为模拟对象或等效替换项。

如果此限制会阻碍其他系统改进,则应咨询安全团队,了解其他替代方案。

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

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

子软件包仅提供指定的 blobfs 软件包子集(目前为深度一级),但声明对其他软件包的依赖项的软件包不再拥有读取该软件包内容的特权。此 RFC 建议对非特权客户端从子软件包请求功能或资源时可以执行的操作保持相同的限制。

从特定软件包运行的组件可能无法查看其子软件包的内容,但如果具有 PackageResolver capability,则可以查看 meta/subpackages 文件并手动解析其内容。

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

实现风险:队头阻塞

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

由于 RFC 规定它只会实现提前解析,因此实现者可以通过确保在释放解析器资源以解析子软件包之前不需要完全解析父级来降低此风险。

隐私注意事项

预计不会对隐私保护状况产生影响。

测试

我们将添加单元测试来验证其他功能。现有测试有助于识别意外的回归问题。

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

我们将编写完整性集成测试,以验证子软件包中的组件解析。

文档

以下已知文档需要更新:

  • 介绍 Fuchsia 软件包网址和组件网址(用于记录如何引用子软件包)的现有文档、软件交付文档和组件框架概念文档。目前显示 CML 示例(包含 fuchsia-pkg:// 网址引用)的示例可以通过使用子软件包的示例进行补充。
  • 应更新关于相对组件引用的文档,以说明与分包组件相比的相似性和差异。

缺点、替代方案和未知情况

请参阅未来工作子部分(在“设计”部分中),其中介绍了一些未来的增强功能,以及在缩小到此 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 团队建议使用字节数组,因为此模式(在网络浏览器术语中可称为“Cookie 模式”)没有任何其他显式 FIDL 类型。

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

  • 这些类型应可序列化,且不会在 fuchsia.pkg 和 fuchsia.component.resolution 之间引入 API 依赖项。
  • 上下文值无需在组件管理器重启后保留,但子软件包实现不应要求将现有的“非关键”(即可重启)软件包服务组件转换为“关键”。(此限制似乎排除了使用服务句柄的可能性。) 请注意,如果组件管理器重启,所有组件都将(目前)重新解析并重新启动,并使用新的上下文值。上下文值无需在组件管理器重启后保持不变,更不用说在重新启动或电源周期后保持不变。
  • 从理论上讲,上下文可以很小(大约与软件包服务器名称和软件包哈希的大小相当)。这意味着,为每个子软件包创建 VMO 句柄的开销可能非常高昂。

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

在解析相对子软件包组件网址时,我们考虑将 URI 架构(例如 fuchsia-pkg)作为 Component::resolution_context 的一部分,以允许组件管理器通过架构查找来通过解析器注册表获取 Resolver。此提案被拒绝,因为从理论上讲,这样做会允许 Resolver 返回一个采用其他方案来解析相对子软件包组件的情境,这被视为架构风险。相反,组件管理器会跟踪用于解析组件的 Resolver。组件管理器始终会使用通过相对网址请求其他组件的组件的 Resolver

替代方案:使用相对组件而非相对软件包

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

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

本 RFC 建议将 URI 相对路径(以路径片段开头且省略架构和授权前缀的 URI)解读为对子软件包中的资源的引用。当前提案还将子软件包引用限制为仅限直接“子”软件包,因此路径不得包含斜线。(在子软件包引用中使用斜线是预留的,以供日后可能的使用。)

我们还考虑了另一种替代方案,即在引用子软件包时要求使用特殊的架构前缀(例如 fuchsia-subpkg://),以确保给定字符串明确用于子软件包解析。

此外,使用架构前缀 fuchsia-subpkg 似乎暗示了对处理 fuchsia-pkg 架构的同一解析器的依赖,这可能会造成混淆。

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

在先技术和参考文档

标准

已接受的 Fuchsia RFC

可能相关的 Fuchsia RFC 草稿