Subpackaging components

Packages can "contain" other packages (referred to as their subpackage s), producing a hierarchy of nested packages. Components can leverage subpackaging to organize a hierarchy of nested components, where each component is encapsulated in its own package, and brings its own set of dependencies.

Subpackages enable:

  • Encapsulated dependencies (a packaged component declares its direct dependencies only)
  • Isolated /pkg directories (grouped components don't need to merge their files, libraries, and metadata into a single shared namespace)
  • Assured dependency resolution (system and build tools ensure subpackages always "travel with" their packages)

Relationship to Fuchsia Components

Fuchsia uses packages to "distribute" its software (for example, to load the software onto a device). A single component with no included dependencies is typically contained in a single package. A component that launches other components can define a package that includes specific versions (determined at build time) of its child components using subpackaging.

When organized this way, the subpackage hierarchy mirrors the component parent-child relationships. Child components will then be loaded from a declared subpackage of the parent component's package. This encapsulates ABI dependencies at package boundaries.

Components can also use a subpackage to declare a dependency on a non-executable component (one without a program declaration) and gain access to the /pkg data therein using directory capabilities. By exposing package data as standard directory capabilities, the components use capability routing to restrict access to specific package subdirectories, thereby upholding the Principle of Least Privilege.

Package dependencies mirror Component dependencies

A Fuchsia system is defined by a hierarchy of components. Starting with the first component (the root of the hierarchy), components add capabilities to the system by launching children (child components) that serve those capabilities. Each component has the opportunity to launch its own subtree of components.

To instantiate a child component, the parent identifies the child's source (implementation software) by its location in the package system using a "component URL" (a package URL combined with the intra-package resource location of the component manifest); for example fuchsia-pkg://fuchsia.com/package#meta/component.cm.

Importantly, under the Component Framework, the only place a component refers to a runtime dependency by component URL is when declaring children. Component URLs are not used to define a dependency on a peer component or any other component outside of its local subtree.

When a child component is defined by an absolute component URL like fuchsia-pkg://fuchsia.com/package#meta/component.cm, the component developer cedes control over the implementation of that dependency, to be determined (potentially) at product assembly time, or at runtime from an ephemeral source (a package server).

Subpackaging allows the developer to instead declare package dependencies with build-time resolution, "baking in" the expected component implementations, including known ABI and behavior, without compromising the encapsulation and isolation benefits of package boundaries. This ensures a package with component dependencies has a hermetic implementation, and the behavior of its child components will not change without rebuilding the parent component's package.

Subpackaged component URLs also avoid problems inherent with absolute component URLs: If a parent component is loaded from (for example) an alternate repository like fuchsia-pkg://alt-repo.com/package#meta/parent.cm, its likely that its children may also be in that alt-repo, and there is no way to statically define an absolute component URL that can resolve from either fuchsia.com or alt-repo.com (or another) not known until runtime.

By using relative package paths, a subpackaged child component's implementation is identified by a relative component URL with subpackage name (a subpackage URL, with a URI fragment specifying the path to the component manifest), such as some-child#meta/default.cm. The mapping from subpackage name some-child is declared in a build configuration, and resolved at build time, by storing the subpackage's package hash in the parent component's package metadata, mapped to the subpackage name.

Dependencies are transitive and encapsulated

Component software implementations do not use other components. Components use capabilities. A component's capabilities may come from its parent (routed directly or indirectly by the parent, without the knowledge of the component) or from a child. Importantly, a capability exposed by a child can also be either direct or indirect. The child's implementation is encapsulated, so a capability it exposes may be implemented by that child, or may be routed from one of the child's children.

Subpackaging allows a component to completely encapsulate its implementation, including any dependencies on sub-components.

When a component declares children using absolute component URLs, the specific implementation of that child is selected at runtime. This may be desired, for certain use cases, but the trade-off is that the parent component is not hermetic: It can be hard to re-use the parent component in new environments. Distributing and porting non-hermetic code requires keeping track of all of the external dependencies as well, and then ensuring the dependencies are always available in each new environment.

    children: [
        {
            name: "intl_property_provider",
            url: "fuchsia-pkg://fuchsia.com/intl_property_manager#meta/intl_property_manager.cm",
        },
        ...
    ]

When runtime resolution is not required, the parent component can update its children to use relative path URLs, and declare the child components' packages as subpackage dependencies, resolved at build time. This way, when a component subpackages a child component, the child's package brings all of its subpackaged components inherently, without exposing those dependencies to the other components and runtime environments that may use it.

    children: [
        {
            name: "intl_property_provider",
            url: "intl_property_manager#meta/intl_property_manager.cm",
        },
        ...
    ]

No ambient authority through the /pkg directory

In order to support the basic runtime requirements of a Fuchsia Component, a component may access a directory containing the contents of its package, via the /pkg directory capability.

As described above, subpackaging allows packages to declare their component dependencies as hierarchical, encapsulated packages of components. This model does not require a separate package per component, but it does encourage it, and the Fuchsia runtime and tools are designed to make the process of declaring, building, and running separately-packaged components natural and performant.

Conversely, multiple components combined in a single package share a single, merged /pkg directory. Bundling more than one component in a single package allows each component to access not only the same data, but also the metadata of the other components in that package as well, without explicit capability routing.

In certain cases, where multiple components share access to the same data, this may be convenient. However, in cases where components need access to different sets of data, or one component uses data that should not be exposed to the other, packaging components together may undermine the principle of least privilege, making subpackages a better fit.

The fact that a component might not take advantage of this consequential privilege is more of a concern than a relief because this might not always be the case, and the privilege opens up an unexpected opportunity for one component to exploit the data of another component.

Advantages over using multiple components in a single package

Today, Fuchsia allows a single package to contain multiple components. This feature predates the existence of subpackages, and it provides another way to declare child components by a relative URL; that is, by a URI fragment that identifies the component by resource path to the component manifest. A component URL of the form #meta/some-child.cm informs the Fuchsia component resolver to load the component implementation for some-child from the same package that contained the parent component's manifest.

Built-in access controls to share package resources

The component framework helps to enforce Fuchsia's capability access control policies by requiring components to declare their capability needs explicitly, and by making the parent component responsible for routing any external capabilities (including resources) from known capability sources (from the parent's parent, or from another child).

If one component needs a resource from another component's package, the Component Framework capability routing declarations allow the source component to expose the specific subdirectory such that the target component can access only what is required, and explicitly offered by its parent component.

This supports any use case that might otherwise have been satisfied by relying on access to a shared /pkg directory from a common package, without exposing the entire /pkg directory.

Subpackage-isolated /pkg directories combined with Component Framework capability routing provide Fuchsia architecture-consistent way to control access to and share package resources.

Changes to transitive dependencies to not break encapsulation

When combining component dependencies into a single package, all components share a single, flat namespace, and transitive dependencies must also be included.

For example, if single package SP bundles component A and component B, but B also depends on C by relative URI fragment (#meta/C.cm), package SP must bundle A, B, and C. If B is later modified to replace C with two new components D and E, the definition of package SP must change to bundle A, B, D, and E, and drop C unless (for the sake of argument) either D or E (or both) also depend on C.

Although some build environments allow a component build target to declare transitive component dependencies, this practice amplifies the risks of merging the contents of these components into a single namespace. If a component or any of its dependencies changes, new files could overwrite files from other components in any part of the component subtree in that package, breaking implementations in undefined and potentially catastrophic ways.

Subpackages greatly simplify transitive dependencies by encapsulating them in the definition of each subpackage, so package SP can be replaced with package A (containing component A) having a dependency on only subpackage B (containing component B). Package A requires no other dependencies, and does not change, even if B's dependencies change.

Subpackaged implementations are build-time guarantees

Using relative URI fragment component URLs (like, #meta/some-child.cm), does not actually guarantee ABI or even API compatibility between parent and child components "in the same package" because they could in fact be resolved from different versions of that package.

If the package is resolved ephemerally (from a package server). A new version of the same package can be re-published between the time the parent component was resolved and a later time when the child component is required and loaded. The child implementation might be different from the implementation included in the original version of the package.

This is not a rare or contrived use case: In Component Framework, components are (by default) resolved only when needed. A component that exposes a single service S will not be loaded until and unless some other component actually requires service S. Depending on the business logic of the program, S might be called upon minutes or hours (or more) after the parent component was launched.

Examples

Declaring build dependencies to subpackages

Fuchsia-enabled build frameworks should include a pattern for declaring a Fuchsia package and its contents. If also enabled to support subpackages, a package declaration will list the subpackages it depends on, by direct containment.

For example, in fuchsia.git, the GN templates for declaring Fuchsia packages support two optional lists, subpackages and (less commonly used) renameable_subpackages. One or both can be included. The renameable_ version allows the package to assign a package-specific name to the subpackage, used when referring to the subpackage by package URL or component URL:

fuchsia_test_package("subpackage-examples") {
  test_components = [ ":subpackage-examples-component" ]
  subpackages = [
    "//examples/components/routing/rust/echo_client",
    ":echo_client_with_subpackaged_server",
    "//src/lib/fuchsia-component-test/realm_builder_server:pkg",
  ]
  renameable_subpackages = [
    {
      name = "my-echo-server"
      package = "//examples/components/routing/rust/echo_server"
    },
  ]
}

The subpackages list contains a list of GN fuchsia_package build targets. By default, the subpackage name (the name the containing package will use to refer to the package) is taken from the defined package_name of the subpackage's fuchsia_package target.

Subpackage targets can also be declared using the package variable in the renameable_subpackages list. renameable_targets also include an optional name variable, to override the default name for the subpackage.

Declaring subpackaged children

A subpackage is only visible to its parent package, and the component(s) in that package. Consequently, subpackage names only need to be unique within that parent package. If two subpackage targets have the same name (or for any other reason), the parent is free to assign its own subpackage names (via renameable_subpackages in GN, for instance).

When declaring subpackaged child components in CML, the url should be the relative subpackaged component URL, as shown in the following example:

children: [
    {
        name: "echo_server",
        url: "echo_server#meta/default.cm",
    },
],

Subpackaged child components can also be referenced in runtime declarations, such as when declaring children through Realm Builder APIs. For example:

// Add the server component to the realm, fetched from a subpackage
let echo_server = builder
    .add_child("echo_server", "my-echo-server#meta/default.cm", ChildOptions::new())
    .await
    .unwrap();

// Add the client component to the realm, fetched from a subpackage, using a
// name that is still scoped to the parent package, but the name matches the
// package's top-level name. In `BUILD.gn` the subpackage name defaults to
// the referenced package name, unless an explicit subpackage name is
// declared.
let echo_client = builder
    .add_child("echo_client", "echo_client#meta/default.cm", ChildOptions::new().eager())
    .await
    .unwrap();