blob: 21eabac8883bcbcbf619435f9a2f13cd47e64737 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Utilities for working with structured configuration during the product assembly process.
use assembly_validate_util::PkgNamespace;
use camino::Utf8PathBuf;
use cm_rust::{FidlIntoNative, NativeIntoFidl};
use fidl::{unpersist, Persistable};
use fuchsia_pkg::{PackageBuilder, PackageManifest, RelativeTo};
use std::{collections::BTreeMap, fmt::Debug};
pub struct Repackager {
builder: PackageBuilder,
outdir: Utf8PathBuf,
}
impl Repackager {
/// Read an existing package manifest for modification. The new manifest will be written to
/// `outdir` along with any needed temporary files.
pub fn new(
original_manifest: PackageManifest,
outdir: impl Into<Utf8PathBuf>,
) -> Result<Self, RepackageError> {
let outdir = outdir.into();
let builder = PackageBuilder::from_manifest(original_manifest, &outdir)
.map_err(RepackageError::CreatePackageBuilder)?;
Ok(Self { builder, outdir })
}
/// Apply structured configuration values to this package, failing if the package already has
/// a configuration value file for the component.
pub fn set_component_config(
&mut self,
manifest_path: &str,
values: BTreeMap<String, serde_json::Value>,
) -> Result<(), RepackageError> {
let manifest_bytes = self
.builder
.read_contents_from_far(manifest_path)
.map_err(RepackageError::ReadManifest)?;
let (config_bytes, path) = config_for_manifest(manifest_bytes, values)?;
let path = path.expect("Tried to set config for non CVF config path");
self.builder
.add_contents_to_far(path, config_bytes, self.outdir.as_std_path())
.map_err(RepackageError::WriteValueFile)?;
Ok(())
}
/// Build the modified package, returning a path to its new manifest.
pub fn build(self) -> Result<Utf8PathBuf, RepackageError> {
let Self { mut builder, outdir } = self;
let manifest_path = outdir.join("package_manifest.json");
builder.manifest_path(&manifest_path);
builder.manifest_blobs_relative_to(RelativeTo::File);
builder.build(&outdir, outdir.join("meta.far")).map_err(RepackageError::BuildPackage)?;
Ok(manifest_path)
}
}
/// Parse the component manifest and return the bytes for the new structured config value file.
fn config_for_manifest(
manifest_bytes: Vec<u8>,
values: BTreeMap<String, serde_json::Value>,
) -> Result<(Vec<u8>, Option<String>), RepackageError> {
let manifest: cm_rust::ComponentDecl =
read_and_validate_fidl(&manifest_bytes, cm_fidl_validator::validate)
.map_err(RepackageError::ParseManifest)?;
let Some(config_decl) = manifest.config else {
return Err(RepackageError::MissingConfigDecl);
};
// create a value file
let config_values =
config_value_file::populate_value_file(&config_decl, values)?.native_into_fidl();
let config_bytes = fidl::persist(&config_values).map_err(RepackageError::EncodeConfig)?;
let path = match &config_decl.value_source {
cm_rust::ConfigValueSource::PackagePath(path) => Some(path.to_string()),
cm_rust::ConfigValueSource::Capabilities(_) => None,
};
Ok((config_bytes, path))
}
#[derive(Debug, thiserror::Error)]
pub enum RepackageError {
#[error("Couldn't read package manifest for modification.")]
CreatePackageBuilder(#[source] anyhow::Error),
#[error("Couldn't read component manifest.")]
ReadManifest(#[source] anyhow::Error),
#[error("Couldn't parse manifest.")]
ParseManifest(#[source] PersistentFidlError),
#[error("No config decl found in manifest.")]
MissingConfigDecl,
#[error("Couldn't compile a configuration value file.")]
ValueFileCreation(
#[source]
#[from]
config_value_file::FileError,
),
#[error("Couldn't encode config values as persistent FIDL.")]
EncodeConfig(#[source] fidl::Error),
#[error("Couldn't write the config value file to the modified package.")]
WriteValueFile(#[source] anyhow::Error),
#[error("Couldn't build the modified package.")]
BuildPackage(#[source] anyhow::Error),
}
/// The list of runners currently supported by structured config.
static SUPPORTED_RUNNERS: &[&str] = &[
"driver",
"elf",
"elf_test_runner",
"elf_test_create_raw_processes_ambient_exec_runner",
"gtest_runner",
];
/// Validate a component manifest given access to the contents of its `/pkg` directory.
///
/// Ensures that if a component has a `config` stanza it can be paired with the specified
/// value file and that together they can produce a valid configuration for the component.
///
/// Also ensures that the component is using a runner which supports structured config.
pub fn validate_component(
manifest_path: &str,
reader: &mut impl PkgNamespace,
) -> Result<(), ValidationError> {
// get the manifest and validate it
let manifest_bytes =
reader.read_file(manifest_path).map_err(ValidationError::ManifestMissing)?;
let manifest: cm_rust::ComponentDecl =
read_and_validate_fidl(&manifest_bytes, cm_fidl_validator::validate)
.map_err(ValidationError::ParseManifest)?;
let Some(config_decl) = manifest.config else {
return Ok(());
};
// make sure the component has a runner that will deliver config before finding values
let runner = manifest
.program
.as_ref()
.ok_or(ValidationError::ProgramMissing)?
.runner
.as_ref()
.ok_or(ValidationError::RunnerMissing)?
.as_str();
if !SUPPORTED_RUNNERS.contains(&runner) {
return Err(ValidationError::UnsupportedRunner(runner.to_owned()));
}
let path = match &config_decl.value_source {
cm_rust::ConfigValueSource::PackagePath(path) => path,
cm_rust::ConfigValueSource::Capabilities(_) => return Ok(()),
};
let config_bytes = reader.read_file(&path).map_err(ValidationError::ConfigValuesMissing)?;
// read and validate the config values
let config_values: cm_rust::ConfigValuesData =
read_and_validate_fidl(&config_bytes, cm_fidl_validator::validate_values_data)
.map_err(ValidationError::ParseConfig)?;
// we have config, make sure it's compatible with the manifest which references it
config_encoder::ConfigFields::resolve(&config_decl, config_values, None)
.map_err(ValidationError::ResolveConfig)?;
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("Couldn't read manifest.")]
ManifestMissing(#[source] assembly_validate_util::ReadError),
#[error("Couldn't parse manifest.")]
ParseManifest(#[source] PersistentFidlError),
#[error("Couldn't find component's config values in package.")]
ConfigValuesMissing(#[source] assembly_validate_util::ReadError),
#[error("Couldn't parse config values.")]
ParseConfig(#[source] PersistentFidlError),
#[error("Couldn't resolve config.")]
ResolveConfig(#[source] config_encoder::ResolutionError),
#[error("Component manifest does not specify `program`.")]
ProgramMissing,
#[error("Component manifest does not specify `program.runner`.")]
RunnerMissing,
#[error("{:?} is not a supported runner. (allowed: {:?})", _0, SUPPORTED_RUNNERS)]
UnsupportedRunner(String),
}
/// Parse bytes as a FIDL type, passing it to a `cm_fidl_validator` function before converting
/// it to the desired `cm_rust` type.
fn read_and_validate_fidl<Raw, Output, VF>(
bytes: &[u8],
validate: VF,
) -> Result<Output, PersistentFidlError>
where
Raw: FidlIntoNative<Output> + Persistable,
VF: Fn(&Raw) -> Result<(), cm_fidl_validator::error::ErrorList>,
{
let raw: Raw = unpersist(&bytes).map_err(PersistentFidlError::Decode)?;
validate(&raw).map_err(PersistentFidlError::Validate)?;
Ok(raw.fidl_into_native())
}
#[derive(Debug, thiserror::Error)]
pub enum PersistentFidlError {
#[error("Couldn't decode bytes.")]
Decode(#[source] fidl::Error),
#[error("Couldn't validate raw FIDL type.")]
Validate(#[source] cm_fidl_validator::error::ErrorList),
}