blob: 9a9cb5fdfad3d84616b7e192805474dee3139aeb [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! FFX plugin for constructing product bundles, which are distributable containers for a product's
//! images and packages, and can be used to emulate, flash, or update a product.
use anyhow::{bail, Context, Result};
use assembly_manifest::{AssemblyManifest, BlobfsContents, Image, PackagesMetadata};
use assembly_partitions_config::{PartitionImageMapper, PartitionsConfig, Slot as PartitionSlot};
use assembly_tool::{SdkToolProvider, ToolProvider};
use assembly_update_package::{Slot, UpdatePackageBuilder};
use assembly_update_packages_manifest::UpdatePackagesManifest;
use camino::{Utf8Path, Utf8PathBuf};
use epoch::EpochFile;
use ffx_config::sdk::{in_tree_sdk_version, SdkVersion};
use ffx_core::ffx_plugin;
use ffx_fastboot::manifest::FlashManifestVersion;
use ffx_product_create_args::CreateCommand;
use fuchsia_pkg::PackageManifest;
use fuchsia_repo::{
repo_builder::RepoBuilder, repo_keys::RepoKeys, repository::FileSystemRepository,
};
use sdk_metadata::{
ProductBundle, ProductBundleV2, Repository, VirtualDevice, VirtualDeviceManifest,
};
use std::fs::File;
use tempfile::TempDir;
/// Default delivery blob type to use for products.
const DEFAULT_DELIVERY_BLOB_TYPE: u32 = 1;
/// Create a product bundle.
#[ffx_plugin()]
pub async fn pb_create(cmd: CreateCommand) -> Result<()> {
let sdk = ffx_config::global_env_context()
.context("loading global environment context")?
.get_sdk()
.await
.context("getting sdk env context")?;
let sdk_version = match sdk.get_version() {
SdkVersion::Version(version) => version.to_string(),
SdkVersion::InTree => in_tree_sdk_version(),
SdkVersion::Unknown => bail!("Unable to determine SDK version"),
};
let tools = SdkToolProvider::try_new()?;
pb_create_with_sdk_version(cmd, &sdk_version, Box::new(tools)).await
}
/// Create a product bundle using the provided sdk.
pub async fn pb_create_with_sdk_version(
cmd: CreateCommand,
sdk_version: &str,
tools: Box<dyn ToolProvider>,
) -> Result<()> {
// We build an update package if `update_version_file` or `update_epoch` is provided.
// If we decide to build an update package, we need to ensure that both of them
// are provided.
let update_details =
if cmd.update_package_version_file.is_some() || cmd.update_package_epoch.is_some() {
if cmd.tuf_keys.is_none() {
anyhow::bail!("TUF keys must be provided to build an update package");
}
let version = cmd.update_package_version_file.ok_or(anyhow::anyhow!(
"A version file must be provided to build an update package"
))?;
let epoch = cmd
.update_package_epoch
.ok_or(anyhow::anyhow!("A epoch must be provided to build an update package"))?;
Some((version, epoch))
} else {
None
};
// Make sure `out_dir` is created and empty.
if cmd.out_dir.exists() {
if cmd.out_dir == "" || cmd.out_dir == "/" {
anyhow::bail!("Avoiding deletion of an unsafe out directory: {}", &cmd.out_dir);
}
std::fs::remove_dir_all(&cmd.out_dir).context("Deleting the out_dir")?;
}
std::fs::create_dir_all(&cmd.out_dir).context("Creating the out_dir")?;
let partitions = load_partitions_config(&cmd.partitions, &cmd.out_dir.join("partitions"))?;
let (system_a, packages_a) =
load_assembly_manifest(&cmd.system_a, &cmd.out_dir.join("system_a"))?;
let (system_b, _packages_b) =
load_assembly_manifest(&cmd.system_b, &cmd.out_dir.join("system_b"))?;
let (system_r, packages_r) =
load_assembly_manifest(&cmd.system_r, &cmd.out_dir.join("system_r"))?;
// Generate a size report for the images after mapping them to partitions.
if let Some(size_report) = cmd.gerrit_size_report {
let mut mapper = PartitionImageMapper::new(partitions.clone());
if let Some(system) = &system_a {
mapper.map_images_to_slot(&system.images, PartitionSlot::A);
}
if let Some(system) = &system_b {
mapper.map_images_to_slot(&system.images, PartitionSlot::B);
}
if let Some(system) = &system_r {
mapper.map_images_to_slot(&system.images, PartitionSlot::R);
}
mapper
.generate_gerrit_size_report(&size_report, &cmd.product_name)
.context("Generating image size report")?;
}
// Generate the update packages if necessary.
let (_gen_dir, update_package_hash, update_packages) =
if let Some((version, epoch)) = update_details {
let epoch: EpochFile = EpochFile::Version1 { epoch };
let gen_dir = TempDir::new().context("creating temporary directory")?;
let mut builder = UpdatePackageBuilder::new(
partitions.clone(),
partitions.hardware_revision.clone(),
version,
epoch,
Utf8Path::from_path(gen_dir.path())
.context("checking if temporary directory is UTF-8")?,
);
let mut all_packages = UpdatePackagesManifest::default();
for (_path, package) in &packages_a {
all_packages.add_by_manifest(&package)?;
}
builder.add_packages(all_packages);
if let Some(manifest) = &system_a {
builder.add_slot_images(Slot::Primary(manifest.clone()));
}
if let Some(manifest) = &system_r {
builder.add_slot_images(Slot::Recovery(manifest.clone()));
}
let update_package = builder.build(tools)?;
(Some(gen_dir), Some(update_package.merkle), update_package.package_manifests)
} else {
(None, None, vec![])
};
let repositories = if let Some(tuf_keys) = &cmd.tuf_keys {
let repo_path = &cmd.out_dir;
let main_metadata_path = repo_path.join("repository");
let recovery_metadata_path = repo_path.join("recovery_repository");
let blobs_path = repo_path.join("blobs");
let keys_path = repo_path.join("keys");
let delivery_blob_type = cmd.delivery_blob_type.unwrap_or(DEFAULT_DELIVERY_BLOB_TYPE);
let repo_keys =
RepoKeys::from_dir(tuf_keys.as_std_path()).context("Gathering repo keys")?;
// Main slot.
let repo = FileSystemRepository::builder(
main_metadata_path.to_path_buf(),
blobs_path.to_path_buf(),
)
.delivery_blob_type(delivery_blob_type.try_into()?)
.build();
RepoBuilder::create(&repo, &repo_keys)
.add_package_manifests(packages_a.into_iter())
.await?
.add_package_manifests(update_packages.into_iter().map(|manifest| (None, manifest)))
.await?
.commit()
.await
.context("Building the repo")?;
// Recovery slot.
// We currently need this for scrutiny to find the recovery blobs.
let recovery_repo = FileSystemRepository::builder(
recovery_metadata_path.to_path_buf(),
blobs_path.to_path_buf(),
)
.delivery_blob_type(delivery_blob_type.try_into()?)
.build();
RepoBuilder::create(&recovery_repo, &repo_keys)
.add_package_manifests(packages_r.into_iter())
.await?
.commit()
.await
.context("Building the recovery repo")?;
std::fs::create_dir_all(&keys_path).context("Creating keys directory")?;
// We intentionally do not add the recovery repository, because no tools currently need
// it. Scrutiny needs the recovery blobs to be accessible, but that's it.
vec![Repository {
name: "fuchsia.com".into(),
metadata_path: main_metadata_path,
blobs_path: blobs_path,
delivery_blob_type,
root_private_key_path: copy_file(tuf_keys.join("root.json"), &keys_path).ok(),
targets_private_key_path: copy_file(tuf_keys.join("targets.json"), &keys_path).ok(),
snapshot_private_key_path: copy_file(tuf_keys.join("snapshot.json"), &keys_path).ok(),
timestamp_private_key_path: copy_file(tuf_keys.join("timestamp.json"), &keys_path).ok(),
}]
} else {
vec![]
};
// Add the virtual devices, and determine the path to the manifest.
let virtual_devices_path = if cmd.virtual_device.is_empty() {
None
} else {
// Prepare a manifest for the virtual devices.
let mut manifest = VirtualDeviceManifest::default();
// Create the virtual_devices directory.
let vd_dir = cmd.out_dir.join("virtual_devices");
std::fs::create_dir_all(&vd_dir).context("Creating the virtual_devices directory.")?;
for path in cmd.virtual_device {
let device = VirtualDevice::try_load_from(&path)
.with_context(|| format!("Parsing file as virtual device: '{}'", path))?;
// Write the virtual device to the directory.
let device_file_name =
path.file_name().unwrap_or_else(|| panic!("Path has no file name: '{}'", path));
let device_file_name = Utf8PathBuf::from(device_file_name);
let path_in_pb = vd_dir.join(&device_file_name);
device
.write(&path_in_pb)
.with_context(|| format!("Writing virtual device: {}", path_in_pb))?;
// Add the virtual device to the manifest.
let name = device.name().to_string();
manifest.device_paths.insert(name, device_file_name);
}
// Write the manifest into the directory.
manifest.recommended = cmd.recommended_device;
let manifest_path = vd_dir.join("manifest.json");
let manifest_file = File::create(&manifest_path)
.with_context(|| format!("Couldn't create manifest file '{}'", manifest_path))?;
serde_json::to_writer(manifest_file, &manifest)
.context("Couldn't serialize manifest to disk.")?;
Some(manifest_path)
};
let product_name = cmd.product_name.to_owned();
let product_version = cmd.product_version.to_owned();
let product_bundle = ProductBundleV2 {
product_name,
product_version,
partitions,
sdk_version: sdk_version.to_string(),
system_a: system_a.map(|s| s.images),
system_b: system_b.map(|s| s.images),
system_r: system_r.map(|s| s.images),
repositories,
update_package_hash,
virtual_devices_path,
};
let product_bundle = ProductBundle::V2(product_bundle);
product_bundle.write(&cmd.out_dir).context("writing product bundle")?;
if cmd.with_deprecated_flash_manifest {
let manifest_path = cmd.out_dir.join("flash.json");
let flash_manifest_file = File::create(&manifest_path)
.with_context(|| format!("Couldn't create flash.json '{}'", manifest_path))?;
FlashManifestVersion::from_product_bundle(&product_bundle)?.write(flash_manifest_file)?
}
Ok(())
}
/// Open and parse a PartitionsConfig from a path, copying the images into `out_dir`.
fn load_partitions_config(
path: impl AsRef<Utf8Path>,
out_dir: impl AsRef<Utf8Path>,
) -> Result<PartitionsConfig> {
let path = path.as_ref();
let out_dir = out_dir.as_ref();
// Make sure `out_dir` is created.
std::fs::create_dir_all(&out_dir).context("Creating the out_dir")?;
let partitions_file = File::open(path).context("Opening partitions config")?;
let mut config: PartitionsConfig =
serde_json::from_reader(partitions_file).context("Parsing partitions config")?;
for cred in &mut config.unlock_credentials {
*cred = copy_file(&cred, &out_dir)?;
}
for bootstrap in &mut config.bootstrap_partitions {
bootstrap.image = copy_file(&bootstrap.image, &out_dir)?;
}
for bootloader in &mut config.bootloader_partitions {
bootloader.image = copy_file(&bootloader.image, &out_dir)?;
}
Ok(config)
}
/// Open and parse an AssemblyManifest from a path, copying the images into `out_dir`.
/// Returns None if the given path is None.
fn load_assembly_manifest(
path: &Option<Utf8PathBuf>,
out_dir: impl AsRef<Utf8Path>,
) -> Result<(Option<AssemblyManifest>, Vec<(Option<Utf8PathBuf>, PackageManifest)>)> {
let out_dir = out_dir.as_ref();
if let Some(path) = path {
// Make sure `out_dir` is created.
std::fs::create_dir_all(&out_dir).context("Creating the out_dir")?;
let manifest = AssemblyManifest::try_load_from(path)
.with_context(|| format!("Loading assembly manifest: {}", path))?;
// Filter out the base package, and the blobfs contents.
let mut images = Vec::new();
let mut packages = Vec::new();
let mut extract_packages = |packages_metadata| -> Result<()> {
let PackagesMetadata { base, cache } = packages_metadata;
let all_packages = [base.0, cache.0].concat();
for package in all_packages {
let manifest = PackageManifest::try_load_from(&package.manifest)
.with_context(|| format!("reading package manifest: {}", package.manifest))?;
packages.push((Some(package.manifest), manifest));
}
Ok(())
};
let mut has_zbi = false;
let mut has_vbmeta = false;
for image in manifest.images.into_iter() {
match image {
Image::BasePackage(..) => {}
Image::Fxfs { path, contents } => {
extract_packages(contents.packages)?;
images.push(Image::Fxfs { path, contents: BlobfsContents::default() });
}
Image::BlobFS { path, contents } => {
extract_packages(contents.packages)?;
images.push(Image::BlobFS { path, contents: BlobfsContents::default() });
}
Image::ZBI { .. } => {
if has_zbi {
anyhow::bail!("Found more than one ZBI");
}
images.push(image);
has_zbi = true;
}
Image::VBMeta(_) => {
if has_vbmeta {
anyhow::bail!("Found more than one VBMeta");
}
images.push(image);
has_vbmeta = true;
}
// We don't need to extract packages from `FxfsSparse`, since it exists only if
// `Fxfs` also exists (and always contains the same set of packages).
Image::FxfsSparse { .. }
| Image::FVM(_)
| Image::FVMSparse(_)
| Image::FVMFastboot(_)
| Image::QemuKernel(_) => {
images.push(image);
}
}
}
// Copy the images to the `out_dir`.
let mut new_images = Vec::<Image>::new();
for mut image in images.into_iter() {
let dest = copy_file(image.source(), &out_dir)?;
image.set_source(dest);
new_images.push(image);
}
Ok((Some(AssemblyManifest { images: new_images }), packages))
} else {
Ok((None, vec![]))
}
}
/// Copy a file from `source` to `out_dir` preserving the filename.
/// Returns the destination, which is equal to {out_dir}{filename}.
fn copy_file(source: impl AsRef<Utf8Path>, out_dir: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {
let source = source.as_ref();
let out_dir = out_dir.as_ref();
let filename = source.file_name().context("getting file name")?;
let destination = out_dir.join(filename);
// Attempt to hardlink, if that fails, fall back to copying.
if let Err(_) = std::fs::hard_link(source, &destination) {
// falling back to copying.
std::fs::copy(source, &destination)
.with_context(|| format!("copying file '{}'", source))?;
}
Ok(destination)
}
#[cfg(test)]
mod test {
use super::*;
use assembly_tool::testing::{blobfs_side_effect, FakeToolProvider};
use fuchsia_repo::test_utils;
use sdk_metadata::VirtualDeviceV1;
use std::io::Write;
const VIRTUAL_DEVICE_VALID: &str =
include_str!("../../../../../../../build/sdk/meta/test_data/virtual_device.json");
#[test]
fn test_copy_file() {
let temp1 = TempDir::new().unwrap();
let tempdir1 = Utf8Path::from_path(temp1.path()).unwrap();
let temp2 = TempDir::new().unwrap();
let tempdir2 = Utf8Path::from_path(temp2.path()).unwrap();
let source_path = tempdir1.join("source.txt");
let mut source_file = File::create(&source_path).unwrap();
write!(source_file, "contents").unwrap();
let destination = copy_file(&source_path, tempdir2).unwrap();
assert!(destination.exists());
}
#[test]
fn test_load_partitions_config() {
let temp = TempDir::new().unwrap();
let tempdir = Utf8Path::from_path(temp.path()).unwrap();
let pb_dir = tempdir.join("pb");
let config_path = tempdir.join("config.json");
let config_file = File::create(&config_path).unwrap();
serde_json::to_writer(&config_file, &PartitionsConfig::default()).unwrap();
let error_path = tempdir.join("error.json");
let mut error_file = File::create(&error_path).unwrap();
error_file.write_all("error".as_bytes()).unwrap();
let parsed = load_partitions_config(&config_path, &pb_dir);
assert!(parsed.is_ok());
let error = load_partitions_config(&error_path, &pb_dir);
assert!(error.is_err());
}
#[test]
fn test_load_assembly_manifest() {
let temp = TempDir::new().unwrap();
let tempdir = Utf8Path::from_path(temp.path()).unwrap();
let pb_dir = tempdir.join("pb");
let manifest_path = tempdir.join("manifest.json");
AssemblyManifest::default().write(&manifest_path).unwrap();
let error_path = tempdir.join("error.json");
let mut error_file = File::create(&error_path).unwrap();
error_file.write_all("error".as_bytes()).unwrap();
let (parsed, packages) = load_assembly_manifest(&Some(manifest_path), &pb_dir).unwrap();
assert!(parsed.is_some());
assert_eq!(packages, Vec::new());
let error = load_assembly_manifest(&Some(error_path), &pb_dir);
assert!(error.is_err());
let (none, _) = load_assembly_manifest(&None, &pb_dir).unwrap();
assert!(none.is_none());
}
#[fuchsia::test]
async fn test_pb_create_minimal() {
let temp = TempDir::new().unwrap();
let tempdir = Utf8Path::from_path(temp.path()).unwrap();
let pb_dir = tempdir.join("pb");
let partitions_path = tempdir.join("partitions.json");
let partitions_file = File::create(&partitions_path).unwrap();
serde_json::to_writer(&partitions_file, &PartitionsConfig::default()).unwrap();
let tool_provider = Box::new(FakeToolProvider::new_with_side_effect(blobfs_side_effect));
pb_create_with_sdk_version(
CreateCommand {
product_name: String::default(),
product_version: String::default(),
partitions: partitions_path,
system_a: None,
system_b: None,
system_r: None,
tuf_keys: None,
update_package_version_file: None,
update_package_epoch: None,
virtual_device: vec![],
recommended_device: None,
out_dir: pb_dir.clone(),
delivery_blob_type: None,
with_deprecated_flash_manifest: false,
gerrit_size_report: None,
},
/*sdk_version=*/ "",
tool_provider,
)
.await
.unwrap();
let pb = ProductBundle::try_load_from(pb_dir).unwrap();
assert_eq!(
pb,
ProductBundle::V2(ProductBundleV2 {
product_name: String::default(),
product_version: String::default(),
partitions: PartitionsConfig::default(),
sdk_version: String::default(),
system_a: None,
system_b: None,
system_r: None,
repositories: vec![],
update_package_hash: None,
virtual_devices_path: None,
})
);
}
#[fuchsia::test]
async fn test_pb_create_a_and_r() {
let temp = TempDir::new().unwrap();
let tempdir = Utf8Path::from_path(temp.path()).unwrap();
let pb_dir = tempdir.join("pb");
let partitions_path = tempdir.join("partitions.json");
let partitions_file = File::create(&partitions_path).unwrap();
serde_json::to_writer(&partitions_file, &PartitionsConfig::default()).unwrap();
let system_path = tempdir.join("system.json");
AssemblyManifest::default().write(&system_path).unwrap();
let tool_provider = Box::new(FakeToolProvider::new_with_side_effect(blobfs_side_effect));
pb_create_with_sdk_version(
CreateCommand {
product_name: String::default(),
product_version: String::default(),
partitions: partitions_path,
system_a: Some(system_path.clone()),
system_b: None,
system_r: Some(system_path.clone()),
tuf_keys: None,
update_package_version_file: None,
update_package_epoch: None,
virtual_device: vec![],
recommended_device: None,
out_dir: pb_dir.clone(),
delivery_blob_type: None,
with_deprecated_flash_manifest: false,
gerrit_size_report: None,
},
/*sdk_version=*/ "",
tool_provider,
)
.await
.unwrap();
let pb = ProductBundle::try_load_from(pb_dir).unwrap();
assert_eq!(
pb,
ProductBundle::V2(ProductBundleV2 {
product_name: String::default(),
product_version: String::default(),
partitions: PartitionsConfig::default(),
sdk_version: String::default(),
system_a: Some(vec![]),
system_b: None,
system_r: Some(vec![]),
repositories: vec![],
update_package_hash: None,
virtual_devices_path: None,
})
);
}
#[fuchsia::test]
async fn test_pb_create_a_and_r_with_multiple_zbi() {
let temp = TempDir::new().unwrap();
let tempdir = Utf8Path::from_path(temp.path()).unwrap();
let pb_dir = tempdir.join("pb");
let partitions_path = tempdir.join("partitions.json");
let partitions_file = File::create(&partitions_path).unwrap();
serde_json::to_writer(&partitions_file, &PartitionsConfig::default()).unwrap();
let system_path = tempdir.join("system.json");
let mut manifest = AssemblyManifest::default();
manifest.images = vec![
Image::ZBI { path: Utf8PathBuf::from("path1"), signed: false },
Image::ZBI { path: Utf8PathBuf::from("path2"), signed: true },
];
manifest.write(&system_path).unwrap();
let tool_provider = Box::new(FakeToolProvider::new_with_side_effect(blobfs_side_effect));
assert!(pb_create_with_sdk_version(
CreateCommand {
product_name: String::default(),
product_version: String::default(),
partitions: partitions_path,
system_a: Some(system_path.clone()),
system_b: None,
system_r: Some(system_path.clone()),
tuf_keys: None,
update_package_version_file: None,
update_package_epoch: None,
virtual_device: vec![],
recommended_device: None,
out_dir: pb_dir.clone(),
delivery_blob_type: None,
with_deprecated_flash_manifest: false,
gerrit_size_report: None,
},
/*sdk_version=*/ "",
tool_provider,
)
.await
.is_err());
}
#[fuchsia::test]
async fn test_pb_create_a_and_r_and_repository() {
let temp = TempDir::new().unwrap();
let tempdir = Utf8Path::from_path(temp.path()).unwrap().canonicalize_utf8().unwrap();
let pb_dir = tempdir.join("pb");
let partitions_path = tempdir.join("partitions.json");
let partitions_file = File::create(&partitions_path).unwrap();
serde_json::to_writer(&partitions_file, &PartitionsConfig::default()).unwrap();
let system_path = tempdir.join("system.json");
AssemblyManifest::default().write(&system_path).unwrap();
let tuf_keys = tempdir.join("keys");
test_utils::make_repo_keys_dir(&tuf_keys);
let tool_provider = Box::new(FakeToolProvider::new_with_side_effect(blobfs_side_effect));
pb_create_with_sdk_version(
CreateCommand {
product_name: String::default(),
product_version: String::default(),
partitions: partitions_path,
system_a: Some(system_path.clone()),
system_b: None,
system_r: Some(system_path.clone()),
tuf_keys: Some(tuf_keys),
update_package_version_file: None,
update_package_epoch: None,
virtual_device: vec![],
recommended_device: None,
out_dir: pb_dir.clone(),
delivery_blob_type: Some(1),
with_deprecated_flash_manifest: false,
gerrit_size_report: None,
},
/*sdk_version=*/ "",
tool_provider,
)
.await
.unwrap();
let pb = ProductBundle::try_load_from(&pb_dir).unwrap();
assert_eq!(
pb,
ProductBundle::V2(ProductBundleV2 {
product_name: String::default(),
product_version: String::default(),
partitions: PartitionsConfig::default(),
sdk_version: String::default(),
system_a: Some(AssemblyManifest::default().images),
system_b: None,
system_r: Some(AssemblyManifest::default().images),
repositories: vec![Repository {
name: "fuchsia.com".into(),
metadata_path: pb_dir.join("repository"),
blobs_path: pb_dir.join("blobs"),
delivery_blob_type: 1,
root_private_key_path: Some(pb_dir.join("keys/root.json")),
targets_private_key_path: Some(pb_dir.join("keys/targets.json")),
snapshot_private_key_path: Some(pb_dir.join("keys/snapshot.json")),
timestamp_private_key_path: Some(pb_dir.join("keys/timestamp.json")),
}],
update_package_hash: None,
virtual_devices_path: None,
})
);
}
#[fuchsia::test]
async fn test_pb_create_with_update() {
let tmp = TempDir::new().unwrap();
let tempdir = Utf8Path::from_path(tmp.path()).unwrap().canonicalize_utf8().unwrap();
let pb_dir = tempdir.join("pb");
let partitions_path = tempdir.join("partitions.json");
let partitions_file = File::create(&partitions_path).unwrap();
serde_json::to_writer(&partitions_file, &PartitionsConfig::default()).unwrap();
let version_path = tempdir.join("version.txt");
std::fs::write(&version_path, "").unwrap();
let tuf_keys = tempdir.join("keys");
test_utils::make_repo_keys_dir(&tuf_keys);
let tool_provider = Box::new(FakeToolProvider::new_with_side_effect(blobfs_side_effect));
pb_create_with_sdk_version(
CreateCommand {
product_name: String::default(),
product_version: String::default(),
partitions: partitions_path,
system_a: None,
system_b: None,
system_r: None,
tuf_keys: Some(tuf_keys),
update_package_version_file: Some(version_path),
update_package_epoch: Some(1),
virtual_device: vec![],
recommended_device: None,
out_dir: pb_dir.clone(),
delivery_blob_type: None,
with_deprecated_flash_manifest: false,
gerrit_size_report: None,
},
/*sdk_version=*/ "",
tool_provider,
)
.await
.unwrap();
let pb = ProductBundle::try_load_from(&pb_dir).unwrap();
// NB: do not assert on the package hash because this test is not hermetic; platform
// changes such as API level bumps may change the package hash and such changes are
// immaterial to the code under test here.
assert_matches::assert_matches!(
pb,
ProductBundle::V2(ProductBundleV2 {
product_name: _,
product_version: _,
partitions,
sdk_version: _,
system_a: None,
system_b: None,
system_r: None,
repositories,
update_package_hash: Some(_),
virtual_devices_path: None,
}) if partitions == Default::default() && repositories == &[Repository {
name: "fuchsia.com".into(),
metadata_path: pb_dir.join("repository"),
blobs_path: pb_dir.join("blobs"),
delivery_blob_type: DEFAULT_DELIVERY_BLOB_TYPE,
root_private_key_path: Some(pb_dir.join("keys/root.json")),
targets_private_key_path: Some(pb_dir.join("keys/targets.json")),
snapshot_private_key_path: Some(pb_dir.join("keys/snapshot.json")),
timestamp_private_key_path: Some(pb_dir.join("keys/timestamp.json")),
}]
);
}
#[fuchsia::test]
async fn test_pb_create_with_virtual_devices() -> Result<()> {
let temp = TempDir::new().unwrap();
let tempdir = Utf8Path::from_path(temp.path()).unwrap().canonicalize_utf8().unwrap();
let pb_dir = tempdir.join("pb");
let partitions_path = tempdir.join("partitions.json");
let partitions_file = File::create(&partitions_path)?;
serde_json::to_writer(&partitions_file, &PartitionsConfig::default())?;
let vd_path1 = tempdir.join("device_1.json");
let vd_path2 = tempdir.join("device_2.json");
let template_path = tempdir.join("device_1.json.template");
let mut vd_file1 = File::create(&vd_path1)?;
let mut vd_file2 = File::create(&vd_path2)?;
File::create(&template_path)?;
vd_file1.write_all(VIRTUAL_DEVICE_VALID.as_bytes())?;
vd_file2.write_all(VIRTUAL_DEVICE_VALID.as_bytes())?;
let tool_provider = Box::new(FakeToolProvider::new_with_side_effect(blobfs_side_effect));
pb_create_with_sdk_version(
CreateCommand {
product_name: String::default(),
product_version: String::default(),
partitions: partitions_path,
system_a: None,
system_b: None,
system_r: None,
tuf_keys: None,
update_package_version_file: None,
update_package_epoch: None,
virtual_device: vec![vd_path1, vd_path2],
recommended_device: Some("device_2".to_string()),
out_dir: pb_dir.clone(),
delivery_blob_type: None,
with_deprecated_flash_manifest: true,
gerrit_size_report: None,
},
/*sdk_version=*/ "",
tool_provider,
)
.await
.unwrap();
let pb = ProductBundle::try_load_from(&pb_dir).unwrap();
assert_eq!(
pb,
ProductBundle::V2(ProductBundleV2 {
product_name: String::default(),
product_version: String::default(),
partitions: PartitionsConfig::default(),
sdk_version: String::default(),
system_a: None,
system_b: None,
system_r: None,
repositories: vec![],
update_package_hash: None,
virtual_devices_path: Some(pb_dir.join("virtual_devices/manifest.json")),
})
);
let internal_pb = match pb {
ProductBundle::V2(pb) => pb,
};
let path = internal_pb.get_virtual_devices_path();
let manifest =
VirtualDeviceManifest::from_path(&path).context("Manifest file from_path")?;
let default = manifest.default_device();
assert!(matches!(default, Ok(Some(VirtualDevice::V1(_)))), "{:?}", default);
let devices = manifest.device_names();
assert_eq!(devices.len(), 2);
assert!(devices.contains(&"device_1".to_string()));
assert!(devices.contains(&"device_2".to_string()));
let device1 = manifest.device("device_1");
assert!(device1.is_ok(), "{:?}", device1.unwrap_err());
assert!(matches!(device1, Ok(VirtualDevice::V1(VirtualDeviceV1 { .. }))));
let device2 = manifest.device("device_2");
assert!(device2.is_ok(), "{:?}", device2.unwrap_err());
assert!(matches!(device2, Ok(VirtualDevice::V1(VirtualDeviceV1 { .. }))));
Ok(())
}
}