| // 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::util; |
| use anyhow::{anyhow, ensure, Context, Result}; |
| use assembly_config::{ |
| self as image_assembly_config, |
| product_config::{AssemblyInputBundle, PackageConfigPatch, StructuredConfigPatches}, |
| FileEntry, |
| }; |
| use assembly_config_data::ConfigDataBuilder; |
| use assembly_structured_config::Repackager; |
| use assembly_util::{InsertAllUniqueExt, InsertUniqueExt, MapEntry}; |
| use fuchsia_pkg::PackageManifest; |
| use image_assembly_config::product_config::ProductPackagesConfig; |
| use std::path::Path; |
| use std::{ |
| collections::{BTreeMap, BTreeSet}, |
| path::PathBuf, |
| }; |
| |
| type ConfigDataMap = BTreeMap<String, FileEntryMap>; |
| |
| pub struct ImageAssemblyConfigBuilder { |
| /// The base packages from the AssemblyInputBundles |
| base: PackageSet, |
| |
| /// The cache packages from the AssemblyInputBundles |
| cache: PackageSet, |
| |
| /// The system packages from the AssemblyInputBundles |
| system: PackageSet, |
| |
| /// The bootfs packages from the AssemblyInputBundles |
| bootfs_packages: PackageSet, |
| |
| /// The boot_args from the AssemblyInputBundles |
| boot_args: BTreeSet<String>, |
| |
| /// The bootfs_files from the AssemblyInputBundles |
| bootfs_files: FileEntryMap, |
| |
| /// The config_data entries, by package and by destination path. |
| config_data: ConfigDataMap, |
| |
| /// Modifications that must be made to structured config within bootfs. |
| bootfs_structured_config: PackageConfigPatch, |
| |
| /// Modifications that must be made to structured config within packages. |
| structured_config: StructuredConfigPatches, |
| |
| kernel_path: Option<PathBuf>, |
| kernel_args: BTreeSet<String>, |
| kernel_clock_backstop: Option<u64>, |
| |
| qemu_kernel: Option<PathBuf>, |
| } |
| |
| impl Default for ImageAssemblyConfigBuilder { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| impl ImageAssemblyConfigBuilder { |
| pub fn new() -> Self { |
| Self { |
| base: PackageSet::new("base packages"), |
| cache: PackageSet::new("cache packages"), |
| system: PackageSet::new("system packages"), |
| bootfs_packages: PackageSet::new("bootfs packages"), |
| boot_args: BTreeSet::default(), |
| bootfs_files: FileEntryMap::new("bootfs files"), |
| config_data: ConfigDataMap::default(), |
| bootfs_structured_config: PackageConfigPatch::default(), |
| structured_config: StructuredConfigPatches::default(), |
| kernel_path: None, |
| kernel_args: BTreeSet::default(), |
| kernel_clock_backstop: None, |
| qemu_kernel: None, |
| } |
| } |
| |
| /// 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<Path>) -> Result<()> { |
| let bundle = util::read_config(bundle_path.as_ref())?; |
| |
| // Strip filename from bundle path. |
| let bundle_path = bundle_path.as_ref().parent().map(PathBuf::from).unwrap_or("".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<Path>, |
| bundle: AssemblyInputBundle, |
| ) -> Result<()> { |
| let bundle_path = bundle_path.as_ref(); |
| let AssemblyInputBundle { image_assembly: bundle, config_data, blobs: _ } = bundle; |
| |
| Self::add_bundle_packages(bundle_path, &bundle.base, &mut self.base)?; |
| Self::add_bundle_packages(bundle_path, &bundle.cache, &mut self.cache)?; |
| Self::add_bundle_packages(bundle_path, &bundle.system, &mut self.system)?; |
| Self::add_bundle_packages(bundle_path, &bundle.bootfs_packages, &mut self.bootfs_packages)?; |
| |
| self.boot_args |
| .try_insert_all_unique(bundle.boot_args) |
| .map_err(|arg| anyhow!("duplicate boot_arg found: {}", arg))?; |
| |
| for entry in Self::file_entry_paths_from(bundle_path, bundle.bootfs_files) { |
| self.bootfs_files.add_entry(entry)?; |
| } |
| |
| if let Some(kernel) = bundle.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))?; |
| |
| assembly_util::set_option_once_or( |
| &mut self.kernel_clock_backstop, |
| kernel.clock_backstop, |
| anyhow!("Only one input bundle can specify a kernel clock backstop"), |
| )?; |
| } |
| |
| for (package, entries) in config_data { |
| for entry in Self::file_entry_paths_from(bundle_path, entries) { |
| self.add_config_data_entry(&package, entry)?; |
| } |
| } |
| |
| assembly_util::set_option_once_or( |
| &mut self.qemu_kernel, |
| bundle.qemu_kernel.map(|p| bundle_path.join(p)), |
| anyhow!("Only one input bundle can specify a qemu kernel path"), |
| )?; |
| |
| Ok(()) |
| } |
| |
| /// 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<()> { |
| for p in &packages.base { |
| self.base.add_package_from_path(p)? |
| } |
| for p in &packages.cache { |
| self.cache.add_package_from_path(p)? |
| } |
| 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( |
| bundle_path: impl AsRef<Path>, |
| bundle_package_paths: &[impl AsRef<Path>], |
| package_set: &mut PackageSet, |
| ) -> Result<()> { |
| for path in bundle_package_paths { |
| let path = bundle_path.as_ref().join(path); |
| package_set.add_package_from_path(path)?; |
| } |
| Ok(()) |
| } |
| |
| fn file_entry_paths_from( |
| base: &Path, |
| entries: impl IntoIterator<Item = FileEntry>, |
| ) -> Vec<FileEntry> { |
| entries |
| .into_iter() |
| .map(|entry| FileEntry { |
| destination: entry.destination, |
| source: base.join(entry.source), |
| }) |
| .collect() |
| } |
| |
| /// 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) -> Result<()> { |
| self.config_data.entry(package.as_ref().into()).or_default().add_entry(entry) |
| } |
| |
| pub fn set_bootfs_structured_config(&mut self, config: PackageConfigPatch) { |
| self.bootfs_structured_config = config; |
| } |
| |
| /// Set the structured configuration updates for a package. Can only be called once per |
| /// package. |
| pub fn set_structured_config( |
| &mut self, |
| package: impl AsRef<str>, |
| config: PackageConfigPatch, |
| ) -> Result<()> { |
| if self.structured_config.insert(package.as_ref().to_owned(), config).is_none() { |
| Ok(()) |
| } else { |
| Err(anyhow::format_err!("duplicate config patch")) |
| } |
| } |
| |
| /// 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 this cannot create a completed ImageAssemblyConfig, it will return an error |
| /// instead. |
| pub fn build( |
| self, |
| outdir: impl AsRef<Path>, |
| ) -> Result<image_assembly_config::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 { |
| structured_config, |
| mut base, |
| mut cache, |
| mut system, |
| boot_args, |
| mut bootfs_files, |
| bootfs_packages, |
| bootfs_structured_config, |
| config_data, |
| kernel_path, |
| kernel_args, |
| kernel_clock_backstop, |
| qemu_kernel, |
| } = self; |
| |
| // add structured config value files to bootfs |
| let mut bootfs_repackager = Repackager::for_bootfs(&mut bootfs_files.entries, &outdir); |
| for (component, values) in bootfs_structured_config.components { |
| // check if we should try to configure the component before attempting so we can still |
| // return errors for other conditions like a missing config field or a wrong type |
| if bootfs_repackager.has_component(&component) { |
| bootfs_repackager.set_component_config(&component, values.fields)?; |
| } else { |
| // TODO(https://fxbug.dev/101556) return an error here |
| } |
| } |
| |
| // repackage any matching packages |
| for (package, config) in structured_config { |
| // get the manifest for this package name, returning the set from which it was removed |
| if let Some((manifest, source_package_set)) = |
| remove_package_from_sets(&package, [&mut base, &mut cache, &mut system]) |
| .with_context(|| format!("removing {} for repackaging", package))? |
| { |
| let outdir = outdir.join("repackaged").join(&package); |
| let mut repackager = Repackager::new(manifest, &outdir) |
| .with_context(|| format!("reading existing manifest for {}", package))?; |
| for (component, values) in &config.components { |
| repackager |
| .set_component_config(component, values.fields.clone()) |
| .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(new_path) |
| .with_context(|| format!("parsing repackaged {}", package))?; |
| source_package_set.insert(new_entry.name().to_owned(), new_entry); |
| } else { |
| // TODO(https://fxbug.dev/101556) return an error here |
| } |
| } |
| |
| if !config_data.is_empty() { |
| // Build the config_data package |
| let mut config_data_builder = ConfigDataBuilder::default(); |
| for (package_name, entries) in config_data { |
| for entry in entries.into_file_entries() { |
| config_data_builder.add_entry( |
| &package_name, |
| entry.destination.into(), |
| entry.source, |
| )?; |
| } |
| } |
| let manifest_path = config_data_builder |
| .build(&outdir) |
| .context("Writing the 'config_data' package metafar.")?; |
| let entry = PackageEntry::parse_from(manifest_path) |
| .context("parsing generated config-data package")?; |
| base.try_insert_unique(entry.name().to_owned(), entry).map_err(|_| { |
| anyhow!("found a duplicate config_data package when adding generated one.") |
| })?; |
| } |
| |
| // 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 partial = image_assembly_config::PartialImageAssemblyConfig { |
| system: system.into_paths().collect(), |
| base: base.into_paths().collect(), |
| cache: cache.into_paths().collect(), |
| kernel: Some(image_assembly_config::PartialKernelConfig { |
| path: kernel_path, |
| args: kernel_args.into_iter().collect(), |
| clock_backstop: kernel_clock_backstop, |
| }), |
| qemu_kernel, |
| boot_args: boot_args.into_iter().collect(), |
| bootfs_files: bootfs_files.into_file_entries(), |
| bootfs_packages: bootfs_packages.into_paths().collect(), |
| }; |
| |
| let image_assembly_config = image_assembly_config::ImageAssemblyConfig::try_from_partials( |
| std::iter::once(partial), |
| )?; |
| |
| Ok(image_assembly_config) |
| } |
| } |
| |
| /// Remove a package with a matching name from the provided package sets, returning its parsed |
| /// manifest and a mutable reference to the set from which it was removed. |
| fn remove_package_from_sets<'a, 'b: 'a, const N: usize>( |
| package_name: &str, |
| package_sets: [&'a mut PackageSet; N], |
| ) -> anyhow::Result<Option<(PackageManifest, &'a mut PackageSet)>> { |
| let mut matches_name = None; |
| |
| for package_set in package_sets { |
| if let Some(entry) = package_set.remove(package_name) { |
| ensure!( |
| matches_name.is_none(), |
| "only one package with a given name is allowed per product" |
| ); |
| matches_name = Some((entry.manifest, package_set)); |
| } |
| } |
| |
| Ok(matches_name) |
| } |
| |
| #[derive(Debug)] |
| struct PackageEntry { |
| path: PathBuf, |
| manifest: PackageManifest, |
| } |
| |
| impl PackageEntry { |
| fn parse_from(path: impl AsRef<Path>) -> Result<Self> { |
| let path = path.as_ref().to_owned(); |
| let manifest = PackageManifest::try_load_from(&path) |
| .context(format!("parsing {} as a package manifest", path.display()))?; |
| Ok(Self { path, manifest }) |
| } |
| |
| fn name(&self) -> &str { |
| self.manifest.name().as_ref() |
| } |
| } |
| |
| #[derive(Default, Debug)] |
| /// A named set of things, which are mapped by a String key. |
| |
| struct NamedMap<T> { |
| /// The name of the Map. |
| name: String, |
| |
| /// The entries in the map. |
| entries: BTreeMap<String, T>, |
| } |
| |
| impl<T> NamedMap<T> |
| where |
| T: std::fmt::Debug, |
| { |
| /// Create a new, named, map. |
| fn new(name: &str) -> Self { |
| Self { name: name.to_owned(), entries: BTreeMap::new() } |
| } |
| |
| fn try_insert_unique(&mut self, name: String, value: T) -> Result<()> { |
| let result = |
| self.entries.try_insert_unique(MapEntry(name, value)).map_err(|e| format!("{:?}", e)); |
| // The error is mapped a second time to separate the borrow of entries |
| // from the borrow of name. |
| result.map_err(|e| anyhow!("duplicate entry for {}: {}", self.name, e)) |
| } |
| } |
| |
| impl<T> std::ops::Deref for NamedMap<T> { |
| type Target = BTreeMap<String, T>; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.entries |
| } |
| } |
| |
| impl<T> std::ops::DerefMut for NamedMap<T> { |
| fn deref_mut(&mut self) -> &mut Self::Target { |
| &mut self.entries |
| } |
| } |
| |
| impl<T> IntoIterator for NamedMap<T> { |
| type Item = T; |
| |
| type IntoIter = std::collections::btree_map::IntoValues<String, Self::Item>; |
| |
| fn into_iter(self) -> Self::IntoIter { |
| self.entries.into_values() |
| } |
| } |
| |
| /// A named set of packages with their manifests parsed into memory, keyed by package name. |
| type PackageSet = NamedMap<PackageEntry>; |
| impl PackageSet { |
| /// Parse the given path as a PackageManifest, and add it to the PackageSet. |
| fn add_package_from_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> { |
| { |
| let entry = PackageEntry::parse_from(path)?; |
| self.try_insert_unique(entry.name().to_owned(), entry) |
| } |
| .with_context(|| format!("Adding package to set: {}", self.name)) |
| } |
| |
| /// Convert the PackageSet into an iterable collection of Paths. |
| fn into_paths(self) -> impl Iterator<Item = PathBuf> { |
| self.entries.into_values().map(|e| e.path) |
| } |
| } |
| |
| type FileEntryMap = NamedMap<PathBuf>; |
| impl FileEntryMap { |
| fn add_entry(&mut self, entry: FileEntry) -> Result<()> { |
| self.try_insert_unique(entry.destination, entry.source) |
| .with_context(|| format!("Adding entry to set: {}", self.name)) |
| } |
| |
| fn into_file_entries(self) -> Vec<FileEntry> { |
| self.entries |
| .into_iter() |
| .map(|(destination, source)| FileEntry { destination, source }) |
| .collect() |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use camino::Utf8PathBuf; |
| use fuchsia_pkg::{PackageBuilder, PackageManifest}; |
| use std::fs::File; |
| use tempfile::TempDir; |
| |
| fn write_empty_pkg(path: impl AsRef<Path>, name: &str) -> Utf8PathBuf { |
| let path = path.as_ref(); |
| let mut builder = PackageBuilder::new(name); |
| let manifest_path = path.join(name); |
| builder.manifest_path(&manifest_path); |
| builder.build(path, path.join(format!("{}_meta.far", name))).unwrap(); |
| Utf8PathBuf::from_path_buf(manifest_path).unwrap() |
| } |
| |
| fn make_test_assembly_bundle(bundle_path: &Path) -> AssemblyInputBundle { |
| let write_empty_bundle_pkg = |name: &str| write_empty_pkg(bundle_path, name).into(); |
| AssemblyInputBundle { |
| image_assembly: image_assembly_config::PartialImageAssemblyConfig { |
| base: vec![write_empty_bundle_pkg("base_package0")], |
| system: vec![write_empty_bundle_pkg("sys_package0")], |
| cache: vec![write_empty_bundle_pkg("cache_package0")], |
| bootfs_packages: vec![write_empty_bundle_pkg("bootfs_package0")], |
| kernel: Some(image_assembly_config::PartialKernelConfig { |
| path: Some("kernel/path".into()), |
| args: vec!["kernel_arg0".into()], |
| clock_backstop: Some(56244), |
| }), |
| qemu_kernel: Some("path/to/qemu/kernel".into()), |
| boot_args: vec!["boot_arg0".into()], |
| bootfs_files: vec![FileEntry { |
| source: "source/path/to/file".into(), |
| destination: "dest/file/path".into(), |
| }], |
| }, |
| config_data: BTreeMap::default(), |
| blobs: Vec::default(), |
| } |
| } |
| |
| #[test] |
| fn test_builder() { |
| let outdir = TempDir::new().unwrap(); |
| let mut builder = ImageAssemblyConfigBuilder::default(); |
| builder.add_parsed_bundle(outdir.path(), make_test_assembly_bundle(outdir.path())).unwrap(); |
| let result: image_assembly_config::ImageAssemblyConfig = builder.build(&outdir).unwrap(); |
| |
| assert_eq!(result.base, vec![outdir.path().join("base_package0")]); |
| assert_eq!(result.cache, vec![outdir.path().join("cache_package0")]); |
| assert_eq!(result.system, vec![outdir.path().join("sys_package0")]); |
| assert_eq!(result.bootfs_packages, vec![outdir.path().join("bootfs_package0")]); |
| assert_eq!(result.boot_args, vec!("boot_arg0".to_string())); |
| assert_eq!( |
| result.bootfs_files, |
| vec!(FileEntry { |
| source: outdir.path().join("source/path/to/file"), |
| destination: "dest/file/path".into() |
| }) |
| ); |
| |
| assert_eq!(result.kernel.path, outdir.path().join("kernel/path")); |
| assert_eq!(result.kernel.args, vec!("kernel_arg0".to_string())); |
| assert_eq!(result.kernel.clock_backstop, 56244); |
| assert_eq!(result.qemu_kernel, outdir.path().join("path/to/qemu/kernel")); |
| } |
| |
| #[test] |
| fn test_builder_with_config_data() { |
| let outdir = TempDir::new().unwrap(); |
| let mut builder = ImageAssemblyConfigBuilder::default(); |
| |
| // Write a file to the temp dir for use with config_data. |
| let bundle_path = outdir.path().join("bundle"); |
| let config_data_target_package_name = "base_package0"; |
| 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"); |
| std::fs::create_dir_all(&config_data_target_package_dir).unwrap(); |
| std::fs::write(&config_data_file_path, "configuration data").unwrap(); |
| |
| // Create an assembly bundle and add a config_data entry to it. |
| let mut bundle = make_test_assembly_bundle(&bundle_path); |
| bundle.config_data.insert( |
| config_data_target_package_name.to_string(), |
| vec![FileEntry { |
| source: config_data_file_path, |
| destination: "dest/file/path".to_owned(), |
| }], |
| ); |
| |
| builder.add_parsed_bundle(&bundle_path, bundle).unwrap(); |
| let result: image_assembly_config::ImageAssemblyConfig = builder.build(&outdir).unwrap(); |
| |
| // config_data's manifest is in outdir |
| let expected_config_data_manifest_path = |
| outdir.path().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(&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::Reader::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", config_data_target_package_name)) |
| .unwrap(); |
| |
| // 4. Validate its contents. |
| assert_eq!(config_file_data, "configuration data".as_bytes()); |
| } |
| |
| #[test] |
| fn test_builder_with_product_packages() { |
| let outdir = TempDir::new().unwrap(); |
| |
| let packages = ProductPackagesConfig { |
| base: vec![write_empty_pkg(&outdir, "base_a"), write_empty_pkg(&outdir, "base_b")], |
| cache: vec![write_empty_pkg(&outdir, "cache_a"), write_empty_pkg(&outdir, "cache_b")], |
| }; |
| let minimum_bundle = AssemblyInputBundle { |
| image_assembly: image_assembly_config::PartialImageAssemblyConfig { |
| base: vec![ |
| write_empty_pkg(&outdir, "platform_a").into_std_path_buf(), |
| write_empty_pkg(&outdir, "platform_b").into_std_path_buf(), |
| ], |
| kernel: Some(image_assembly_config::PartialKernelConfig { |
| path: Some("kernel/path".into()), |
| args: Vec::default(), |
| clock_backstop: Some(0), |
| }), |
| qemu_kernel: Some("kernel/qemu/path".into()), |
| ..image_assembly_config::PartialImageAssemblyConfig::default() |
| }, |
| config_data: BTreeMap::default(), |
| blobs: Vec::default(), |
| }; |
| let mut builder = ImageAssemblyConfigBuilder::default(); |
| builder.add_parsed_bundle(outdir.path().join("minimum_bundle"), minimum_bundle).unwrap(); |
| builder.add_product_packages(&packages).unwrap(); |
| let result: image_assembly_config::ImageAssemblyConfig = builder.build(&outdir).unwrap(); |
| |
| assert_eq!( |
| result.base, |
| ["base_a", "base_b", "platform_a", "platform_b"] |
| .iter() |
| .map(|p| outdir.path().join(p)) |
| .collect::<Vec<_>>() |
| ); |
| assert_eq!( |
| result.cache, |
| vec![outdir.path().join("cache_a"), outdir.path().join("cache_b")] |
| ); |
| } |
| |
| #[test] |
| fn test_builder_with_product_packages_catches_duplicates() { |
| let outdir = TempDir::new().unwrap(); |
| |
| let packages = ProductPackagesConfig { |
| base: vec![write_empty_pkg(&outdir, "base_a")], |
| ..ProductPackagesConfig::default() |
| }; |
| let minimum_bundle = AssemblyInputBundle { |
| image_assembly: image_assembly_config::PartialImageAssemblyConfig { |
| base: vec![write_empty_pkg(&outdir, "base_a").into_std_path_buf()], |
| kernel: Some(image_assembly_config::PartialKernelConfig { |
| path: Some("kernel/path".into()), |
| args: Vec::default(), |
| clock_backstop: Some(0), |
| }), |
| qemu_kernel: Some("kernel/qemu/path".into()), |
| ..image_assembly_config::PartialImageAssemblyConfig::default() |
| }, |
| config_data: BTreeMap::default(), |
| blobs: Vec::default(), |
| }; |
| let mut builder = ImageAssemblyConfigBuilder::default(); |
| builder.add_parsed_bundle(outdir.path().join("minimum_bundle"), minimum_bundle).unwrap(); |
| let result = builder.add_product_packages(&packages); |
| assert!(result.is_err()); |
| } |
| |
| /// 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_base_pkgs_in_aib() { |
| let temp = TempDir::new().unwrap(); |
| let mut aib = make_test_assembly_bundle(temp.path()); |
| duplicate_first(&mut aib.image_assembly.base); |
| |
| let mut builder = ImageAssemblyConfigBuilder::default(); |
| assert!(builder.add_parsed_bundle(temp.path(), aib).is_err()); |
| } |
| |
| #[test] |
| fn test_builder_catches_dupe_cache_pkgs_in_aib() { |
| let temp = TempDir::new().unwrap(); |
| let mut aib = make_test_assembly_bundle(temp.path()); |
| duplicate_first(&mut aib.image_assembly.cache); |
| |
| let mut builder = ImageAssemblyConfigBuilder::default(); |
| assert!(builder.add_parsed_bundle(temp.path(), aib).is_err()); |
| } |
| |
| #[test] |
| fn test_builder_catches_dupe_system_pkgs_in_aib() { |
| let temp = TempDir::new().unwrap(); |
| let mut aib = make_test_assembly_bundle(temp.path()); |
| duplicate_first(&mut aib.image_assembly.system); |
| |
| let mut builder = ImageAssemblyConfigBuilder::default(); |
| assert!(builder.add_parsed_bundle(temp.path(), aib).is_err()); |
| } |
| |
| #[test] |
| fn test_builder_catches_dupe_bootfs_pkgs_in_aib() { |
| let temp = TempDir::new().unwrap(); |
| let mut aib = make_test_assembly_bundle(temp.path()); |
| duplicate_first(&mut aib.image_assembly.bootfs_packages); |
| |
| let mut builder = ImageAssemblyConfigBuilder::default(); |
| assert!(builder.add_parsed_bundle(temp.path(), aib).is_err()); |
| } |
| |
| fn test_duplicates_across_aibs_impl< |
| T: Clone, |
| F: Fn(&mut AssemblyInputBundle) -> &mut Vec<T>, |
| >( |
| accessor: F, |
| ) { |
| let outdir = TempDir::new().unwrap(); |
| let mut aib = make_test_assembly_bundle(outdir.path()); |
| 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::default(); |
| builder.add_parsed_bundle(outdir.path(), aib).unwrap(); |
| assert!(builder.add_parsed_bundle(outdir.path().join("second"), second_aib).is_err()); |
| } |
| |
| #[test] |
| #[ignore] // As packages from different bundles have different paths, |
| // this isn't currently working |
| fn test_builder_catches_dupe_base_pkgs_across_aibs() { |
| test_duplicates_across_aibs_impl(|a| &mut a.image_assembly.base); |
| } |
| |
| #[test] |
| #[ignore] // As packages from different bundles have different paths, |
| // this isn't currently working |
| fn test_builder_catches_dupe_cache_pkgs_across_aibs() { |
| test_duplicates_across_aibs_impl(|a| &mut a.image_assembly.cache); |
| } |
| |
| #[test] |
| #[ignore] // As packages from different bundles have different paths, |
| // this isn't currently working |
| fn test_builder_catches_dupe_system_pkgs_across_aibs() { |
| test_duplicates_across_aibs_impl(|a| &mut a.image_assembly.system); |
| } |
| |
| #[test] |
| fn test_builder_catches_dupe_bootfs_files_across_aibs() { |
| test_duplicates_across_aibs_impl(|a| &mut a.image_assembly.bootfs_files); |
| } |
| |
| #[test] |
| fn test_builder_catches_dupe_config_data_across_aibs() { |
| let temp = TempDir::new().unwrap(); |
| let mut first_aib = make_test_assembly_bundle(temp.path()); |
| let mut second_aib = AssemblyInputBundle::default(); |
| |
| let config_data_file_entry = FileEntry { |
| source: "source/path/to/file".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::default(); |
| builder.add_parsed_bundle(temp.path(), first_aib).unwrap(); |
| assert!(builder.add_parsed_bundle(temp.path().join("second"), second_aib).is_err()); |
| } |
| } |