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();