| // Copyright 2021 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. |
| |
| use crate::compiled_package::CompiledPackageBuilder; |
| use anyhow::{anyhow, bail, Context, Result}; |
| use assembly_config_data::ConfigDataBuilder; |
| use assembly_config_schema::{ |
| assembly_config::{AssemblyInputBundle, CompiledPackageDefinition, ShellCommands}, |
| board_config::{BoardInputBundle, HardwareInfo}, |
| common::PackagedDriverDetails, |
| developer_overrides::{DeveloperOnlyOptions, DeveloperOverrides}, |
| image_assembly_config::{BoardDriverArguments, KernelConfig}, |
| platform_config::BuildType, |
| product_config::{ProductConfigData, ProductPackageDetails, ProductPackagesConfig}, |
| BoardInformation, DriverDetails, PackageDetails, PackageSet, |
| }; |
| use assembly_domain_config::DomainConfigPackage; |
| use assembly_driver_manifest::{DriverManifestBuilder, DriverPackageType}; |
| use assembly_named_file_map::NamedFileMap; |
| use assembly_package_utils::PackageInternalPathBuf; |
| use assembly_platform_configuration::{ |
| DomainConfig, DomainConfigs, PackageConfigs, PackageConfiguration, |
| }; |
| use assembly_shell_commands::ShellCommandsBuilder; |
| use assembly_structured_config::Repackager; |
| use assembly_tool::ToolProvider; |
| use assembly_util as util; |
| use assembly_util::{ |
| BootfsDestination, BootfsPackageDestination, FileEntry, InsertAllUniqueExt, InsertUniqueExt, |
| NamedMap, PackageDestination, PackageSetDestination, |
| }; |
| use camino::{Utf8Path, Utf8PathBuf}; |
| use fuchsia_pkg::PackageManifest; |
| use itertools::Itertools; |
| use serde::Serialize; |
| use std::collections::{BTreeMap, BTreeSet}; |
| use util::MapEntry; |
| |
| #[derive(Debug, Serialize)] |
| pub struct ImageAssemblyConfigBuilder { |
| /// The RFC-0115 Build Type of the assembled product + platform. |
| build_type: BuildType, |
| |
| /// All of the packages, in all package sets. |
| packages: Packages, |
| |
| /// The base driver packages from the AssemblyInputBundles |
| base_drivers: NamedMap<String, DriverDetails>, |
| |
| /// The boot driver packages from the AssemblyInputBundles |
| boot_drivers: NamedMap<String, DriverDetails>, |
| |
| /// The boot_args from the AssemblyInputBundles |
| boot_args: BTreeSet<String>, |
| |
| /// The bootfs_files from the AssemblyInputBundles |
| bootfs_files: NamedFileMap<BootfsDestination>, |
| |
| /// Modifications that must be made to configuration for packages. |
| package_configs: PackageConfigs, |
| |
| /// Domain config packages to create. |
| domain_configs: DomainConfigs, |
| |
| kernel_path: Option<Utf8PathBuf>, |
| kernel_args: BTreeSet<String>, |
| |
| qemu_kernel: Option<Utf8PathBuf>, |
| shell_commands: ShellCommands, |
| |
| /// The packages for assembly to create specified by AIBs |
| packages_to_compile: BTreeMap<String, CompiledPackageBuilder>, |
| |
| /// Data passed to the board's Board Driver, if provided. |
| board_driver_arguments: Option<BoardDriverArguments>, |
| |
| /// Configuration capabilities to add to a configuration component/package. |
| configuration_capabilities: Option<assembly_config_capabilities::CapabilityNamedMap>, |
| |
| /// Devicetree binary to be added to zbi |
| devicetree: Option<Utf8PathBuf>, |
| |
| /// Developer override options |
| developer_only_options: Option<DeveloperOnlyOptions>, |
| } |
| |
| impl ImageAssemblyConfigBuilder { |
| pub fn new(build_type: BuildType) -> Self { |
| Self { |
| build_type, |
| packages: Packages::default(), |
| base_drivers: NamedMap::new("base_drivers"), |
| boot_drivers: NamedMap::new("boot_drivers"), |
| boot_args: BTreeSet::default(), |
| shell_commands: ShellCommands::default(), |
| bootfs_files: NamedFileMap::new("bootfs files"), |
| package_configs: PackageConfigs::new("package configs"), |
| domain_configs: DomainConfigs::new("domain configs"), |
| kernel_path: None, |
| kernel_args: BTreeSet::default(), |
| qemu_kernel: None, |
| packages_to_compile: BTreeMap::default(), |
| board_driver_arguments: None, |
| configuration_capabilities: None, |
| devicetree: None, |
| developer_only_options: None, |
| } |
| } |
| |
| /// Add developer overrides to the builder |
| pub fn add_developer_overrides( |
| &mut self, |
| developer_overrides: DeveloperOverrides, |
| ) -> Result<()> { |
| let DeveloperOverrides { developer_only_options, kernel, target_name: _ } = |
| developer_overrides; |
| |
| // Set the developer-only options for the buidler to use. |
| self.developer_only_options = Some(developer_only_options); |
| |
| // Add the kernel command line args from the developer |
| self.kernel_args.extend(kernel.command_line_args.into_iter()); |
| |
| Ok(()) |
| } |
| |
| /// Add an Assembly Input Bundle to the builder, via the path to its |
| /// manifest. |
| /// |
| /// If any of the items it's trying to add are duplicates (either of itself |
| /// or others, this will return an error.) |
| pub fn add_bundle(&mut self, bundle_path: impl AsRef<Utf8Path>) -> Result<()> { |
| let bundle = util::read_config(bundle_path.as_ref())?; |
| |
| // Strip filename from bundle path. |
| let bundle_path = |
| bundle_path.as_ref().parent().map(Utf8PathBuf::from).unwrap_or_else(|| "".into()); |
| |
| // Now add the parsed bundle |
| self.add_parsed_bundle(bundle_path, bundle) |
| } |
| |
| /// Add an Assembly Input Bundle to the builder, using a parsed |
| /// AssemblyInputBundle, and the path to the folder that contains it. |
| /// |
| /// If any of the items it's trying to add are duplicates (either of itself |
| /// or others, this will return an error.) |
| pub fn add_parsed_bundle( |
| &mut self, |
| bundle_path: impl AsRef<Utf8Path>, |
| bundle: AssemblyInputBundle, |
| ) -> Result<()> { |
| let bundle_path = bundle_path.as_ref(); |
| let AssemblyInputBundle { |
| kernel, |
| qemu_kernel, |
| boot_args, |
| bootfs_packages: _, |
| bootfs_files: _, |
| packages, |
| config_data, |
| blobs: _, |
| base_drivers, |
| boot_drivers, |
| shell_commands, |
| packages_to_compile, |
| bootfs_files_package, |
| } = bundle; |
| |
| self.add_bundle_packages(bundle_path, &packages)?; |
| |
| if let Some(path) = bootfs_files_package { |
| self.add_bootfs_files_from_path(bundle_path, path)?; |
| } |
| |
| // Base drivers are added to the base packages |
| for driver_details in base_drivers { |
| let driver_package_path = &bundle_path.join(&driver_details.package); |
| self.add_package_from_path(driver_package_path, PackageOrigin::AIB, &PackageSet::Base)?; |
| |
| let package_url = DriverManifestBuilder::get_package_url( |
| DriverPackageType::Base, |
| driver_package_path, |
| )?; |
| self.base_drivers.try_insert_unique(package_url, driver_details)?; |
| } |
| |
| // Boot drivers are added to the bootfs package set |
| for driver_details in boot_drivers { |
| let driver_package_path = &bundle_path.join(&driver_details.package); |
| self.add_package_from_path( |
| driver_package_path, |
| PackageOrigin::AIB, |
| &PackageSet::Bootfs, |
| )?; |
| |
| let package_url = DriverManifestBuilder::get_package_url( |
| DriverPackageType::Boot, |
| driver_package_path, |
| )?; |
| self.boot_drivers.try_insert_unique(package_url, driver_details)?; |
| } |
| |
| self.boot_args |
| .try_insert_all_unique(boot_args) |
| .map_err(|arg| anyhow!("duplicate boot_arg found: {}", arg))?; |
| |
| if let Some(kernel) = kernel { |
| assembly_util::set_option_once_or( |
| &mut self.kernel_path, |
| kernel.path.map(|p| bundle_path.join(p)), |
| anyhow!("Only one input bundle can specify a kernel path"), |
| )?; |
| |
| self.kernel_args |
| .try_insert_all_unique(kernel.args) |
| .map_err(|arg| anyhow!("duplicate kernel arg found: {}", arg))?; |
| } |
| |
| for (package, entries) in config_data { |
| for entry in Self::file_entry_paths_from_bundle(bundle_path, entries) { |
| self.add_config_data_entry(&package, entry)?; |
| } |
| } |
| |
| for (package, binaries) in shell_commands { |
| for binary in binaries { |
| self.add_shell_command_entry(&package, binary)?; |
| } |
| } |
| |
| for compiled_package in packages_to_compile { |
| self.add_compiled_package(&compiled_package, bundle_path)?; |
| } |
| |
| assembly_util::set_option_once_or( |
| &mut self.qemu_kernel, |
| qemu_kernel.map(|p| bundle_path.join(p)), |
| anyhow!("Only one input bundle can specify a qemu kernel path"), |
| )?; |
| |
| Ok(()) |
| } |
| |
| /// Add a Board input Bundle to the builder, using the path to the |
| /// folder that contains it. |
| /// |
| /// If any of the items it's trying to add are duplicates (either of itself |
| /// or others, this will return an error). |
| pub fn add_board_input_bundle( |
| &mut self, |
| bundle: BoardInputBundle, |
| bootstrap_only: bool, |
| ) -> Result<()> { |
| for PackagedDriverDetails { package, set, components } in bundle.drivers { |
| // These need to be consolidated into a single type so that they are |
| // less cumbersome. |
| let driver_package_type = match &set { |
| PackageSet::Base => DriverPackageType::Base, |
| PackageSet::Bootfs => DriverPackageType::Boot, |
| _ => bail!("Unsupported board package set type {:?}", &set), |
| }; |
| |
| // Always add the drivers if bootfs, and only add non-bootfs drivers |
| // if this is not a bootstrap_only build. |
| if set == PackageSet::Bootfs || !bootstrap_only { |
| self.add_package_from_path(&package, PackageOrigin::Board, &set)?; |
| |
| let package_url = |
| DriverManifestBuilder::get_package_url(driver_package_type, &package)?; |
| |
| let driver_set = match &set { |
| PackageSet::Base => &mut self.base_drivers, |
| PackageSet::Bootfs => &mut self.boot_drivers, |
| _ => bail!("Unsupported board package set type {:?}", &set), |
| }; |
| driver_set.try_insert_unique( |
| package_url, |
| DriverDetails { package: package.into(), components }, |
| )?; |
| } |
| } |
| |
| for PackageDetails { package, set } in bundle.packages { |
| // Always add the package if bootfs, and only add non-bootfs packages |
| // if this is not a bootstrap_only build. |
| if set == PackageSet::Bootfs || !bootstrap_only { |
| self.add_package_from_path(package, PackageOrigin::Board, &set)?; |
| } |
| } |
| |
| self.kernel_args |
| .try_insert_all_unique(bundle.kernel_boot_args) |
| .map_err(|arg| anyhow!("duplicate boot_arg found: {}", arg))?; |
| |
| Ok(()) |
| } |
| |
| /// Set the (optional) arguments for the Board Driver. |
| pub fn set_board_driver_arguments(&mut self, board_info: &BoardInformation) -> Result<()> { |
| if self.board_driver_arguments.is_some() { |
| bail!("Board driver arguments have already been set"); |
| } |
| self.board_driver_arguments = match &board_info.hardware_info { |
| HardwareInfo { |
| name, |
| vendor_id: Some(vendor_id), |
| product_id: Some(product_id), |
| revision: Some(revision), |
| } => Some(BoardDriverArguments { |
| vendor_id: *vendor_id, |
| product_id: *product_id, |
| revision: *revision, |
| name: name.as_ref().unwrap_or(&board_info.name).clone(), |
| }), |
| HardwareInfo { name: _, vendor_id: None, product_id: None, revision: None } => None, |
| _ => { |
| bail!("If any of 'vendor_id', 'product_id', or 'revision' are set, all must be provided: {:?}", &board_info.hardware_info); |
| } |
| }; |
| Ok(()) |
| } |
| |
| /// Add all the bootfs file entries to the builder. |
| pub fn add_bootfs_files(&mut self, files: &NamedFileMap<BootfsDestination>) -> Result<()> { |
| for entry in files.clone().into_file_entries() { |
| self.bootfs_files.add_entry(entry.to_owned())?; |
| } |
| Ok(()) |
| } |
| |
| /// Add kernel args to the builder |
| pub fn add_kernel_args(&mut self, args: impl IntoIterator<Item = String>) -> Result<()> { |
| self.kernel_args |
| .try_insert_all_unique(args) |
| .map_err(|arg| anyhow!("duplicate boot_arg found: {}", arg))?; |
| Ok(()) |
| } |
| |
| fn add_bootfs_files_from_path( |
| &mut self, |
| bundle_path: impl AsRef<Utf8Path>, |
| path: impl AsRef<Utf8Path>, |
| ) -> Result<()> { |
| let path = bundle_path.as_ref().join(path); |
| let manifest = PackageManifest::try_load_from(&path) |
| .with_context(|| format!("parsing {path} as a package manifest"))?; |
| for mut blob in manifest.into_blobs() { |
| if blob.path.starts_with("meta/") { |
| continue; |
| } |
| if let Some(path) = blob.path.strip_prefix("bootfs/") { |
| blob.path = path.to_string(); |
| } |
| self.bootfs_files |
| .add_blob_from_aib(blob) |
| .with_context(|| format!("adding bootfs file from {path}"))?; |
| } |
| Ok(()) |
| } |
| |
| fn add_package_from_path( |
| &mut self, |
| path: impl AsRef<Utf8Path>, |
| origin: PackageOrigin, |
| to_package_set: &PackageSet, |
| ) -> Result<()> { |
| // Create PackageEntry |
| let (d, package_entry) = PackageEntry::parse_from(origin, to_package_set.clone(), path)?; |
| |
| // Now store the package and it's destination. |
| self.packages |
| .try_insert_unique(d, package_entry) |
| .with_context(|| format!("Adding packages to {to_package_set}"))?; |
| |
| Ok(()) |
| } |
| |
| /// Add a set of packages from a bundle, resolving each path to a package |
| /// manifest from the bundle's path to locate it. |
| fn add_bundle_packages( |
| &mut self, |
| bundle_path: impl AsRef<Utf8Path>, |
| packages: &Vec<PackageDetails>, |
| ) -> Result<()> { |
| for entry in packages { |
| let manifest_path: Utf8PathBuf = |
| entry.package.clone().resolve_from_dir(&bundle_path)?.into(); |
| let set = match (&entry.set, &self.build_type, &self.developer_only_options) { |
| // BootFS packages are always in BootFS. |
| (&PackageSet::Bootfs, _, _) => PackageSet::Bootfs, |
| // System packages are always system packages |
| (&PackageSet::System, _, _) => PackageSet::System, |
| |
| // When the all_packages_in_base developer override option is |
| // enabled, that takes precedence over all the rest on eng and userdebug |
| // build-types. |
| (_, BuildType::Eng, Some(DeveloperOnlyOptions { all_packages_in_base: true })) |
| | ( |
| _, |
| BuildType::UserDebug, |
| Some(DeveloperOnlyOptions { all_packages_in_base: true }), |
| ) => PackageSet::Base, |
| |
| // The Flexible package set is in Cache for eng builds, and base |
| // for user/userdebug. |
| (&PackageSet::Flexible, BuildType::Eng, _) => PackageSet::Cache, |
| (&PackageSet::Flexible, _, _) => PackageSet::Base, |
| |
| // In all other cases, packages are just in their original |
| // package set. |
| (ps, _, _) => ps.clone(), |
| }; |
| self.add_package_from_path(manifest_path, PackageOrigin::AIB, &set)?; |
| } |
| |
| Ok(()) |
| } |
| |
| fn file_entry_paths_from_bundle( |
| base: &Utf8Path, |
| entries: impl IntoIterator<Item = FileEntry<String>>, |
| ) -> Vec<FileEntry<String>> { |
| entries |
| .into_iter() |
| .map(|entry| FileEntry { |
| destination: entry.destination, |
| source: base.join(entry.source), |
| }) |
| .collect() |
| } |
| |
| /// Add all the product-provided packages to the assembly configuration. |
| /// |
| /// This should be performed after the platform's bundles have been added, |
| /// so that any packages that are in conflict with the platform bundles are |
| /// flagged as being the issue (and not the platform being the issue). |
| pub fn add_product_packages(&mut self, packages: ProductPackagesConfig) -> Result<()> { |
| // Add the config data entries to the map |
| self.add_product_packages_to_set(packages.base, PackageSet::Base)?; |
| self.add_product_packages_to_set(packages.cache, PackageSet::Cache)?; |
| Ok(()) |
| } |
| |
| /// Add a vector of product packages to a specific package set. |
| fn add_product_packages_to_set( |
| &mut self, |
| entries: Vec<ProductPackageDetails>, |
| to_package_set: PackageSet, |
| ) -> Result<()> { |
| for entry in entries { |
| // Load the PackageManifest from the given path, in order to get the |
| // package name. |
| let manifest = PackageManifest::try_load_from(&entry.manifest) |
| .with_context(|| format!("parsing {} as a package manifest", &entry.manifest))?; |
| |
| // Add the package to the set of packages in the assembly. |
| self.add_package_from_path(entry.manifest, PackageOrigin::Product, &to_package_set)?; |
| |
| // Add the config data entries to the map |
| for ProductConfigData { source, destination } in entry.config_data { |
| // If there are config_data entries, convert the TypedPathBuf pairs into |
| // FileEntry objects. From this point on, they are handled as FileEntry |
| // TODO(tbd): Switch FileEntry to use TypedPathBuf instead of String and |
| // PathBuf. |
| self.add_config_data_entry( |
| &manifest.name().to_string(), |
| FileEntry { source: source.into(), destination: destination.to_string() }, |
| )?; |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Add the product-provided base-drivers to the assembly configuration. |
| /// |
| /// This should be performed after all the platform bundles have |
| /// been added as it is for packages. Packages specified as |
| /// base driver packages should not be in the base package set and |
| /// are added automatically. |
| pub fn add_product_base_drivers(&mut self, drivers: Vec<DriverDetails>) -> Result<()> { |
| // Base drivers are added to the base packages |
| // Config data is not supported for driver packages since it is deprecated. |
| for driver_details in drivers { |
| self.add_package_from_path( |
| &driver_details.package, |
| PackageOrigin::Product, |
| &PackageSet::Base, |
| ) |
| .context("Adding base drivers")?; |
| |
| let package_url = DriverManifestBuilder::get_package_url( |
| DriverPackageType::Base, |
| &driver_details.package, |
| )?; |
| |
| self.base_drivers.try_insert_unique(package_url, driver_details)?; |
| } |
| Ok(()) |
| } |
| |
| /// Add an entry to `config_data` for the given package. If the entry |
| /// duplicates an existing entry, return an error. |
| fn add_config_data_entry( |
| &mut self, |
| package: impl AsRef<str>, |
| entry: FileEntry<String>, |
| ) -> Result<()> { |
| let config_data = &mut self |
| .package_configs |
| .entry(package.as_ref().into()) |
| .or_insert_with(|| PackageConfiguration::new(package.as_ref())) |
| .config_data; |
| config_data.add_entry(entry).map_err(|dup| { |
| anyhow!( |
| "duplicate config data file found for package: {}\n error: {}", |
| package.as_ref(), |
| dup, |
| ) |
| }) |
| } |
| |
| fn add_shell_command_entry( |
| &mut self, |
| package_name: impl AsRef<str>, |
| binary: PackageInternalPathBuf, |
| ) -> Result<()> { |
| self.shell_commands |
| .entry(package_name.as_ref().into()) |
| .or_default() |
| .try_insert_unique(binary) |
| .map_err(|dup| { |
| anyhow!( |
| "duplicate shell command found in package: {} = {}", |
| package_name.as_ref(), |
| dup |
| ) |
| }) |
| } |
| |
| /// Set the configuration updates for a package. Can only be called once per |
| /// package. |
| pub fn set_package_config( |
| &mut self, |
| package: impl AsRef<str>, |
| config: PackageConfiguration, |
| ) -> Result<()> { |
| if self.package_configs.insert(package.as_ref().to_owned(), config).is_none() { |
| Ok(()) |
| } else { |
| Err(anyhow::format_err!("duplicate config patch")) |
| } |
| } |
| |
| /// Add a domain config package. |
| pub fn add_domain_config( |
| &mut self, |
| package: PackageSetDestination, |
| config: DomainConfig, |
| ) -> Result<()> { |
| if self.domain_configs.insert(package.into(), config).is_none() { |
| Ok(()) |
| } else { |
| Err(anyhow::format_err!("duplicate domain config")) |
| } |
| } |
| |
| pub fn add_configuration_capabilities( |
| &mut self, |
| config: assembly_config_capabilities::CapabilityNamedMap, |
| ) -> Result<()> { |
| if self.configuration_capabilities.is_some() { |
| return Err(anyhow::format_err!("duplicate configuration capabilities")); |
| } |
| self.configuration_capabilities = Some(config); |
| Ok(()) |
| } |
| |
| pub fn add_compiled_package( |
| &mut self, |
| compiled_package_def: &CompiledPackageDefinition, |
| bundle_path: &Utf8Path, |
| ) -> Result<()> { |
| let name = compiled_package_def.name().to_string(); |
| self.packages_to_compile |
| .entry(name.clone()) |
| .or_insert_with(|| CompiledPackageBuilder::new(name)) |
| .add_package_def(compiled_package_def, bundle_path) |
| .context("adding package def")?; |
| Ok(()) |
| } |
| |
| pub fn add_devicetree(&mut self, devicetree_path: &Utf8Path) -> Result<()> { |
| if self.devicetree.is_some() { |
| return Err(anyhow::format_err!("duplicate devicetree binary")); |
| } |
| self.devicetree = Some(devicetree_path.into()); |
| Ok(()) |
| } |
| |
| /// Construct an ImageAssembly ImageAssemblyConfig from the collected items in the |
| /// builder. |
| /// |
| /// If there are config_data entries, the config_data package will be |
| /// created in the outdir, and it will be added to the returned |
| /// ImageAssemblyConfig. |
| /// |
| /// If there are compiled packages specified, the compiled packages will |
| /// also be created in the outdir and added to the ImageAssemblyConfig. |
| /// |
| /// If this cannot create a completed ImageAssemblyConfig, it will return an error |
| /// instead. |
| pub fn build( |
| self, |
| outdir: impl AsRef<Utf8Path>, |
| tools: &impl ToolProvider, |
| ) -> Result<assembly_config_schema::ImageAssemblyConfig> { |
| let outdir = outdir.as_ref(); |
| // Decompose the fields in self, so that they can be recomposed into the generated |
| // image assembly configuration. |
| let Self { |
| build_type: _, |
| package_configs, |
| domain_configs, |
| mut packages, |
| base_drivers, |
| boot_drivers, |
| boot_args, |
| bootfs_files, |
| kernel_path, |
| kernel_args, |
| qemu_kernel, |
| shell_commands, |
| packages_to_compile, |
| board_driver_arguments, |
| configuration_capabilities, |
| devicetree, |
| developer_only_options: _, |
| } = self; |
| |
| let cmc_tool = tools.get_tool("cmc")?; |
| |
| // Add dynamically compiled packages first so they are all present |
| // and can be repackaged and configured |
| for (_, package_builder) in packages_to_compile { |
| let package_name = package_builder.name.to_owned(); |
| let package_manifest_path = package_builder |
| .build(cmc_tool.as_ref(), outdir) |
| .with_context(|| format!("building compiled package {}", &package_name))?; |
| |
| match package_manifest_path { |
| (p, PackageSet::Base) => { |
| let d = PackageSetDestination::Blob(PackageDestination::FromProduct( |
| package_name.clone(), |
| )); |
| let (_d, package_entry) = |
| PackageEntry::parse_from(PackageOrigin::AIB, PackageSet::Base, p)?; |
| packages |
| .try_insert_unique(d, package_entry) |
| .with_context(|| format!("Adding compiled package {package_name}"))?; |
| } |
| (p, PackageSet::Bootfs) => { |
| let d = PackageSetDestination::Boot(BootfsPackageDestination::FromAIB( |
| package_name.clone(), |
| )); |
| let (_d, package_entry) = |
| PackageEntry::parse_from(PackageOrigin::AIB, PackageSet::Bootfs, p)?; |
| packages |
| .try_insert_unique(d, package_entry) |
| .with_context(|| format!("Adding compiled package {package_name}"))?; |
| } |
| (_, package_set) => panic!("Unexpected package set {package_set}"), |
| }; |
| } |
| |
| // Repackage any matching packages |
| for (package, config) in &package_configs { |
| // Only process configs that have component entries for structured config. |
| if !config.components.is_empty() { |
| // Get the manifest for this package name, removing it from the set. There are two |
| // different potential destinations for this, so try each in order, removing the |
| // first entry that's found, so that we can replace it with the |
| if let Some((destination, entry)) = vec![ |
| PackageSetDestination::Blob(PackageDestination::FromAIB(package.clone())), |
| PackageSetDestination::Boot(BootfsPackageDestination::FromAIB(package.clone())), |
| ] |
| .into_iter() |
| .find_map(|d| packages.remove(&d).map(|entry| (d, entry))) |
| { |
| // Create the new package |
| let outdir = outdir.join("repackaged").join(&package); |
| let mut repackager = Repackager::new(entry.manifest, outdir) |
| .with_context(|| format!("reading existing manifest for {package}"))?; |
| |
| // Iterate over the components to get their structured config values |
| for (component, values) in &config.components { |
| repackager |
| .set_component_config(component, values.fields.clone().into()) |
| .with_context(|| format!("setting new config for {component}"))?; |
| } |
| let new_path = repackager |
| .build() |
| .with_context(|| format!("building repackaged {package}"))?; |
| let (_, new_entry) = |
| PackageEntry::parse_from(PackageOrigin::AIB, entry.package_set, new_path) |
| .with_context(|| { |
| "Parsing new package manifest for repackaged package: {package}" |
| })?; |
| |
| // Re-add the package now that it's been repackaged. |
| packages.try_insert_unique(destination, new_entry).with_context(|| { |
| format!("Re-adding package after repackaging with new config: {package}") |
| })?; |
| } else { |
| // TODO(https://fxbug.dev/42052394) return an error here |
| } |
| } |
| } |
| |
| // Construct the domain config packages |
| for (destination, config) in domain_configs { |
| let outdir = outdir.join(destination.to_string()); |
| std::fs::create_dir_all(&outdir) |
| .with_context(|| format!("creating directory {outdir}"))?; |
| let package = DomainConfigPackage::new(config); |
| let (path, manifest) = package |
| .build(outdir) |
| .with_context(|| format!("building domain config package {destination}"))?; |
| |
| let package_set = match &destination { |
| PackageSetDestination::Blob(_) => PackageSet::Base, |
| PackageSetDestination::Boot(_) => PackageSet::Bootfs, |
| }; |
| packages |
| .try_insert_unique(destination, PackageEntry { path, manifest, package_set }) |
| .context("Adding domain config package")?; |
| } |
| |
| // Generate the driver manifests and add to the configuration capability package |
| let mut configuration_capabilities = |
| configuration_capabilities.unwrap_or_else(|| NamedMap::new("config capabilities")); |
| { |
| let mut driver_manifest_builder = DriverManifestBuilder::default(); |
| for (package_url, driver_details) in boot_drivers.entries { |
| driver_manifest_builder |
| .add_driver(driver_details, &package_url) |
| .with_context(|| format!("adding driver {}", &package_url))?; |
| } |
| |
| configuration_capabilities.try_insert_unique( |
| "fuchsia.driver.BootDrivers".to_string(), |
| driver_manifest_builder.create_config(), |
| )?; |
| } |
| { |
| let mut driver_manifest_builder = DriverManifestBuilder::default(); |
| for (package_url, driver_details) in base_drivers.entries { |
| driver_manifest_builder |
| .add_driver(driver_details, &package_url) |
| .with_context(|| format!("adding driver {}", &package_url))?; |
| } |
| |
| configuration_capabilities.try_insert_unique( |
| "fuchsia.driver.BaseDrivers".to_string(), |
| driver_manifest_builder.create_config(), |
| )?; |
| } |
| |
| // Construct the config capability package. |
| { |
| let package_name = "config"; |
| let outdir = outdir.join(package_name); |
| std::fs::create_dir_all(&outdir) |
| .with_context(|| format!("creating directory {outdir}"))?; |
| |
| let (path, manifest) = assembly_config_capabilities::build_config_capability_package( |
| configuration_capabilities, |
| &outdir, |
| ) |
| .with_context(|| format!("building config capabilties package {package_name}"))?; |
| packages |
| .try_insert_unique( |
| PackageSetDestination::Boot(BootfsPackageDestination::Config), |
| PackageEntry { path, manifest, package_set: PackageSet::Bootfs }, |
| ) |
| .context("Adding config capabilities package")?; |
| } |
| |
| // Build the config_data package if we have any blobfs packages. |
| if packages.has_blobs() { |
| let mut config_data_builder = ConfigDataBuilder::default(); |
| for (package_name, config) in &package_configs { |
| for (destination, source_merkle_pair) in config.config_data.iter() { |
| config_data_builder.add_entry( |
| package_name, |
| destination.clone().into(), |
| source_merkle_pair.source.clone(), |
| )?; |
| } |
| } |
| let manifest_path = config_data_builder |
| .build(outdir) |
| .context("writing the 'config_data' package metafar.")?; |
| let (_, entry) = |
| PackageEntry::parse_from(PackageOrigin::AIB, PackageSet::Base, manifest_path)?; |
| packages |
| .try_insert_unique( |
| PackageSetDestination::Blob(PackageDestination::ConfigData), |
| entry, |
| ) |
| .context("Adding generated config-data package")?; |
| } |
| |
| if !shell_commands.is_empty() { |
| let mut shell_commands_builder = ShellCommandsBuilder::new(); |
| shell_commands_builder.add_shell_commands(shell_commands, "fuchsia.com".to_string()); |
| let manifest_path = |
| shell_commands_builder.build(outdir).context("building shell commands package")?; |
| let (_, entry) = |
| PackageEntry::parse_from(PackageOrigin::AIB, PackageSet::Base, manifest_path)?; |
| packages |
| .try_insert_unique( |
| PackageSetDestination::Blob(PackageDestination::ShellCommands), |
| entry, |
| ) |
| .context("Adding shell commands package to base")?; |
| } |
| |
| let bootfs_files = bootfs_files |
| .into_file_entries() |
| .iter() |
| .map(|e| FileEntry { source: e.source.clone(), destination: e.destination.to_string() }) |
| .collect(); |
| |
| // Construct a single "partial" config from the combined fields, and |
| // then pass this to the ImageAssemblyConfig::try_from_partials() to get the |
| // final validation that it's complete. |
| let image_assembly_config = assembly_config_schema::ImageAssemblyConfig { |
| base: packages.package_manifest_paths(PackageSet::Base), |
| cache: packages.package_manifest_paths(PackageSet::Cache), |
| system: packages.package_manifest_paths(PackageSet::System), |
| bootfs_packages: packages.package_manifest_paths(PackageSet::Bootfs), |
| kernel: KernelConfig { |
| path: kernel_path.context("A kernel path must be specified")?, |
| args: kernel_args.into_iter().collect(), |
| }, |
| qemu_kernel: qemu_kernel.context("A qemu kernel configuration must be specified")?, |
| boot_args: boot_args.into_iter().collect(), |
| bootfs_files, |
| images_config: Default::default(), |
| board_driver_arguments, |
| devicetree, |
| }; |
| Ok(image_assembly_config) |
| } |
| } |
| |
| /// A wrapper around all the packages |
| #[derive(Debug, Default, Serialize)] |
| struct Packages { |
| inner: BTreeMap<PackageSetDestination, PackageEntry>, |
| } |
| |
| impl Packages { |
| /// Insert a package entry by it's destination. This enforces that there |
| /// no duplicate destinations. |
| fn try_insert_unique( |
| &mut self, |
| destination: PackageSetDestination, |
| entry: PackageEntry, |
| ) -> Result<()> { |
| self.inner |
| .try_insert_unique(MapEntry(destination, entry)) |
| .map_err(|e| anyhow!("Duplicate package found {}", e.existing_entry.key())) |
| } |
| |
| /// Remove a package entry by its destination. |
| fn remove(&mut self, destination: &PackageSetDestination) -> Option<PackageEntry> { |
| self.inner.remove(destination) |
| } |
| |
| /// Returns true if there are any packages destined for BlobFS. |
| fn has_blobs(&self) -> bool { |
| self.inner.iter().any(|(d, _)| matches!(d, PackageSetDestination::Blob(_))) |
| } |
| |
| /// Returns a sorted list of the package manifests for a given package set. |
| fn package_manifest_paths(&self, package_set: PackageSet) -> Vec<Utf8PathBuf> { |
| self.inner |
| .values() |
| .filter_map(|e| if e.package_set == package_set { Some(e.path.clone()) } else { None }) |
| .sorted() |
| .collect() |
| } |
| } |
| |
| /// Information about a single package in the set. |
| #[derive(Debug, Serialize)] |
| struct PackageEntry { |
| /// Path to the package manifest. |
| path: Utf8PathBuf, |
| |
| /// Parsed package manifest. |
| manifest: PackageManifest, |
| |
| /// Which package set that the package belongs to. |
| package_set: PackageSet, |
| } |
| |
| impl PackageEntry { |
| /// Construct a PackageEntry from a path to a package manifest, and a given |
| /// destination package set. |
| pub fn parse_from( |
| origin: PackageOrigin, |
| package_set: PackageSet, |
| path: impl AsRef<Utf8Path>, |
| ) -> Result<(PackageSetDestination, Self)> { |
| let path = path.as_ref(); |
| let manifest = PackageManifest::try_load_from(path) |
| .with_context(|| format!("parsing {path} as a package manifest"))?; |
| |
| let name = manifest.name().to_string(); |
| if name == "" { |
| bail!("Package with no name {path}"); |
| } |
| |
| let destination = match &package_set { |
| PackageSet::Base |
| | PackageSet::Cache |
| | PackageSet::System |
| | PackageSet::Flexible |
| | PackageSet::OnDemand => PackageSetDestination::Blob(match &origin { |
| PackageOrigin::AIB => PackageDestination::FromAIB(name), |
| PackageOrigin::Board => PackageDestination::FromBoard(name), |
| PackageOrigin::Product => PackageDestination::FromProduct(name), |
| }), |
| PackageSet::Bootfs => PackageSetDestination::Boot(match &origin { |
| PackageOrigin::AIB => BootfsPackageDestination::FromAIB(name), |
| PackageOrigin::Board => BootfsPackageDestination::FromBoard(name), |
| PackageOrigin::Product => bail!("Products cannot add packages to bootfs ({path})"), |
| }), |
| }; |
| |
| Ok((destination, Self { path: path.to_path_buf(), manifest, package_set })) |
| } |
| } |
| |
| /// The origin of a package. |
| #[derive(Debug, Serialize)] |
| #[serde(rename_all = "snake_case")] |
| enum PackageOrigin { |
| /// The package is from a platform AIB |
| AIB, |
| |
| /// The package is from the board |
| Board, |
| |
| /// The package is from the product |
| Product, |
| } |
| |
| impl std::fmt::Display for PackageOrigin { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.write_str(match self { |
| PackageOrigin::AIB => "platform", |
| PackageOrigin::Board => "board", |
| PackageOrigin::Product => "product", |
| }) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use assembly_config_schema::assembly_config::{ |
| AdditionalPackageContents, MainPackageDefinition, |
| }; |
| use assembly_config_schema::image_assembly_config::PartialKernelConfig; |
| use assembly_file_relative_path::FileRelativePathBuf; |
| use assembly_named_file_map::SourceMerklePair; |
| use assembly_package_utils::PackageManifestPathBuf; |
| use assembly_platform_configuration::ComponentConfigs; |
| use assembly_test_util::generate_test_manifest; |
| use assembly_tool::testing::FakeToolProvider; |
| use assembly_tool::ToolCommandLog; |
| use assembly_util::{CompiledPackageDestination, TestCompiledPackageDestination::ForTest}; |
| use fuchsia_pkg::{BlobInfo, MetaPackage, PackageBuilder, PackageManifestBuilder}; |
| use serde_json::json; |
| use std::fs::File; |
| use std::io::Write; |
| use tempfile::TempDir; |
| |
| struct TempdirPathsForTest { |
| _tmp: TempDir, |
| pub outdir: Utf8PathBuf, |
| pub bundle_path: Utf8PathBuf, |
| pub config_data_target_package_name: String, |
| pub config_data_target_package_dir: Utf8PathBuf, |
| pub config_data_file_path: Utf8PathBuf, |
| } |
| |
| impl TempdirPathsForTest { |
| fn new() -> Self { |
| let tmp = TempDir::new().unwrap(); |
| let outdir = Utf8Path::from_path(tmp.path()).unwrap().to_path_buf(); |
| let bundle_path = outdir.join("bundle"); |
| let config_data_target_package_name = "base_package0".to_owned(); |
| let config_data_target_package_dir = |
| bundle_path.join("config_data").join(&config_data_target_package_name); |
| let config_data_file_path = |
| config_data_target_package_dir.join("config_data_source_file"); |
| Self { |
| _tmp: tmp, |
| outdir, |
| bundle_path, |
| config_data_target_package_name, |
| config_data_target_package_dir, |
| config_data_file_path, |
| } |
| } |
| } |
| |
| fn write_empty_pkg( |
| path: impl AsRef<Utf8Path>, |
| name: &str, |
| repo: Option<&str>, |
| ) -> PackageManifestPathBuf { |
| let path = path.as_ref(); |
| let mut builder = PackageBuilder::new_platform_internal_package(name); |
| let manifest_path = path.join(name); |
| builder.manifest_path(&manifest_path); |
| if let Some(repo_name) = repo { |
| builder.repository(repo_name); |
| } else { |
| builder.repository("fuchsia.com"); |
| } |
| builder.build(path, path.join(format!("{name}_meta.far"))).unwrap(); |
| manifest_path.into() |
| } |
| |
| fn make_test_assembly_bundle(outdir: &Utf8Path, bundle_path: &Utf8Path) -> AssemblyInputBundle { |
| let test_file_path = outdir.join("bootfs_files_package"); |
| let mut test_file = File::create(&test_file_path).unwrap(); |
| let builder = PackageManifestBuilder::new(MetaPackage::from_name_and_variant_zero( |
| "bootfs_files_package".parse().unwrap(), |
| )); |
| let builder = builder.repository("testrepository.com"); |
| let builder = builder.add_blob(BlobInfo { |
| source_path: "source/path/to/file".into(), |
| path: "dest/file/path".into(), |
| merkle: "0000000000000000000000000000000000000000000000000000000000000000" |
| .parse() |
| .unwrap(), |
| size: 1, |
| }); |
| let manifest = builder.build(); |
| serde_json::to_writer(&test_file, &manifest).unwrap(); |
| test_file.flush().unwrap(); |
| |
| let write_empty_bundle_pkg = |name: &str| { |
| FileRelativePathBuf::FileRelative(write_empty_pkg(bundle_path, name, None).clone()) |
| }; |
| AssemblyInputBundle { |
| kernel: Some(PartialKernelConfig { |
| path: Some("kernel/path".into()), |
| args: vec!["kernel_arg0".into()], |
| }), |
| qemu_kernel: Some("path/to/qemu/kernel".into()), |
| boot_args: vec!["boot_arg0".into()], |
| bootfs_files: vec![], |
| bootfs_packages: vec![], |
| packages: vec![ |
| PackageDetails { |
| package: write_empty_bundle_pkg("base_package0"), |
| set: PackageSet::Base, |
| }, |
| PackageDetails { |
| package: write_empty_bundle_pkg("cache_package0"), |
| set: PackageSet::Cache, |
| }, |
| PackageDetails { |
| package: write_empty_bundle_pkg("flexible_package0"), |
| set: assembly_config_schema::PackageSet::Flexible, |
| }, |
| PackageDetails { |
| package: write_empty_bundle_pkg("bootfs_package0"), |
| set: PackageSet::Bootfs, |
| }, |
| PackageDetails { |
| package: write_empty_bundle_pkg("sys_package0"), |
| set: PackageSet::System, |
| }, |
| ], |
| base_drivers: Vec::default(), |
| boot_drivers: Vec::default(), |
| config_data: BTreeMap::default(), |
| blobs: Vec::default(), |
| shell_commands: ShellCommands::default(), |
| packages_to_compile: Vec::default(), |
| bootfs_files_package: Some(test_file_path), |
| } |
| } |
| |
| fn make_test_driver(package_name: &str, outdir: impl AsRef<Utf8Path>) -> Result<DriverDetails> { |
| let driver_package_manifest_file_path = outdir.as_ref().join(package_name); |
| let mut driver_package_manifest_file = File::create(&driver_package_manifest_file_path)?; |
| let package_manifest = generate_test_manifest(package_name, None); |
| serde_json::to_writer(&driver_package_manifest_file, &package_manifest)?; |
| driver_package_manifest_file.flush()?; |
| |
| Ok(DriverDetails { |
| package: driver_package_manifest_file_path, |
| components: vec![Utf8PathBuf::from("meta/foobar.cm")], |
| }) |
| } |
| |
| /// Create an ImageAssemblyConfigBuilder with a minimal AssemblyInputBundle |
| /// for testing product configuration. |
| /// |
| /// # Arguments |
| /// |
| /// * `package_names` - names for empty stub packages to create and add to the |
| /// base set. |
| fn get_minimum_config_builder( |
| outdir: impl AsRef<Utf8Path>, |
| package_names: Vec<String>, |
| ) -> ImageAssemblyConfigBuilder { |
| let minimum_bundle = AssemblyInputBundle { |
| kernel: Some(PartialKernelConfig { |
| path: Some("kernel/path".into()), |
| args: Vec::default(), |
| }), |
| qemu_kernel: Some("kernel/qemu/path".into()), |
| packages: package_names |
| .iter() |
| .map(|package_name| PackageDetails { |
| package: FileRelativePathBuf::FileRelative( |
| write_empty_pkg(&outdir, package_name, None).into(), |
| ), |
| set: PackageSet::Base, |
| }) |
| .collect(), |
| base_drivers: Vec::default(), |
| boot_drivers: Vec::default(), |
| config_data: BTreeMap::default(), |
| blobs: Vec::default(), |
| shell_commands: ShellCommands::default(), |
| packages_to_compile: Vec::default(), |
| bootfs_files_package: None, |
| ..AssemblyInputBundle::default() |
| }; |
| let mut builder = ImageAssemblyConfigBuilder::new(BuildType::Eng); |
| builder.add_parsed_bundle(outdir.as_ref().join("minimum_bundle"), minimum_bundle).unwrap(); |
| builder |
| } |
| |
| #[test] |
| fn test_builder() { |
| let vars = TempdirPathsForTest::new(); |
| let tools = FakeToolProvider::default(); |
| |
| let mut builder = ImageAssemblyConfigBuilder::new(BuildType::Eng); |
| builder |
| .add_parsed_bundle( |
| &vars.outdir, |
| make_test_assembly_bundle(&vars.outdir, &vars.bundle_path), |
| ) |
| .unwrap(); |
| let result: assembly_config_schema::ImageAssemblyConfig = |
| builder.build(&vars.outdir, &tools).unwrap(); |
| |
| assert_eq!( |
| result.base, |
| vec![ |
| vars.bundle_path.join("base_package0"), |
| vars.outdir.join("config_data/package_manifest.json") |
| ] |
| ); |
| assert_eq!( |
| result.cache, |
| vec![ |
| vars.bundle_path.join("cache_package0"), |
| vars.bundle_path.join("flexible_package0"), |
| ] |
| ); |
| assert_eq!(result.system, vec![vars.bundle_path.join("sys_package0")]); |
| assert_eq!( |
| result.bootfs_packages, |
| vec![ |
| vars.bundle_path.join("bootfs_package0"), |
| vars.outdir.join("config/package_manifest.json"), |
| ] |
| ); |
| assert_eq!(result.boot_args, vec!("boot_arg0".to_string())); |
| assert_eq!( |
| result |
| .bootfs_files |
| .iter() |
| .map(|f| f.destination.to_owned()) |
| .sorted() |
| .collect::<Vec<_>>(), |
| vec!["dest/file/path"], |
| ); |
| |
| assert_eq!(result.kernel.path, vars.outdir.join("kernel/path")); |
| assert_eq!(result.kernel.args, vec!("kernel_arg0".to_string())); |
| assert_eq!(result.qemu_kernel, vars.outdir.join("path/to/qemu/kernel")); |
| } |
| |
| #[test] |
| fn test_builder_userdebug() { |
| let vars = TempdirPathsForTest::new(); |
| let tools = FakeToolProvider::default(); |
| |
| let mut builder = ImageAssemblyConfigBuilder::new(BuildType::UserDebug); |
| builder |
| .add_parsed_bundle( |
| &vars.outdir, |
| make_test_assembly_bundle(&vars.outdir, &vars.bundle_path), |
| ) |
| .unwrap(); |
| let result: assembly_config_schema::ImageAssemblyConfig = |
| builder.build(&vars.outdir, &tools).unwrap(); |
| |
| assert_eq!( |
| result.base, |
| vec![ |
| vars.bundle_path.join("base_package0"), |
| vars.bundle_path.join("flexible_package0"), |
| vars.outdir.join("config_data/package_manifest.json") |
| ] |
| ); |
| assert_eq!(result.cache, vec![vars.bundle_path.join("cache_package0"),]); |
| assert_eq!(result.system, vec![vars.bundle_path.join("sys_package0")]); |
| assert_eq!( |
| result.bootfs_packages, |
| vec![ |
| vars.bundle_path.join("bootfs_package0"), |
| vars.outdir.join("config/package_manifest.json"), |
| ] |
| ); |
| assert_eq!(result.boot_args, vec!("boot_arg0".to_string())); |
| assert_eq!( |
| result |
| .bootfs_files |
| .iter() |
| .map(|f| f.destination.to_owned()) |
| .sorted() |
| .collect::<Vec<_>>(), |
| vec!["dest/file/path".to_string()], |
| ); |
| |
| assert_eq!(result.kernel.path, vars.outdir.join("kernel/path")); |
| assert_eq!(result.kernel.args, vec!("kernel_arg0".to_string())); |
| assert_eq!(result.qemu_kernel, vars.outdir.join("path/to/qemu/kernel")); |
| } |
| |
| #[test] |
| fn test_builder_user() { |
| let vars = TempdirPathsForTest::new(); |
| let tools = FakeToolProvider::default(); |
| |
| let mut builder = ImageAssemblyConfigBuilder::new(BuildType::User); |
| builder |
| .add_parsed_bundle( |
| &vars.outdir, |
| make_test_assembly_bundle(&vars.outdir, &vars.bundle_path), |
| ) |
| .unwrap(); |
| let result: assembly_config_schema::ImageAssemblyConfig = |
| builder.build(&vars.outdir, &tools).unwrap(); |
| |
| assert_eq!( |
| result.base, |
| vec![ |
| vars.bundle_path.join("base_package0"), |
| vars.bundle_path.join("flexible_package0"), |
| vars.outdir.join("config_data/package_manifest.json") |
| ] |
| ); |
| assert_eq!(result.cache, vec![vars.bundle_path.join("cache_package0"),]); |
| assert_eq!(result.system, vec![vars.bundle_path.join("sys_package0")]); |
| assert_eq!( |
| result.bootfs_packages, |
| vec![ |
| vars.bundle_path.join("bootfs_package0"), |
| vars.outdir.join("config/package_manifest.json"), |
| ] |
| ); |
| assert_eq!(result.boot_args, vec!("boot_arg0".to_string())); |
| assert_eq!( |
| result |
| .bootfs_files |
| .iter() |
| .map(|f| f.destination.to_owned()) |
| .sorted() |
| .collect::<Vec<_>>(), |
| vec!["dest/file/path".to_string()], |
| ); |
| |
| assert_eq!(result.kernel.path, vars.outdir.join("kernel/path")); |
| assert_eq!(result.kernel.args, vec!("kernel_arg0".to_string())); |
| assert_eq!(result.qemu_kernel, vars.outdir.join("path/to/qemu/kernel")); |
| } |
| |
| fn setup_builder( |
| vars: &TempdirPathsForTest, |
| bundles: Vec<AssemblyInputBundle>, |
| ) -> ImageAssemblyConfigBuilder { |
| let mut builder = ImageAssemblyConfigBuilder::new(BuildType::Eng); |
| |
| // Write a file to the temp dir for use with config_data. |
| std::fs::create_dir_all(&vars.config_data_target_package_dir).unwrap(); |
| std::fs::write(&vars.config_data_file_path, "configuration data").unwrap(); |
| for bundle in bundles { |
| builder.add_parsed_bundle(&vars.bundle_path, bundle).unwrap(); |
| } |
| builder |
| } |
| |
| #[test] |
| fn test_builder_with_config_data() { |
| let vars = TempdirPathsForTest::new(); |
| let tools = FakeToolProvider::default(); |
| |
| // Create an assembly bundle and add a config_data entry to it. |
| let mut bundle = make_test_assembly_bundle(&vars.outdir, &vars.bundle_path); |
| |
| bundle.config_data.insert( |
| vars.config_data_target_package_name.clone(), |
| vec![FileEntry { |
| source: vars.config_data_file_path.clone(), |
| destination: "dest/file/path".to_owned(), |
| }], |
| ); |
| |
| let mut builder = setup_builder(&vars, vec![]); |
| builder |
| .set_package_config( |
| vars.config_data_target_package_name.clone(), |
| PackageConfiguration { |
| components: ComponentConfigs::new("component configs"), |
| name: vars.config_data_target_package_name.clone(), |
| config_data: NamedFileMap { |
| map: NamedMap { |
| name: "config data".into(), |
| entries: [( |
| "dest/platform/configuration".into(), |
| SourceMerklePair { |
| merkle: None, |
| source: vars.config_data_file_path, |
| }, |
| )] |
| .into(), |
| }, |
| }, |
| }, |
| ) |
| .unwrap(); |
| builder.add_parsed_bundle(&vars.bundle_path, bundle).unwrap(); |
| let result: assembly_config_schema::ImageAssemblyConfig = |
| builder.build(&vars.outdir, &tools).unwrap(); |
| |
| // config_data's manifest is in outdir |
| let expected_config_data_manifest_path = |
| vars.outdir.join("config_data").join("package_manifest.json"); |
| |
| // Validate that the base package set contains config_data. |
| assert_eq!(result.base.len(), 2); |
| assert!(result.base.contains(&vars.bundle_path.join("base_package0"))); |
| assert!(result.base.contains(&expected_config_data_manifest_path)); |
| |
| // Validate the contents of config_data is what is, expected by: |
| // 1. Reading in the package manifest to get the metafar path |
| // 2. Opening the metafar |
| // 3. Reading the config_data entry's file |
| // 4. Validate the contents of the file |
| |
| // 1. Read the config_data package manifest |
| let config_data_manifest = |
| PackageManifest::try_load_from(expected_config_data_manifest_path).unwrap(); |
| assert_eq!(config_data_manifest.name().as_ref(), "config-data"); |
| |
| // and get the metafar path. |
| let blobs = config_data_manifest.into_blobs(); |
| let metafar_blobinfo = blobs.get(0).unwrap(); |
| assert_eq!(metafar_blobinfo.path, "meta/"); |
| |
| // 2. Read the metafar. |
| let mut config_data_metafar = File::open(&metafar_blobinfo.source_path).unwrap(); |
| let mut far_reader = fuchsia_archive::Utf8Reader::new(&mut config_data_metafar).unwrap(); |
| |
| // 3. Read the configuration file. |
| let config_file_data = far_reader |
| .read_file(&format!( |
| "meta/data/{}/dest/file/path", |
| vars.config_data_target_package_name |
| )) |
| .unwrap(); |
| |
| // 4. Validate its contents. |
| assert_eq!(config_file_data, "configuration data".as_bytes()); |
| |
| // 5. Read the configuration file from the platform configuration. |
| let config_file_data = far_reader |
| .read_file(&format!( |
| "meta/data/{}/dest/platform/configuration", |
| vars.config_data_target_package_name |
| )) |
| .unwrap(); |
| |
| // 6. Validate its contents. |
| assert_eq!(config_file_data, "configuration data".as_bytes()); |
| } |
| |
| #[test] |
| fn test_builder_with_domain_config() { |
| let vars = TempdirPathsForTest::new(); |
| let tools = FakeToolProvider::default(); |
| |
| let bundle = make_test_assembly_bundle(&vars.outdir, &vars.bundle_path); |
| let mut builder = setup_builder(&vars, vec![bundle]); |
| |
| let destination = PackageSetDestination::Blob(PackageDestination::ForTest); |
| let config = DomainConfig { |
| directories: NamedMap::new("test"), |
| name: destination.clone(), |
| expose_directories: false, |
| }; |
| builder.add_domain_config(destination, config).unwrap(); |
| |
| let result: assembly_config_schema::ImageAssemblyConfig = |
| builder.build(&vars.outdir, &tools).unwrap(); |
| |
| // The domain config's manifest is in outdir |
| let expected_manifest_path = vars.outdir.join("for-test").join("package_manifest.json"); |
| |
| // Validate that the base package set contains the domain config. |
| assert!(result.base.contains(&expected_manifest_path)); |
| } |
| |
| #[test] |
| fn test_builder_with_bootfs_domain_config() { |
| let vars = TempdirPathsForTest::new(); |
| let tools = FakeToolProvider::default(); |
| |
| let bundle = make_test_assembly_bundle(&vars.outdir, &vars.bundle_path); |
| let mut builder = setup_builder(&vars, vec![bundle]); |
| |
| let destination = PackageSetDestination::Boot(BootfsPackageDestination::ForTest); |
| let config = DomainConfig { |
| directories: NamedMap::new("test"), |
| name: destination.clone(), |
| expose_directories: false, |
| }; |
| builder.add_domain_config(destination, config).unwrap(); |
| |
| let result: assembly_config_schema::ImageAssemblyConfig = |
| builder.build(&vars.outdir, &tools).unwrap(); |
| |
| // The domain config's manifest is in outdir |
| let expected_manifest_path = vars.outdir.join("for-test").join("package_manifest.json"); |
| |
| // Validate that the bootfs package set contains the domain config. |
| assert!(result.bootfs_packages.contains(&expected_manifest_path)); |
| } |
| |
| #[test] |
| fn test_builder_with_shell_commands() { |
| let vars = TempdirPathsForTest::new(); |
| let tools = FakeToolProvider::default(); |
| |
| // Make an assembly input bundle with Shell Commands in it |
| let mut bundle = make_test_assembly_bundle(&vars.outdir, &vars.bundle_path); |
| bundle.shell_commands.insert( |
| "package1".to_string(), |
| BTreeSet::from([ |
| PackageInternalPathBuf::from("bin/binary1"), |
| PackageInternalPathBuf::from("bin/binary2"), |
| ]), |
| ); |
| let builder = setup_builder(&vars, vec![bundle]); |
| |
| let result: assembly_config_schema::ImageAssemblyConfig = |
| builder.build(&vars.outdir, &tools).unwrap(); |
| |
| // config_data's manifest is in outdir |
| let expected_manifest_path = |
| vars.outdir.join("shell-commands").join("package_manifest.json"); |
| |
| // Validate that the base package set contains shell_commands. |
| assert_eq!(result.base.len(), 3); |
| assert!(result.base.contains(&expected_manifest_path)); |
| } |
| |
| #[test] |
| fn test_builder_with_product_packages_and_config() { |
| let tmp = TempDir::new().unwrap(); |
| let outdir = Utf8Path::from_path(tmp.path()).unwrap(); |
| let tools = FakeToolProvider::default(); |
| |
| // Create some config_data source files |
| let config_data_source_dir = outdir.join("config_data_source"); |
| let config_data_source_a = config_data_source_dir.join("cfg.txt"); |
| let config_data_source_b = config_data_source_dir.join("other.json"); |
| std::fs::create_dir_all(&config_data_source_dir).unwrap(); |
| std::fs::write(&config_data_source_a, "source a").unwrap(); |
| std::fs::write(&config_data_source_b, "{}").unwrap(); |
| |
| let packages = ProductPackagesConfig { |
| base: vec![ |
| write_empty_pkg(outdir, "base_a", None).into(), |
| ProductPackageDetails { |
| manifest: write_empty_pkg(outdir, "base_b", None), |
| config_data: Vec::default(), |
| }, |
| ProductPackageDetails { |
| manifest: write_empty_pkg(outdir, "base_c", None), |
| config_data: vec![ |
| ProductConfigData { |
| destination: "dest/path/cfg.txt".into(), |
| source: config_data_source_a.into(), |
| }, |
| ProductConfigData { |
| destination: "other_data.json".into(), |
| source: config_data_source_b.into(), |
| }, |
| ], |
| }, |
| ], |
| cache: vec![ |
| write_empty_pkg(outdir, "cache_a", None).into(), |
| write_empty_pkg(outdir, "cache_b", None).into(), |
| ], |
| }; |
| |
| let mut builder = get_minimum_config_builder( |
| outdir, |
| vec!["platform_a".to_owned(), "platform_b".to_owned()], |
| ); |
| builder.add_product_packages(packages).unwrap(); |
| let result: assembly_config_schema::ImageAssemblyConfig = |
| builder.build(outdir, &tools).unwrap(); |
| |
| assert_eq!( |
| result.base, |
| [ |
| "base_a", |
| "base_b", |
| "base_c", |
| "config_data/package_manifest.json", |
| "platform_a", |
| "platform_b", |
| ] |
| .iter() |
| .map(|p| outdir.join(p)) |
| .collect::<Vec<_>>() |
| ); |
| assert_eq!(result.cache, vec![outdir.join("cache_a"), outdir.join("cache_b")]); |
| |
| // Validate product-provided config-data is correct |
| let config_data_pkg = |
| PackageManifest::try_load_from(outdir.join("config_data/package_manifest.json")) |
| .unwrap(); |
| let metafar_blobinfo = config_data_pkg.blobs().iter().find(|b| b.path == "meta/").unwrap(); |
| let mut far_reader = |
| fuchsia_archive::Utf8Reader::new(File::open(&metafar_blobinfo.source_path).unwrap()) |
| .unwrap(); |
| |
| // Assert both config_data files match those written above |
| let config_data_a_bytes = |
| far_reader.read_file("meta/data/base_c/dest/path/cfg.txt").unwrap(); |
| let config_data_a = std::str::from_utf8(&config_data_a_bytes).unwrap(); |
| let config_data_b_bytes = far_reader.read_file("meta/data/base_c/other_data.json").unwrap(); |
| let config_data_b = std::str::from_utf8(&config_data_b_bytes).unwrap(); |
| assert_eq!(config_data_a, "source a"); |
| assert_eq!(config_data_b, "{}"); |
| } |
| |
| #[test] |
| fn test_builder_with_compiled_packages() -> Result<()> { |
| let vars = TempdirPathsForTest::new(); |
| let tools = FakeToolProvider::default(); |
| // Write the expected output component files since the component |
| // compiler is mocked. |
| let component1_dir = vars.outdir.join("for-test/component1"); |
| let component2_dir = vars.outdir.join("for-test/component2"); |
| std::fs::create_dir_all(&component1_dir).unwrap(); |
| std::fs::create_dir_all(&component2_dir).unwrap(); |
| std::fs::write(component1_dir.join("component1.cm"), "component fake contents").unwrap(); |
| std::fs::write(component2_dir.join("component2.cm"), "component fake contents").unwrap(); |
| |
| // Create 2 assembly bundle and add a config_data entry to it. |
| let mut bundle1 = make_test_assembly_bundle(&vars.outdir, &vars.bundle_path); |
| bundle1.packages_to_compile.push(CompiledPackageDefinition::MainDefinition( |
| MainPackageDefinition { |
| name: CompiledPackageDestination::Test(ForTest), |
| components: BTreeMap::from([ |
| ("component1".into(), "cml1".into()), |
| ("component2".into(), "cml2".into()), |
| ]), |
| contents: Vec::default(), |
| includes: Vec::default(), |
| bootfs_package: false, |
| }, |
| )); |
| let bundle2 = AssemblyInputBundle { |
| packages_to_compile: vec![CompiledPackageDefinition::Additional( |
| AdditionalPackageContents { |
| name: CompiledPackageDestination::Test(ForTest), |
| component_shards: BTreeMap::from([( |
| "component2".into(), |
| vec!["shard1".into()], |
| )]), |
| }, |
| )], |
| ..Default::default() |
| }; |
| |
| let builder = setup_builder(&vars, vec![bundle1, bundle2]); |
| let _: assembly_config_schema::ImageAssemblyConfig = |
| builder.build(&vars.outdir, &tools).unwrap(); |
| |
| // Make sure all the components and CML shards from the separate bundles |
| // are merged. |
| let expected_commands: ToolCommandLog = serde_json::from_value(json!({ |
| "commands": [ |
| { |
| "tool": "./host_x64/cmc", |
| "args": [ |
| "merge", |
| "--output", |
| vars.outdir.join("for-test/component1/component1.cml").as_str(), |
| vars.outdir.join("bundle/cml1").as_str() |
| ] |
| }, |
| { |
| "tool": "./host_x64/cmc", |
| "args": [ |
| "compile", |
| "--features=allow_long_names", |
| "--includeroot", |
| vars.outdir.join("bundle/compiled_packages/include").as_str(), |
| "--includepath", |
| vars.outdir.join("bundle/compiled_packages/include").as_str(), |
| "--config-package-path", |
| "meta/component1.cvf", |
| "-o", |
| vars.outdir.join("for-test/component1/component1.cm").as_str(), |
| vars.outdir.join("for-test/component1/component1.cml").as_str() |
| ] |
| }, |
| { |
| "tool": "./host_x64/cmc", |
| "args": [ |
| "merge", |
| "--output", |
| vars.outdir.join("for-test/component2/component2.cml").as_str(), |
| vars.outdir.join("bundle/cml2").as_str(), |
| vars.outdir.join("bundle/shard1") |
| ] |
| }, |
| { |
| "tool": "./host_x64/cmc", |
| "args": [ |
| "compile", |
| "--features=allow_long_names", |
| "--includeroot", |
| vars.outdir.join("bundle/compiled_packages/include").as_str(), |
| "--includepath", |
| vars.outdir.join("bundle/compiled_packages/include").as_str(), |
| "--config-package-path", |
| "meta/component2.cvf", |
| "-o", |
| vars.outdir.join("for-test/component2/component2.cm").as_str(), |
| vars.outdir.join("for-test/component2/component2.cml").as_str() |
| ] |
| } |
| ] |
| })) |
| .unwrap(); |
| assert_eq!(&expected_commands, tools.log()); |
| |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_builder_with_product_packages_catches_duplicates() -> Result<()> { |
| let tmp = TempDir::new().unwrap(); |
| let outdir = Utf8Path::from_path(tmp.path()).unwrap(); |
| |
| let packages = ProductPackagesConfig { |
| base: vec![write_empty_pkg(outdir, "base_a", None).into()], |
| ..ProductPackagesConfig::default() |
| }; |
| let mut builder = get_minimum_config_builder(outdir, vec!["base_a".to_owned()]); |
| |
| let result = builder.add_product_packages(packages); |
| assert!(result.is_err()); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_builder_with_product_drivers_catches_duplicates() -> Result<()> { |
| let tmp = TempDir::new().unwrap(); |
| let outdir = Utf8Path::from_path(tmp.path()).unwrap(); |
| |
| let base_driver_1 = make_test_driver("driver1", outdir)?; |
| let mut builder = get_minimum_config_builder(outdir, vec!["driver1".to_owned()]); |
| |
| let result = builder.add_product_base_drivers(vec![base_driver_1]); |
| |
| assert!(result.is_err()); |
| Ok(()) |
| } |
| |
| /// Helper to duplicate the first item in an Vec<T: Clone> and make it also |
| /// the last item. This intentionally panics if the Vec is empty. |
| fn duplicate_first<T: Clone>(vec: &mut Vec<T>) { |
| vec.push(vec.first().unwrap().clone()); |
| } |
| |
| #[test] |
| fn test_builder_catches_dupe_pkgs_in_aib() { |
| let temp = TempDir::new().unwrap(); |
| let root = Utf8Path::from_path(temp.path()).unwrap(); |
| |
| let mut aib = make_test_assembly_bundle(root, root); |
| duplicate_first(&mut aib.packages); |
| |
| let mut builder = ImageAssemblyConfigBuilder::new(BuildType::Eng); |
| assert!(builder.add_parsed_bundle(root, aib).is_err()); |
| } |
| |
| fn test_duplicates_across_aibs_impl< |
| T: Clone, |
| F: Fn(&mut AssemblyInputBundle) -> &mut Vec<T>, |
| >( |
| accessor: F, |
| ) { |
| let tmp = TempDir::new().unwrap(); |
| let outdir = Utf8Path::from_path(tmp.path()).unwrap(); |
| |
| let mut aib = make_test_assembly_bundle(outdir, outdir); |
| let mut second_aib = AssemblyInputBundle::default(); |
| |
| let first_list = (accessor)(&mut aib); |
| let second_list = (accessor)(&mut second_aib); |
| |
| // Clone the first item in the first AIB into the same list in the |
| // second AIB to create a duplicate item across the two AIBs. |
| let value = first_list.get(0).unwrap(); |
| second_list.push(value.clone()); |
| |
| let mut builder = ImageAssemblyConfigBuilder::new(BuildType::Eng); |
| builder.add_parsed_bundle(outdir, aib).unwrap(); |
| assert!(builder.add_parsed_bundle(outdir.join("second"), second_aib).is_err()); |
| } |
| |
| #[test] |
| fn test_builder_catches_dupe_pkgs_across_aibs() { |
| test_duplicates_across_aibs_impl(|a| &mut a.packages); |
| } |
| |
| fn assert_two_pkgs_same_name_diff_path_errors() { |
| let tmp = TempDir::new().unwrap(); |
| let outdir = Utf8Path::from_path(tmp.path()).unwrap(); |
| let tmp_path1 = TempDir::new_in(outdir).unwrap(); |
| let dir_path1 = Utf8Path::from_path(tmp_path1.path()).unwrap(); |
| let tmp_path2 = TempDir::new_in(outdir).unwrap(); |
| let dir_path2 = Utf8Path::from_path(tmp_path2.path()).unwrap(); |
| let aib = AssemblyInputBundle { |
| packages: vec![ |
| PackageDetails { |
| package: FileRelativePathBuf::FileRelative( |
| write_empty_pkg(dir_path1, "base_package2", None).into(), |
| ), |
| set: PackageSet::Base, |
| }, |
| PackageDetails { |
| package: FileRelativePathBuf::FileRelative( |
| write_empty_pkg(dir_path2, "base_package2", None).into(), |
| ), |
| set: PackageSet::Base, |
| }, |
| ], |
| ..Default::default() |
| }; |
| let mut builder = ImageAssemblyConfigBuilder::new(BuildType::Eng); |
| assert!(builder.add_parsed_bundle(outdir, aib).is_err()); |
| } |
| |
| #[test] |
| /// Asserts that attempting to add a package to the base package set with the same |
| /// PackageName but a different package manifest path will result in an error if coming |
| /// from the same AIB |
| fn test_builder_catches_same_name_diff_path_one_aib() { |
| assert_two_pkgs_same_name_diff_path_errors(); |
| } |
| |
| fn assert_two_pkgs_same_name_diff_path_across_aibs_errors() { |
| let tmp = TempDir::new().unwrap(); |
| let outdir = Utf8Path::from_path(tmp.path()).unwrap(); |
| let tmp_path1 = TempDir::new_in(outdir).unwrap(); |
| let dir_path1 = Utf8Path::from_path(tmp_path1.path()).unwrap(); |
| let tmp_path2 = TempDir::new_in(outdir).unwrap(); |
| let dir_path2 = Utf8Path::from_path(tmp_path2.path()).unwrap(); |
| let tools = FakeToolProvider::default(); |
| let aib = AssemblyInputBundle { |
| packages: vec![PackageDetails { |
| package: FileRelativePathBuf::FileRelative( |
| write_empty_pkg(dir_path1, "base_package2", None).into(), |
| ), |
| set: PackageSet::Base, |
| }], |
| ..Default::default() |
| }; |
| |
| let aib2 = AssemblyInputBundle { |
| packages: vec![PackageDetails { |
| package: FileRelativePathBuf::FileRelative( |
| write_empty_pkg(dir_path2, "base_package2", None).into(), |
| ), |
| set: PackageSet::Base, |
| }], |
| ..Default::default() |
| }; |
| |
| let mut builder = ImageAssemblyConfigBuilder::new(BuildType::Eng); |
| builder.add_parsed_bundle(outdir, aib).ok(); |
| builder.add_parsed_bundle(outdir, aib2).ok(); |
| assert!(builder.build(outdir, &tools).is_err()); |
| } |
| /// Asserts that attempting to add a package to the base package set with the same |
| /// PackageName but a different package manifest path will result in an error if coming |
| /// from DIFFERENT AIBs |
| #[test] |
| fn test_builder_catches_same_name_diff_path_multi_aib() { |
| assert_two_pkgs_same_name_diff_path_across_aibs_errors(); |
| } |
| |
| #[test] |
| fn test_builder_catches_dupes_across_package_sets() { |
| let tmp = TempDir::new().unwrap(); |
| let outdir = Utf8Path::from_path(tmp.path()).unwrap(); |
| let tmp_path1 = TempDir::new_in(outdir).unwrap(); |
| let dir_path1 = Utf8Path::from_path(tmp_path1.path()).unwrap(); |
| let tmp_path2 = TempDir::new_in(outdir).unwrap(); |
| let dir_path2 = Utf8Path::from_path(tmp_path2.path()).unwrap(); |
| let aib = AssemblyInputBundle { |
| packages: vec![PackageDetails { |
| package: FileRelativePathBuf::FileRelative( |
| write_empty_pkg(dir_path1, "foo", None).into(), |
| ), |
| set: PackageSet::Base, |
| }], |
| ..Default::default() |
| }; |
| |
| let aib2 = AssemblyInputBundle { |
| packages: vec![PackageDetails { |
| package: FileRelativePathBuf::FileRelative( |
| write_empty_pkg(dir_path2, "foo", None).into(), |
| ), |
| set: PackageSet::Cache, |
| }], |
| ..Default::default() |
| }; |
| let mut builder = ImageAssemblyConfigBuilder::new(BuildType::Eng); |
| builder.add_parsed_bundle(outdir, aib).ok(); |
| assert!(builder.add_parsed_bundle(outdir, aib2).is_err()); |
| } |
| |
| #[test] |
| fn test_builder_catches_dupe_config_data_across_aibs() { |
| let temp = TempDir::new().unwrap(); |
| let root = Utf8Path::from_path(temp.path()).unwrap(); |
| |
| let mut first_aib = make_test_assembly_bundle(root, root); |
| let mut second_aib = AssemblyInputBundle::default(); |
| |
| // Write the config data files. |
| std::fs::create_dir(root.join("second")).unwrap(); |
| |
| let config = root.join("config_data"); |
| let mut f = File::create(&config).unwrap(); |
| write!(&mut f, "config_data").unwrap(); |
| |
| let config = root.join("second/config_data"); |
| let mut f = File::create(&config).unwrap(); |
| write!(&mut f, "config_data2").unwrap(); |
| |
| let config_data_file_entry = |
| FileEntry { source: "config_data".into(), destination: "dest/file/path".into() }; |
| |
| first_aib.config_data.insert("base_package0".into(), vec![config_data_file_entry.clone()]); |
| second_aib.config_data.insert("base_package0".into(), vec![config_data_file_entry]); |
| |
| let mut builder = ImageAssemblyConfigBuilder::new(BuildType::Eng); |
| builder.add_parsed_bundle(root, first_aib).unwrap(); |
| assert!(builder.add_parsed_bundle(root.join("second"), second_aib).is_err()); |
| } |
| } |