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. 在 build 时获取包含特定版本 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

共同化

在发布之前,SWD 团队参与了此 RFC 的初稿。Component Framework 团队始终了解这项工作的进展情况,并在每周的会议上就范围和功能提供反馈,其中 CF 团队的成员也是 RFC 的作者。

设计

受子软件包影响的用例

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

子软件包表示法

软件包通过子软件包的软件包级范围名称声明对子软件包的引用,该名称映射到同一软件包商店和同一软件包集中定义的软件包的 package hash。(例如,如果父软件包是从“universe”软件包集中解析的,则其子软件包也必须是从“universe”软件包集中解析的)。RFC 的其余部分描述了这些声明的假想表示形式,即一个名为 meta/subpackages 的文件,其格式与 meta/contents 目前使用的格式 (=) 相匹配。

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

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

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

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

构建包含子软件包的软件包

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

各种 Fuchsia SDK build 系统(目前为 GN 和 Bazel)通常支持创建“软件包”目标,该目标可以依赖于其他目标以包含在软件包中。在这些 build 系统中,添加对软件包的依赖项会告知 build 系统也生成依赖软件包,但目前不会在生成的软件包中对该依赖项进行编码。例如,以下 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. Component Manager 获取 fuchsia-pkg 方案的已注册 Resolver 功能,并调用 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() 构建并返回已解析的 Component(包含 resolution_context),组件管理器会将其缓存在相应组件的状态中。

  2. 如需从子软件包 child-package 加载子组件 B(相对于上面的组件 A),请执行以下操作:

    a. 组件管理器获取用于解析 component-aResolver 功能,并调用 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,组件管理器会将该 resolution_context 缓存到相应组件的状态中。

未来的工作

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

实现

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

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

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

FIDL 方法变更

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

软件包表示法的变化

向 Fuchsia 添加子软件包不会改变不含子软件包的软件包的表示方式。现有软件包网址和组件网址不受影响。开发者可以选择使用对子软件包的引用(使用 child-package#...,将 child-package 映射到 top-level-package 的软件包哈希)替换完全限定的组件网址(例如,使用 fuchsia-pkg://fuchsia.com/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 安全主管迄今为止提出的意见,即需要尽可能减少对系统安全状况的更改,并简化对可能影响可审核性和可执行性的更改的审核。

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

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

目前在特许许可名单中识别出的软件包或组件(由于其敏感功能)可以作为子软件包包含在内,但需要一些额外的限制才能保留其所需的特权。在测试之外,父软件包不得包含子组件的子软件包,该子组件所需的权限高于父组件。在封闭式测试中运行的组件可能需要对此规则进行例外处理,但如果允许列入许可名单的软件包或组件在封闭式测试中用作子软件包,则应尽可能使用模拟对象或等效替代项来替换特权功能。

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

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

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

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

从特定软件包运行的组件可能无法查看其子软件包的内容,但如果它们具有 PackageResolver 功能,则可以查看 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 更改。

此替代方案被拒绝,因为与建议的解决方案相比,实现此方案所需的工具和 build 基础架构会增加更多复杂性,尤其是在工具和 build 工作流方面。例如:

  • 此替代方案假设软件包哈希值未嵌入到开发者 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.resolution 之间引入 API 依赖项。
  • 上下文值不必在组件管理器重启后继续存在,但子软件包实现不应要求将现有的“非关键”(即可重启)软件包服务组件转换为“关键”组件。(此限制似乎排除了使用服务句柄的可能性。) 请注意,如果组件管理器重启,所有组件都会(目前)重新解析并重新启动,并使用新的上下文值。上下文值不需要在组件管理器重启时保持不变,更不用说在重新启动或电源循环时保持不变。
  • 从概念上讲,上下文可以很小(大约相当于软件包服务器名称和软件包哈希的大小)。这意味着,为每个子软件包提供 VMO 句柄的成本可能过高。

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

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

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

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

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

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

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

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

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

在先技术和参考资料

标准

已接受的 Fuchsia RFC

可能相关的 Fuchsia RFC 草稿