RFC-0106: Component manifest includes in the Fuchsia SDK | |
---|---|
Status | Accepted |
Areas |
|
Description | Create a component manifest include shards sysroot in the SDK and distribute it to out-of-tree integrators. |
Issues | |
Gerrit change | |
Authors | |
Reviewers | |
Date submitted (year-month-day) | 2021-05-12 |
Date reviewed (year-month-day) | 2021-06-23 |
Summary
This document proposes adding component manifest shards to the Integrator Development Kit (IDK). This will create a standard affordance for publishing component manifest shards to out-of-tree (OOT) developers and establish a common pattern for making manifest shards portable between different developer environments.
Readers are expected to be familiar with the following:
- Component manifests
- Particularly, the
include
syntax in component manifests.
Motivation
Aligning workflows across environments
Since their introduction, component manifest shards and the include
syntax in
cml
and cmx
have seen wide adoption. They are used for a
variety of tasks such as reducing boilerplate,
encapsulating implementation details,
simplifying developer workflows, and in
core realm variations.
Currently the mechanism for processing manifest includes relies on the source layout of the Fuchsia tree. This works fine for manifests that live in the Fuchsia tree, but doesn't port well to OOT development. Consider for instance this guide for logging in C/C++ components. The guide opens by instructing developers to add the following to their component manifest:
{
include: [ "sdk/lib/diagnostics/syslog/client.shard.cml" ],
}
However, the guide notes that the above only works in-tree (IT). Since OOT developers are also in the target audience for this guide, the guide instructs these developers to instead add the following to their component manifest, effectively inlining the contents of the shard:
{
use: [
{ protocol: "fuchsia.logger.LogSink" },
],
}
The same issue is present elsewhere. For instance the Test Runner Framework inventory of test runners (for supporting various types of tests on Fuchsia, for instance C/C++ GoogleTest, Rust libtest, gotest on Go etc') has instructions that only work IT. This hinders the proliferation of good testing practices to OOT developers.
This compromise defeats the purpose of including a manifest shard in the first place, as clients are exposed to an implementation detail of Fuchsia logging. Furthermore it makes it more difficult to orchestrate large scale changes in the future, such as a planned future soft transition to a different protocol for publishing logs from components. Finally it adds friction between IT and OOT developer workflows.
Obscuring implementation details
If we revisit the LogSink
shard example, we encourage clients of the syslog
library to include this shard as it brings into their component's sandbox the
capabilities needed to make that client library work.
Currently that set of capabilities is very simple, just a single protocol, but it's expected to change in the future. For instance instead of the current protocol, where clients connect a socket to syslog and then buffer characters into that socket to log them, we may introduce a system for writing structured data in a specialized format into VMOs and rotating them between the client and the server, not unlike how tracing works today.
Obscuring these details behind the shard reduces the cognitive load on the component author, who just wants to write to syslog (a common utility from their perspective) and doesn't care about those details. But this also allows the syslog maintainer to evolve those details over the time, without requiring the attention of the component owner.
This also works in the opposite direction (exposing capabilities from a component rather than consuming them) and with different types of capabilities (for instance directory capabilities rather than protocol capabilities). Consider for instance this Inspect discovery and hosting guide. The guide explains that developers are required to add the following to their component manifest:
{
capabilities: [
{
directory: "diagnostics",
rights: [ "connect" ],
path: "/diagnostics",
},
],
expose: [
{
directory: "diagnostics",
from: "self",
to: "framework",
},
],
}
Alternatively developers could simply include a shard. This saves the developer from 14 lines of boilerplate, the obscurity of having to deal with concepts such as exposing a directory capability to the framework (which the majority of component developers don't need to be familiar with), and the trouble of having to change this mechanism if needed in the future. Lastly, if shards could be portable between IT and OOT component manifests then the inspect guide would be considerably shorter as it wouldn't need to specify a separate set of instructions for IT and OOT developers.
Simplifying the consumption of system capabilities
Another example that's more sophisticated is the various capabilities that may
be needed when working with the web engine, directly as a FIDL client rather
than via a client library as in the previous example. The web runtime is
powerful and robust, with today's web apps being capable of performing many
privileged device operations given the required capabilities.
fuchsia.web
defines the different capabilities that the web
engine implementation would need to be able to host certain web apps.
In order to simplify consuming those same capabilities from the environment and then providing them to the web engine, we have shards that slice up these capabilities by common categories. For instance this shard defines a baseline set of "table stakes" capabilities that the web engine requires:
{
"sandbox": {
"services": [
"fuchsia.device.NameProvider",
"fuchsia.fonts.Provider",
"fuchsia.intl.PropertyProvider",
"fuchsia.logger.LogSink",
"fuchsia.memorypressure.Provider",
"fuchsia.process.Launcher",
"fuchsia.sysmem.Allocator",
]
}
}
This shard defines additional capabilities that are needed if the web app is not local and/or requires the network:
{
"sandbox": {
"services": [
"fuchsia.net.name.Lookup",
"fuchsia.net.interfaces.State",
"fuchsia.posix.socket.Provider"
]
}
}
Whereas this shard is required in order to render the web app in a Scenic View:
{
"sandbox": {
"services": [
"fuchsia.accessibility.semantics.SemanticsManager",
"fuchsia.ui.input.ImeService",
"fuchsia.ui.input.ImeVisibilityService",
"fuchsia.ui.scenic.Scenic"
]
}
}
Additional shards exist for instance to unlock hardware acceleration for graphics or for hardware media codecs.
This is a non-trivial amount of information. Keeping it in its own shard is tidier than adding it to a pile of other services present in a component's sandbox. In addition, this once again makes evolution easier.
Note that this is a hypothetical example. These shards exist IT today. This is an exploration of the possibility of moving these shards to the IDK to make them available OOT, not a promise to do so. Also, the content of the shards above is still in flux, and is used here as an illustrative example but not as reference documentation.
Implementation
Packaging manifest shards in the IDK distribution
C/C++ development targeting Fuchsia is made possible by setting up the include
path (as specified to the compiler via the --include-directory
or -I
flag) to one or more directories that contain a particularly nested
hierarchy of subdirectories and header files. This makes code that includes
Fuchsia-specific headers portable between IT and OOT builds.
For instance the following line of C code is valid both IT and OOT:
#include <lib/zx/process.h>
This is because both the IT build and OOT builds targeting Fuchsia add to their
include directories a path that has the file lib/zx/process.h
below it. In a
Fuchsia checkout the corresponding include directory is
//zircon/system/ulib/zx/include/
, whereas in an OOT build this path will need
to be $FUCHSIA_SDK/pkg/zx/include/
. See also: IDK layout.
Setting up directories in an include path to make include directives portable is also known as setting up a "sysroot".
We will deploy component manifest shards similarly, below $FUCHSIA_SDK/pkg/
in
sub-paths that are conceptually associated with the purpose of the shards, and
set up the --includepath
flag in cmc
accordingly in IT and OOT
builds.
For instance, the syslog shard used as an example above might be:
- Included as follows:
include: [ "syslog/client.shard.cml" ]
. - Found IT under
//sdk/lib/syslog/client.shard.cml
, hence we would configurecmc
IT with--includepath $FUCHSIA_CHECKOUT/sdk/lib/
. - Found OOT under
$FUCHSIA_SDK/pkg/syslog/client.shard.cml
, hence we would configurecmc
OOT with--includepath $FUCHSIA_SDK_ROOT/pkg/
.
Portable shards vs local shards
It's expected that some shards are to be made available to OOT developers, while others are only to be used IT. Above we reviewed some examples for shards that can be used OOT. An example for a shard that is only useful IT is this shard which is used to share a large portion of complex capability routing between two component definitions, one of which is a system component and the other is a test double for that component. There is no use for this specific shard OOT.
Therefore some shards should be made portable and published in the IDK while others should remain private to a particular repository.
We propose codifying this distinction using a common notation for relative and
absolute paths. Paths used in manifest include
directives that should resolve
to portable shards should have no leading //
, for instance
syslog/client.shard.cml
. These will be resolved against the sysroot of shards.
On the other hand, paths to shards that are purely local to a certain repository
should begin with //
and resolve against the source root (or checkout root) of
their repository.
For instance this shard should be included IT via
the path //src/sys/test_manager/meta/common.shard.cml
.
Build system integration with cmc
Build systems, such as the in-tree GN & Ninja build and
any out-of-tree builds targeting Fuchsia, already integrate with cmc
. Such
integrations will need to be amended to afford for the new include behavior.
Specifically for the following cmc
subcommands:
compile
include
check-includes
The invocations of cmc
will need to specify the following flags:
--includeroot
: path to resolve//
-prefixed include paths against.--includepath
: zero or more paths to resolve other paths against, in the order specified (first match).
Shards as SDK atoms
Shards will be included in the IDK by the build system, similar to how other IDK
elements are treated. We will reuse the existing sdk_atom()
template,
specifying the id
parameter according to how we'd like the
IDK layout.
Process for adding shards to the IDK
Shards can specify contractual expectations that may overlap with the platform
surface. For instance the syslog shard references a protocol in the Fuchsia
namespace - fuchsia.logger.LogSink
- that is well known to be offered by a
Fuchsia system component. Therefore shards that are published in the IDK will be
treated as APIs and as SDK atoms, and will undergo API review via
the same process that's used today for instance to add or modify FIDL files that
are published in the IDK. It's also possible to use the sdk_atom()
notion of a
category, for instance first introducing a shard as "internal" (not to be
distributed OOT) and then elevating it to a higher-exposure category via the
existing process.
Future work
Port expect_includes()
Fuchsia's GN build offers a template for expecting that dependent components include a certain shard in their manifest (see this guide). One of the benefits that this offers is that it directs developers that added a dependency on, for instance, a client library for a particular service, to also include a manifest shard in their component that ensures that their library has access to capabilities that it requires at runtime to operate correctly.
IT developers have given very positive feedback for this, and some OOT developers expressed interest. This template or the concept of it can be ported to the GN SDK using GN metadata, as well as to the Bazel/Blaze SDK using Bazel Aspects or Bazel Providers.
See also: https://fxbug.dev/42156975
Performance
This proposal has no impact on runtime performance since all work is done at build time.
Processing includes at build time adds some extra work. However this is expected
to have no impact on clean build wall time. We know from previous research that
work that the build does that's not on the critical path generally does not
contribute to build latency, and also that work that's derived directly from
sources (such as cmc
invocations, fidlc
invocations, etc') has virtually
zero presence on the critical path since this work parallelizes well and can be
scheduled very flexibly.
As an example, consider this change where a common operation in
cmc
was made about 300ms faster, but despite having thousands of invocations
of cmc
in the build we measured no impact on build wall time.
Ergonomics
Attention has been given to keeping the manifest includes mechanism simple and transparent. For instance it is possible to produce a post-processed component manifest, with include directives replaced with the contents of included files (transitively if needed), with a simple command.
fx cmc include manifest --includepath $(fx get-src-dir)
This is similar in principle to running the C preprocessor on C/C++ source code
(see: man cpp
).
In addition, cmc
generates simple errors that are easy to troubleshoot,
including for unusual error cases such as include cycles. All supported errors
are unit tested.
The cmc
tool itself is already bundled as a prebuilt in the Fuchsia IDK. It is
fully hermetic and runs without any external dependencies. It can be invoked
from the command line, no build system integration is required.
Backwards compatibility
Changing shards in the IDK will affect newly-built OOT components once they pick up the latest IDK release, but won't affect old prebuilts. Care must be taken to avoid breaking changes and use standard practices for platform changes: soft transitions over multiple releases, support windows, and communicating with stakeholders.
As with all matters of platform evolution, changes should be tested thoroughly, and breaking changes should be managed in some way that affords for breakage, for instance using some versioning mechanism. Note that questions of platform versioning, and subsets of this problem such as FIDL versioning, remain open at this time and are outside the scope of this RFC.
Security considerations
Component manifests define capability routing and sandboxes, which have direct implications on security. However the goal of manifest includes is not to ultimately produce a different manifest, but rather to produce the same manifest in more ergonomic ways. As long as the same final manifest is produced after includes are processed, there are no implications on security.
To help developers understand what their manifests would look like after
includes are processed they can use the cmc include
demonstrated above.
Testing
The existing functionality in cmc
is already covered by unit and integration
tests. Test coverage for cmc
is >90%, and specifically coverage for the
include subcommand is complete. Any outstanding changes needed to the Fuchsia
IDK or to OOT integrations will be tested as instructed and expected by the SDK
team.
Component manifests that are in the IDK should be treated as any other IDK artifacts in terms of CTS test coverage.
Documentation
The include
syntax is already documented.
The cmc include
command is self-documented via cmc help include
.
Existing documentation that currently instructs OOT developers to use a replacement for manifest includes will be updated to no longer make this distinction between IT and OOT development once it's no longer necessary.
Drawbacks, alternatives, and unknowns
Do nothing
We can maintain the status quo, at the cost of not solving the problems stated in the motivation section above.
Organize shards in the IDK as top-level artifacts
As an alternative to publishing component manifest shards in various possible
locations in the IDK, such as under $FUCHSIA_SDK_ROOT/pkg
or
$FUCHSIA_SDK_ROOT/fidl
(based on the logical association of the shard), we
also explored the alternative of establishing a single base directory for
component manifest shards. For instance:
$FUCHSIA_SDK_ROOT/component_manifests/
. This is similar to how all .fidl
files are organized under $FUCHSIA_SDK_ROOT/fidl/
, that is to say they are
aggregated by type (being FIDL files) rather than by another logical association
(for instance pkg/async/
, pkg/memfs/
, pkg/zx/
).
This alternative was rejected based on feedback from SDK customers, stating that they prefer the logical association of the SDK's contents over having different types of files be spread across different base directories. The accepted design allows for instance to put the component manifest shard for using syslog next to C++ headers for writing to syslog from components written in C++.
Prior art and references
Component manifest includes are inspired by C/C++ includes.
The cmc include
command is inspired by the cpp
command, which runs the C
preprocessor on a given file and prints the postprocessed result. See:
man cpp
.