| // Copyright 2024 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 anyhow::{anyhow, bail, ensure, Context, Error}; |
| use assembly_manifest::Image; |
| use assembly_partitions_config::{Partition, PartitionsConfig, Slot}; |
| use byteorder::{BigEndian, WriteBytesExt}; |
| use camino::{Utf8Path, Utf8PathBuf}; |
| use crc::crc32; |
| use fatfs::{FsOptions, NullTimeProvider, OemCpConverter, TimeProvider}; |
| use gpt::partition_types::{OperatingSystem, Type as PartType}; |
| use gpt::{DiskDevice, GptDisk}; |
| use rand::{RngCore, SeedableRng}; |
| use rand_xorshift::XorShiftRng; |
| use sdk_metadata::{LoadedProductBundle, ProductBundle}; |
| use serde::{Deserialize, Serialize}; |
| use std::collections::BTreeMap; |
| use std::ffi::CString; |
| use std::fs::{File, OpenOptions}; |
| use std::io::{BufReader, Read, Seek, SeekFrom, Write}; |
| use std::ops::Range; |
| use std::os::unix::fs::FileExt; |
| use std::path::Path; |
| use std::process::Command; |
| use std::str::FromStr; |
| use zerocopy::{Immutable, IntoBytes}; |
| |
| pub mod args; |
| use args::{Arch, BootPart, TopLevel}; |
| |
| const fn part_type(guid: &'static str) -> PartType { |
| PartType { guid, os: OperatingSystem::None } |
| } |
| |
| const ZIRCON_A_GUID: PartType = part_type("DE30CC86-1F4A-4A31-93C4-66F147D33E05"); |
| const ZIRCON_B_GUID: PartType = part_type("23CC04DF-C278-4CE7-8471-897D1A4BCDF7"); |
| const ZIRCON_R_GUID: PartType = part_type("A0E5CF57-2DEF-46BE-A80C-A2067C37CD49"); |
| const VBMETA_A_GUID: PartType = part_type("A13B4D9A-EC5F-11E8-97D8-6C3BE52705BF"); |
| const VBMETA_B_GUID: PartType = part_type("A288ABF2-EC5F-11E8-97D8-6C3BE52705BF"); |
| const VBMETA_R_GUID: PartType = part_type("6A2460C3-CD11-4E8B-80A8-12CCE268ED0A"); |
| const MISC_GUID: PartType = part_type("1D75395D-F2C6-476B-A8B7-45CC1C97B476"); |
| const INSTALLER_GUID: PartType = part_type("4DCE98CE-E77E-45C1-A863-CAF92F1330C1"); |
| const FVM_GUID: PartType = part_type("41D0E340-57E3-954E-8C1E-17ECAC44CFF5"); |
| |
| /// On QEMU, the bootloader will look for an EFI application commandline in this partition, to |
| /// allow tests to control the boot mode. |
| /// |
| /// The bootloader will match against the partition name; the type GUID doesn't matter. |
| const QEMU_COMMANDLINE_PARTITION_NAME: &str = "qemu-commandline"; |
| const QEMU_COMMANDLINE_PARTITION_SIZE: u64 = 1024; |
| const QEMU_COMMANDLINE_PARTITION_GUID: PartType = part_type("F5F38420-F848-423C-9945-3D845492A05A"); |
| |
| // The relevant part of the product bundle metadata schema, as realized by |
| // entries in the product_bundles.json build API module. |
| #[derive(Deserialize, Serialize)] |
| struct ProductBundleMetadata { |
| name: String, |
| path: Utf8PathBuf, |
| } |
| |
| // Shift takes something that implements Read + Write + Seek and offsets it by `offset`. This is |
| // used to allow fatfs to read the ESP partition. There are no checks to ensure that fatfs doesn't |
| // escape the partition, but that shouldn't happen. |
| struct Shift<T> { |
| inner: T, |
| offset: u64, |
| } |
| |
| impl<T: Seek> Shift<T> { |
| fn new(mut inner: T, offset: u64) -> Self { |
| assert_eq!(inner.seek(SeekFrom::Start(offset)).unwrap(), offset); |
| Self { inner, offset } |
| } |
| } |
| |
| impl<T: Read> Read for Shift<T> { |
| fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { |
| self.inner.read(buf) |
| } |
| } |
| |
| impl<T: Write> Write for Shift<T> { |
| fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { |
| self.inner.write(buf) |
| } |
| |
| fn flush(&mut self) -> std::io::Result<()> { |
| self.inner.flush() |
| } |
| } |
| |
| impl<T: Seek> Seek for Shift<T> { |
| fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> { |
| match pos { |
| SeekFrom::Start(offset) => { |
| Ok(self.inner.seek(SeekFrom::Start(offset + self.offset))? - self.offset) |
| } |
| SeekFrom::End(_) => panic!("Not supported"), |
| SeekFrom::Current(offset) => { |
| Ok(self.inner.seek(SeekFrom::Current(offset))? - self.offset) |
| } |
| } |
| } |
| } |
| |
| fn get_product_bundle_partitions(path: &Utf8Path) -> Result<PartitionsConfig, Error> { |
| let product_bundle = LoadedProductBundle::try_load_from(path)?; |
| |
| match product_bundle.into() { |
| ProductBundle::V2(pb) => Ok(pb.partitions), |
| } |
| } |
| |
| fn get_bootloader_partition_name(partitions: &Option<PartitionsConfig>) -> Result<&str, Error> { |
| if let Some(partitions) = &partitions { |
| match &partitions.bootloader_partitions[..] { |
| [bootloader_partition] => { |
| if let Some(name) = &bootloader_partition.name { |
| return Ok(name); |
| } else { |
| bail!("product bundle bootloader partition doesn't have a name"); |
| } |
| } |
| [] => bail!("product bundle doesn't have any bootloader partitions"), |
| _ => bail!("product bundles with multiple bootloader partitions is not supported"), |
| } |
| } |
| |
| Ok("fuchsia-esp") |
| } |
| |
| fn get_zbi_partition_name( |
| partitions: &Option<PartitionsConfig>, |
| partition_slot: Slot, |
| ) -> Result<&str, Error> { |
| let Some(partitions) = partitions else { |
| return Ok(match partition_slot { |
| Slot::A => "zircon-a", |
| Slot::B => "zircon-b", |
| Slot::R => "zircon-r", |
| }); |
| }; |
| |
| for partition in &partitions.partitions { |
| if let Partition::ZBI { name, slot, .. } = &partition { |
| if slot == &partition_slot { |
| return Ok(name); |
| } |
| } |
| } |
| |
| bail!("could not find ZBI partition name for slot {partition_slot:?}") |
| } |
| |
| fn get_vbmeta_partition_name( |
| partitions: &Option<PartitionsConfig>, |
| partition_slot: Slot, |
| ) -> Result<&str, Error> { |
| let Some(partitions) = partitions else { |
| return Ok(match partition_slot { |
| Slot::A => "vbmeta_a", |
| Slot::B => "vbmeta_b", |
| Slot::R => "vbmeta_r", |
| }); |
| }; |
| |
| for partition in &partitions.partitions { |
| if let Partition::VBMeta { name, slot, .. } = &partition { |
| if slot == &partition_slot { |
| return Ok(name); |
| } |
| } |
| } |
| |
| bail!("could not find ZBI partition name for slot {partition_slot:?}") |
| } |
| |
| /// Create a protective MBR |
| /// |
| /// The disk size is the number of logical blocks on the disk. |
| fn write_protective_mbr<D>(disk: &mut D, disk_size: u64, block_size: u64) -> Result<usize, Error> |
| where |
| D: DiskDevice, |
| { |
| let mbr = gpt::mbr::ProtectiveMBR::with_lb_size( |
| // The with_lb_size fn expects the disk size minus 1 block since the protected partition |
| // begins at LBA 1, not 0. |
| u32::try_from((disk_size - 1) / block_size).unwrap_or(0xffffffff), |
| ); |
| Ok(mbr.overwrite_lba0(disk)?) |
| } |
| |
| pub fn run(mut args: TopLevel) -> Result<(), Error> { |
| check_args(&mut args)?; |
| |
| let product_bundle_partitions = if let Some(pbms_path) = &args.product_bundle { |
| Some(get_product_bundle_partitions(pbms_path)?) |
| } else { |
| None |
| }; |
| |
| let mut disk = OpenOptions::new() |
| .read(true) |
| .write(true) |
| .truncate(args.resize.is_some()) |
| .create(args.resize.is_some()) |
| .open(&args.disk_path) |
| .context(format!("Failed to open output file {}", args.disk_path))?; |
| |
| let disk_size = if let Some(size) = args.resize { |
| disk.set_len(size)?; |
| size |
| } else { |
| disk.seek(SeekFrom::End(0))? |
| }; |
| |
| let mut config = gpt::GptConfig::new().writable(true).initialized(false); |
| if let Some(block_size) = args.block_size { |
| config = config.logical_block_size(block_size.try_into()?); |
| } |
| let mut gpt_disk = config.create_from_device(Box::new(&mut disk), None)?; |
| gpt_disk.update_partitions(BTreeMap::new())?; |
| |
| let efi_partition_name = get_bootloader_partition_name(&product_bundle_partitions)?; |
| let efi_part = |
| add_partition(&mut gpt_disk, efi_partition_name, args.efi_size, gpt::partition_types::EFI)?; |
| |
| // If a QEMU commandline is specified, this tuple will hold (partition_range, contents). |
| let qemu_commandline = match args.qemu_commandline { |
| Some(ref commandline) => Some(( |
| add_partition( |
| &mut gpt_disk, |
| QEMU_COMMANDLINE_PARTITION_NAME, |
| QEMU_COMMANDLINE_PARTITION_SIZE, |
| QEMU_COMMANDLINE_PARTITION_GUID, |
| )?, |
| commandline, |
| )), |
| None => None, |
| }; |
| |
| struct Partitions { |
| zircon_a: Range<u64>, |
| vbmeta_a: Range<u64>, |
| zircon_b: Range<u64>, |
| vbmeta_b: Range<u64>, |
| zircon_r: Range<u64>, |
| vbmeta_r: Range<u64>, |
| misc: Range<u64>, |
| } |
| |
| let abr_partitions = if !args.no_abr { |
| Some(Partitions { |
| zircon_a: add_partition( |
| &mut gpt_disk, |
| get_zbi_partition_name(&product_bundle_partitions, Slot::A)?, |
| args.abr_size, |
| ZIRCON_A_GUID, |
| )?, |
| vbmeta_a: add_partition( |
| &mut gpt_disk, |
| get_vbmeta_partition_name(&product_bundle_partitions, Slot::A)?, |
| args.vbmeta_size, |
| VBMETA_A_GUID, |
| )?, |
| zircon_b: add_partition( |
| &mut gpt_disk, |
| get_zbi_partition_name(&product_bundle_partitions, Slot::B)?, |
| args.abr_size, |
| ZIRCON_B_GUID, |
| )?, |
| vbmeta_b: add_partition( |
| &mut gpt_disk, |
| get_vbmeta_partition_name(&product_bundle_partitions, Slot::B)?, |
| args.vbmeta_size, |
| VBMETA_B_GUID, |
| )?, |
| zircon_r: add_partition( |
| &mut gpt_disk, |
| get_zbi_partition_name(&product_bundle_partitions, Slot::R)?, |
| args.abr_size, |
| ZIRCON_R_GUID, |
| )?, |
| vbmeta_r: add_partition( |
| &mut gpt_disk, |
| get_vbmeta_partition_name(&product_bundle_partitions, Slot::R)?, |
| args.vbmeta_size, |
| VBMETA_R_GUID, |
| )?, |
| misc: add_partition(&mut gpt_disk, "misc", args.vbmeta_size, MISC_GUID)?, |
| }) |
| } else { |
| None |
| }; |
| |
| let block_size: u64 = gpt_disk.logical_block_size().clone().into(); |
| |
| let fvm_part = if !args.ramdisk_only && !args.use_fxfs { |
| let size = args.system_disk_size.unwrap_or_else(|| { |
| gpt_disk.find_free_sectors().iter().map(|(_offset, length)| length).max().unwrap() |
| * block_size |
| }); |
| if args.use_sparse_fvm { |
| Some(add_partition(&mut gpt_disk, "storage-sparse", size, INSTALLER_GUID)?) |
| } else { |
| Some(add_partition(&mut gpt_disk, "fvm", size, FVM_GUID)?) |
| } |
| } else { |
| None |
| }; |
| let fxfs_part = if args.fxfs.is_some() { |
| assert!(fvm_part.is_none(), "Can't have both FVM and Fxfs"); |
| let size = args.system_disk_size.unwrap_or_else(|| { |
| gpt_disk.find_free_sectors().iter().map(|(_offset, length)| length).max().unwrap() |
| * block_size |
| }); |
| // For now, we use the same name and type as FVM because the paver looks for this. |
| Some(add_partition(&mut gpt_disk, "fvm", size, FVM_GUID)?) |
| } else { |
| None |
| }; |
| |
| if let Some(seed) = &args.seed { |
| let mut seed_bytes = [0; 16]; |
| for chunk in seed.as_bytes().chunks(16) { |
| seed_bytes.iter_mut().zip(chunk.iter()).for_each(|(x, b)| *x ^= *b); |
| } |
| let mut rng = XorShiftRng::from_seed(seed_bytes); |
| let mut bytes = [0; 16]; |
| |
| rng.fill_bytes(&mut bytes); |
| let uuid = uuid::Builder::from_bytes(bytes) |
| .with_variant(uuid::Variant::RFC4122) |
| .with_version(uuid::Version::Random) |
| .into_uuid(); |
| |
| // Unfortunately, the at time o writing, the GPT crate is using a different version of |
| // the uuid crate than we have access to. |
| gpt_disk.update_guid(Some(FromStr::from_str(&uuid.to_string()).unwrap()))?; |
| |
| let mut partitions = gpt_disk.partitions().clone(); |
| for (_id, partition) in &mut partitions { |
| rng.fill_bytes(&mut bytes); |
| let uuid = uuid::Builder::from_bytes(bytes) |
| .with_variant(uuid::Variant::RFC4122) |
| .with_version(uuid::Version::Random) |
| .into_uuid(); |
| partition.part_guid = FromStr::from_str(&uuid.to_string()).unwrap(); |
| } |
| gpt_disk.update_partitions(partitions)?; |
| } |
| |
| gpt_disk.write()?; |
| |
| write_protective_mbr(&mut disk, disk_size, block_size)?; |
| |
| let search_path = if let Some(build_dir) = &args.fuchsia_build_dir { |
| // Use tools from the build directory over $PATH. |
| let host_path = match args.host_arch { |
| Arch::X64 => "host_x64", |
| Arch::Arm64 => "host_arm64", |
| }; |
| format!("{}/{}:{}", build_dir, host_path, std::env::var("PATH").unwrap_or(String::new())) |
| } else { |
| String::new() |
| }; |
| |
| let mut command = if let Some(p) = &args.mkfs_msdosfs { |
| Command::new(p) |
| } else { |
| Command::new("mkfs-msdosfs") |
| }; |
| |
| // If a seed is provided, use a fixed timestamp. |
| if args.seed.is_some() { |
| command.arg("-T").arg("1661998826"); |
| } |
| |
| let output = command |
| .arg("-@") |
| .arg(format!("{}", efi_part.start)) |
| .arg("-S") |
| .arg(format!("{}", args.efi_size + efi_part.start)) |
| .arg("-F") |
| .arg("32") |
| .arg("-L") |
| .arg("ESP") |
| .arg("-O") |
| .arg("Fuchsia") |
| .arg("-b") |
| .arg(format!("{}", block_size)) |
| .arg(&args.disk_path) |
| .env("PATH", &search_path) |
| .output() |
| .context("Failed to run mkfs-msdosfs")?; |
| |
| if !output.status.success() { |
| bail!( |
| "mkfs-msdosfs failed, stdout={}, stderr={}", |
| std::str::from_utf8(&output.stdout).unwrap(), |
| std::str::from_utf8(&output.stderr).unwrap() |
| ); |
| } |
| |
| if args.seed.is_some() { |
| write_esp_content( |
| Shift::new(&mut disk, efi_part.start), |
| &args, |
| FsOptions::new().time_provider(NullTimeProvider::new()), |
| )?; |
| } else { |
| write_esp_content(Shift::new(&mut disk, efi_part.start), &args, FsOptions::new())?; |
| } |
| |
| if let Some((partition, contents)) = qemu_commandline { |
| // Bootloader expects a nul-terminated string. |
| let contents = CString::new(contents.as_bytes())?; |
| let contents = contents.as_bytes_with_nul(); |
| let part_size = partition.end - partition.start; |
| if contents.len() > part_size.try_into()? { |
| bail!("QEMU commandline contents too large (max {})", part_size); |
| } |
| disk.write_all_at(contents, partition.start)?; |
| } |
| |
| if let Some(partitions) = abr_partitions { |
| copy_partition(&mut disk, partitions.zircon_a, &args.zircon_a.unwrap())?; |
| copy_partition(&mut disk, partitions.vbmeta_a, &args.vbmeta_a.unwrap())?; |
| copy_partition(&mut disk, partitions.zircon_b, &args.zircon_b.unwrap())?; |
| copy_partition(&mut disk, partitions.vbmeta_b, &args.vbmeta_b.unwrap())?; |
| copy_partition(&mut disk, partitions.zircon_r, &args.zircon_r.unwrap())?; |
| copy_partition(&mut disk, partitions.vbmeta_r, &args.vbmeta_r.unwrap())?; |
| write_abr(&mut disk, partitions.misc.start, args.abr_boot)?; |
| } |
| |
| if let Some(fvm_part) = fvm_part { |
| if args.verbose { |
| println!("Populating FVM in GPT image"); |
| } |
| if args.use_sparse_fvm { |
| copy_partition(&mut disk, fvm_part, &args.sparse_fvm.unwrap())?; |
| } else { |
| let status = Command::new("fvm") |
| .arg(&args.disk_path) |
| .arg("create") |
| .arg("--offset") |
| .arg(format!("{}", fvm_part.start)) |
| .arg("--length") |
| .arg(format!("{}", fvm_part.end - fvm_part.start)) |
| .arg("--slice") |
| .arg("8388608") |
| .arg("--blob") |
| .arg(args.blob.unwrap()) |
| .arg("--with-empty-minfs") |
| .env("PATH", &search_path) |
| .status() |
| .context("Failed to run fvm tool")?; |
| ensure!(status.success(), "fvm tool failed"); |
| } |
| } |
| |
| if let Some(fxfs_part) = fxfs_part { |
| copy_partition(&mut disk, fxfs_part, &args.fxfs.unwrap())?; |
| } |
| |
| Ok(()) |
| } |
| |
| fn check_args(args: &mut TopLevel) -> Result<(), Error> { |
| if args.ramdisk_only { |
| if args.blob.is_some() || args.sparse_fvm.is_some() || args.use_sparse_fvm { |
| bail!( |
| "--ramdisk_only incompatible with --blob, --sparse-fvm and \ |
| --use-sparse-fvm" |
| ); |
| } |
| } else if (args.blob.is_some()) && (args.sparse_fvm.is_some() || args.use_sparse_fvm) { |
| bail!("--blob incompatbile with --use-sparse-fvm|--sparse-fvm"); |
| } |
| |
| if args.sparse_fvm.is_some() { |
| args.use_sparse_fvm = true |
| } |
| |
| if args.fuchsia_build_dir.is_none() { |
| if let Ok(build_dir) = std::env::var("FUCHSIA_BUILD_DIR") { |
| args.fuchsia_build_dir = Some(build_dir.into()) |
| } |
| } |
| |
| let mut dependencies = vec![]; |
| |
| if args.product_bundle.is_none() { |
| args.product_bundle = get_build_product_bundle_path(args, &mut dependencies)?; |
| }; |
| |
| let product_bundle_path = if let Some(product_bundle_dir) = &args.product_bundle { |
| Some(product_bundle_dir.join("product_bundle.json")) |
| } else { |
| None |
| }; |
| // This is a separate statement so that 'product_bundle_path' lives long enough to provide a |
| // value that can be referenced by 'dependencies' until the end of the fn. |
| if let Some(path) = &product_bundle_path { |
| dependencies.push(path.as_path()); |
| } |
| |
| // The order of precedence for file paths is as follows: |
| // |
| // 1) --zircon_[a|b], --vbmeta_[a|b], and fvm/fxfs image args first. |
| // 2) --zbi, for both zircon_a and zircon_b. |
| // 3) the images in the product bundle, and if using fxfs: |
| // a) the sparse image |
| // b) the full image (as it's being removed as an output) |
| |
| // Apply the --zbi argument to the zircon_[a|b] args, if it's been specified. |
| if let Some(path) = &args.zbi { |
| args.zircon_a.get_or_insert_with(|| path.clone()); |
| args.zircon_b.get_or_insert_with(|| path.clone()); |
| } |
| |
| // If there's a product bundle, use that for all remaining, unset, image paths. |
| if let Some(product_bundle_dir) = &args.product_bundle { |
| match LoadedProductBundle::try_load_from(product_bundle_dir)?.into() { |
| ProductBundle::V2(product_bundle) => { |
| // If the bundle contains images for slot b, use them for slot b args that haven't |
| // been provided. |
| if let Some(system_b) = product_bundle.system_b { |
| for image in &system_b { |
| match image { |
| Image::ZBI { path, .. } => { |
| args.zircon_b.get_or_insert_with(|| path.clone()); |
| } |
| Image::VBMeta(utf8_path_buf) => { |
| args.vbmeta_b.get_or_insert_with(|| utf8_path_buf.clone()); |
| } |
| _ => {} |
| } |
| } |
| } |
| |
| // If the bundle contains images for slot a, use them for for slot a AND slot b args |
| // that haven't been provided. |
| if let Some(system_a) = product_bundle.system_a { |
| for image in &system_a { |
| match image { |
| Image::ZBI { path, .. } => { |
| args.zbi.get_or_insert_with(|| path.clone()); |
| args.zircon_a.get_or_insert_with(|| path.clone()); |
| args.zircon_b.get_or_insert_with(|| path.clone()); |
| } |
| Image::VBMeta(utf8_path_buf) => { |
| args.vbmeta_a.get_or_insert_with(|| utf8_path_buf.clone()); |
| args.vbmeta_b.get_or_insert_with(|| utf8_path_buf.clone()); |
| } |
| |
| // Always set these paths if present in the product bundle, but they |
| // won't be used if args.ramdisk_only is true. |
| Image::FVMSparse(utf8_path_buf) => { |
| args.sparse_fvm.get_or_insert_with(|| utf8_path_buf.clone()); |
| } |
| Image::BlobFS { path, .. } => { |
| args.blob.get_or_insert_with(|| path.clone()); |
| } |
| Image::FxfsSparse { path, .. } => { |
| if args.use_fxfs { |
| args.fxfs.get_or_insert_with(|| path.clone()); |
| } |
| } |
| _ => {} |
| } |
| } |
| |
| // fall back to the full (non-sparse) Fxfs image if we didn't find a sparse one. |
| if args.use_fxfs && args.fxfs.is_none() { |
| args.fxfs = system_a.iter().find_map(|i| match i { |
| Image::Fxfs { path, .. } => Some(path.clone()), |
| _ => None, |
| }); |
| } |
| } |
| |
| // And the recovery zbi |
| if let Some(system_r) = product_bundle.system_r { |
| for image in &system_r { |
| match image { |
| Image::ZBI { path, .. } => { |
| args.zedboot.get_or_insert_with(|| path.clone()); |
| args.zircon_r.get_or_insert_with(|| path.clone()); |
| } |
| Image::VBMeta(utf8_path_buf) => { |
| args.vbmeta_r.get_or_insert_with(|| utf8_path_buf.clone()); |
| } |
| _ => {} |
| } |
| } |
| } |
| } |
| } |
| }; |
| |
| if args.bootloader.is_none() { |
| if let Some(build_dir) = &args.fuchsia_build_dir { |
| let bootloader_dir = match args.arch { |
| Arch::X64 => build_dir.join("kernel.efi_x64"), |
| Arch::Arm64 => build_dir.join("kernel.efi_arm64"), |
| }; |
| args.bootloader = Some(bootloader_dir.join("fuchsia-efi.efi")); |
| } else { |
| bail!("Missing --bootloader"); |
| } |
| } |
| |
| dependencies.push(args.bootloader.as_ref().unwrap()); |
| |
| ensure!(args.zbi.is_some(), "Missing --zbi"); |
| dependencies.push(args.zbi.as_ref().unwrap()); |
| ensure!(args.zedboot.is_some(), "Missing --zedboot"); |
| dependencies.push(args.zedboot.as_ref().unwrap()); |
| |
| if !args.no_abr { |
| ensure!(args.zircon_a.is_some(), "Missing --zircon_a"); |
| dependencies.push(args.zircon_a.as_ref().unwrap()); |
| ensure!(args.vbmeta_a.is_some(), "Missing --vbmeta_a"); |
| dependencies.push(args.vbmeta_a.as_ref().unwrap()); |
| ensure!(args.zircon_b.is_some(), "Missing --zircon_b"); |
| dependencies.push(args.zircon_b.as_ref().unwrap()); |
| ensure!(args.vbmeta_b.is_some(), "Missing --vbmeta_b"); |
| dependencies.push(args.vbmeta_b.as_ref().unwrap()); |
| ensure!(args.zircon_r.is_some(), "Missing --zircon_r"); |
| dependencies.push(args.zircon_r.as_ref().unwrap()); |
| ensure!(args.vbmeta_r.is_some(), "Missing --vbmeta_r"); |
| dependencies.push(args.vbmeta_r.as_ref().unwrap()); |
| } |
| |
| if !args.ramdisk_only { |
| if args.use_sparse_fvm { |
| ensure!(args.sparse_fvm.is_some(), "Missing --sparse-fvm"); |
| dependencies.push(args.sparse_fvm.as_ref().unwrap()); |
| } else if args.use_fxfs { |
| ensure!(args.fxfs.is_some(), "Missing --fxfs"); |
| dependencies.push(args.fxfs.as_ref().unwrap()); |
| } else { |
| ensure!(args.blob.is_some(), "Missing --blob"); |
| dependencies.push(args.blob.as_ref().unwrap()); |
| } |
| } |
| |
| if args.depfile { |
| // Write a dependency file |
| // The output file and dependencies needs to be relative to the build dir. |
| let mut disk_path = args.disk_path.as_path(); |
| if let Some(build_dir) = &args.fuchsia_build_dir { |
| if let Ok(dir) = disk_path.strip_prefix(build_dir) { |
| disk_path = dir; |
| } |
| |
| for dep in dependencies.iter_mut() { |
| if let Ok(d) = dep.strip_prefix(build_dir) { |
| *dep = d; |
| } |
| } |
| } |
| let depfile = format!("{}.d", args.disk_path); |
| std::fs::write( |
| &depfile, |
| format!( |
| "{}: {}", |
| disk_path, |
| dependencies.iter().map(|dep| dep.as_str()).collect::<Vec<_>>().join(" ") |
| ), |
| ) |
| .context(format!("Failed to write {}", &depfile))?; |
| } |
| |
| Ok(()) |
| } |
| |
| fn get_build_product_bundle_path( |
| args: &TopLevel, |
| dependencies: &mut Vec<&Utf8Path>, |
| ) -> Result<Option<Utf8PathBuf>, Error> { |
| let Some(build_dir) = &args.fuchsia_build_dir else { return Ok(None) }; |
| let Some(name) = &args.product_bundle_name else { return Ok(None) }; |
| |
| dependencies.push(Utf8Path::new("product_bundles.json")); |
| let product_bundles_path = &build_dir.join(dependencies.last().unwrap()); |
| let f = match File::open(product_bundles_path) { |
| Ok(f) => f, |
| Err(err) if err.kind() == std::io::ErrorKind::NotFound => { |
| bail!("{product_bundles_path} not found"); |
| } |
| Err(err) => { |
| return Err(err.into()); |
| } |
| }; |
| |
| let product_bundles: Vec<ProductBundleMetadata> = serde_json::from_reader(BufReader::new(f))?; |
| match product_bundles.iter().find(|&pb| &pb.name == name) { |
| None => Ok(None), |
| Some(pb) => Ok(Some(build_dir.join(&pb.path))), |
| } |
| } |
| |
| fn read_file(path: impl AsRef<std::path::Path>) -> Result<Vec<u8>, Error> { |
| std::fs::read(&path).context(format!("Failed to read {}", path.as_ref().display())) |
| } |
| |
| // Adds a partition and returns the partition byte range. |
| fn add_partition( |
| disk: &mut GptDisk<'_>, |
| name: &str, |
| size: u64, |
| part_type: PartType, |
| ) -> Result<Range<u64>, Error> { |
| let part_id = disk |
| .add_partition(name, size, part_type, 0, None) |
| .context(format!("Failed to add {} partition (size={})", name, size))?; |
| Ok(part_range(disk, part_id)) |
| } |
| |
| fn write_esp_content<TP: TimeProvider, OCC: OemCpConverter>( |
| part: impl Read + Write + Seek, |
| args: &TopLevel, |
| options: FsOptions<TP, OCC>, |
| ) -> Result<(), Error> { |
| // If a seed is provided, all timestamps used will be the epoch. |
| let fs = fatfs::FileSystem::new(part, options)?; |
| |
| { |
| let root_dir = fs.root_dir(); |
| |
| let bootloader_name = match args.arch { |
| Arch::X64 => "bootx64.efi", |
| Arch::Arm64 => "bootaa64.efi", |
| }; |
| |
| root_dir.create_dir("EFI")?; |
| root_dir.create_dir("EFI/Google")?; |
| root_dir.create_dir("EFI/Google/GSetup")?; |
| root_dir |
| .create_file("EFI/Google/GSetup/Boot")? |
| .write_all(format!("efi\\boot\\{}", bootloader_name).as_bytes())?; |
| |
| root_dir.create_dir("EFI/BOOT")?; |
| root_dir |
| .create_file(&format!("EFI/BOOT/{}", bootloader_name))? |
| .write_all(&read_file(args.bootloader.as_ref().unwrap())?)?; |
| |
| if args.no_abr { |
| root_dir |
| .create_file("zircon.bin")? |
| .write_all(&read_file(args.zbi.as_ref().unwrap())?)?; |
| root_dir |
| .create_file("zedboot.bin")? |
| .write_all(&read_file(args.zedboot.as_ref().unwrap())?)?; |
| } |
| |
| if let Some(cmdline) = &args.cmdline { |
| root_dir.create_file("cmdline")?.write_all(&read_file(cmdline)?)?; |
| } |
| } |
| |
| fs.unmount()?; |
| |
| Ok(()) |
| } |
| |
| // Copies the file at `source` path to the disk. `range` is the partition byte range. |
| fn copy_partition(disk: &mut File, range: Range<u64>, source: &Utf8Path) -> Result<(), Error> { |
| let source_file = std::fs::File::open(source).context(format!("Failed to read {}", source))?; |
| let mut reader = std::io::BufReader::new(source_file); |
| |
| let contents = if sparse::is_sparse_image(&mut reader) { |
| let mut writer = std::io::Cursor::new(Vec::new()); |
| sparse::unsparse(&mut reader, &mut writer) |
| .map_err(|e| anyhow!("cannot inflate sparse image: {e}"))?; |
| writer.into_inner() |
| } else { |
| let mut buf = Vec::new(); |
| reader.read_to_end(&mut buf).map_err(|e| anyhow!("cannot read image: {e}"))?; |
| buf |
| }; |
| |
| let max_len = (range.end - range.start) as usize; |
| let contents = if contents.len() > max_len { |
| println!("{} too big", source); |
| &contents[..max_len] |
| } else { |
| &contents |
| }; |
| disk.write_all_at(contents, range.start)?; |
| Ok(()) |
| } |
| |
| // Returns the partition range given a partition ID. |
| fn part_range(disk: &GptDisk<'_>, part_id: u32) -> Range<u64> { |
| let lbs = u64::from(disk.logical_block_size().clone()); |
| let part = &disk.partitions()[&part_id]; |
| part.first_lba * lbs..(part.last_lba + 1) * lbs |
| } |
| |
| // Routines for writing the ABR. |
| const MAX_TRIES: u8 = 7; |
| const MAX_PRIORITY: u8 = 15; |
| |
| #[derive(IntoBytes, Immutable)] |
| #[repr(C, packed)] |
| #[derive(Default)] |
| struct AbrData { |
| magic: [u8; 4], |
| version_major: u8, |
| version_minor: u8, |
| reserved1: u16, |
| slot_data: [AbrSlotData; 2], |
| one_shot_recovery_boot: u8, |
| reserved2: [u8; 11], |
| // A CRC32 comes next. |
| } |
| |
| #[derive(IntoBytes, Immutable)] |
| #[repr(C, packed)] |
| #[derive(Default)] |
| struct AbrSlotData { |
| priority: u8, |
| tries_remaining: u8, |
| successful_boot: u8, |
| reserved: u8, |
| } |
| |
| impl AbrSlotData { |
| fn with_priority(priority: u8) -> AbrSlotData { |
| AbrSlotData { tries_remaining: MAX_TRIES, priority, ..Default::default() } |
| } |
| } |
| |
| // Writes the ABR to the disk at `offset`. |
| fn write_abr(disk: &mut File, offset: u64, boot_part: BootPart) -> Result<(), Error> { |
| let data = AbrData { |
| magic: b"\0AB0".clone(), |
| version_major: 2, |
| version_minor: 1, |
| slot_data: match boot_part { |
| BootPart::BootA => { |
| [AbrSlotData::with_priority(MAX_PRIORITY), AbrSlotData::with_priority(1)] |
| } |
| BootPart::BootB => { |
| [AbrSlotData::with_priority(1), AbrSlotData::with_priority(MAX_PRIORITY)] |
| } |
| BootPart::BootR => [AbrSlotData::with_priority(0), AbrSlotData::with_priority(0)], |
| }, |
| ..Default::default() |
| }; |
| |
| disk.seek(SeekFrom::Start(offset))?; |
| disk.write_all(data.as_bytes())?; |
| disk.write_u32::<BigEndian>(crc32::checksum_ieee(data.as_bytes()))?; |
| Ok(()) |
| } |
| |
| /// Sets up a Fuchsia disk with the required partitions and an imaged UEFI partition. |
| pub fn make_empty_disk_with_uefi(disk_path: &Path, efi_data: &[u8]) -> anyhow::Result<()> { |
| // TODO(http://b/369045905): Some of this can probably be deduplicated with run() |
| const EFI_SIZE: u64 = 63 * 1024 * 1024; |
| const VBMETA_SIZE: u64 = 64 * 1024; |
| const ABR_SIZE: u64 = 256 * 1024 * 1024; |
| |
| // For x64, the FVM size is hardcoded to 16GB. |
| // If the partition size is less than 16GB, it fails to mount. |
| const DISK_SIZE: u64 = 20 * 1024 * 1024 * 1024; |
| |
| let mut disk = OpenOptions::new() |
| .read(true) |
| .write(true) |
| .truncate(true) |
| .create(true) |
| .open(disk_path) |
| .context(format!("Failed to open output file {}", disk_path.display()))?; |
| |
| disk.set_len(DISK_SIZE)?; |
| |
| let config = gpt::GptConfig::new().writable(true).initialized(false); |
| let mut gpt_disk = config.create_from_device(Box::new(&mut disk), None)?; |
| gpt_disk.update_partitions(std::collections::BTreeMap::new())?; |
| |
| struct Partitions { |
| efi: Range<u64>, |
| _zircon_a: Range<u64>, |
| _vbmeta_a: Range<u64>, |
| _zircon_b: Range<u64>, |
| _vbmeta_b: Range<u64>, |
| _zircon_r: Range<u64>, |
| _vbmeta_r: Range<u64>, |
| _misc: Range<u64>, |
| } |
| |
| let part = Partitions { |
| efi: add_partition(&mut gpt_disk, "fuchsia-esp", EFI_SIZE, gpt::partition_types::EFI)?, |
| _zircon_a: add_partition(&mut gpt_disk, "zircon-a", ABR_SIZE, ZIRCON_A_GUID)?, |
| _vbmeta_a: add_partition(&mut gpt_disk, "vbmeta_a", VBMETA_SIZE, VBMETA_A_GUID)?, |
| _zircon_b: add_partition(&mut gpt_disk, "zircon-b", ABR_SIZE, ZIRCON_B_GUID)?, |
| _vbmeta_b: add_partition(&mut gpt_disk, "vbmeta_b", VBMETA_SIZE, VBMETA_B_GUID)?, |
| _zircon_r: add_partition(&mut gpt_disk, "zircon-r", ABR_SIZE, ZIRCON_R_GUID)?, |
| _vbmeta_r: add_partition(&mut gpt_disk, "vbmeta_r", VBMETA_SIZE, VBMETA_R_GUID)?, |
| _misc: add_partition(&mut gpt_disk, "misc", VBMETA_SIZE, MISC_GUID)?, |
| }; |
| |
| let block_size: u64 = gpt_disk.logical_block_size().clone().into(); |
| |
| let fvm_size = |
| gpt_disk.find_free_sectors().iter().map(|(_offset, length)| length).max().unwrap() |
| * block_size; |
| |
| let _fvm_part = add_partition(&mut gpt_disk, "fvm", fvm_size, FVM_GUID)?; |
| |
| gpt_disk.write()?; |
| |
| write_protective_mbr(&mut disk, DISK_SIZE, block_size)?; |
| |
| disk.write_all_at(efi_data, part.efi.start)?; |
| |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use argh::FromArgs; |
| use gpt::disk::read_disk; |
| use std::str::from_utf8; |
| use tempfile::TempDir; |
| |
| fn compare_golden(test_data_dir: &Utf8Path, image_path: &Utf8Path) { |
| let image = std::fs::read(&image_path).expect("Unable to read image"); |
| let file = |
| std::fs::File::open(test_data_dir.join("golden")).expect("Unable to read golden"); |
| let golden = zstd::decode_all(file).expect("Unable to decompress"); |
| |
| // If this fails, here are some tips for debugging: |
| // |
| // Create a loopback device: |
| // |
| // sudo losetup -fP /tmp/image |
| // |
| // Inspect the partition map: |
| // |
| // fdisk /tmp/image |
| // |
| // Verify the ESP partition: |
| // |
| // sudo fsck sudo fsck /dev/loop0p1 |
| // |
| // Mount the ESP partition: |
| // |
| // mkdir /tmp/esp |
| // sudo mount -t vfat -o loop /dev/loop0p1 /tmp/esp |
| // |
| // To overwrite the golden image: |
| // |
| // zstd /tmp/image -o golden |
| // |
| // View the contents of one of the partitions e.g. the misc partition: |
| // |
| // sudo dd if=/dev/loop0p8 | hexdump -C |
| assert!(image == golden); |
| } |
| |
| #[test] |
| fn test_with_product_bundle_name() { |
| let tmp = tempfile::tempdir().unwrap(); |
| let dir = Utf8Path::from_path(tmp.path()).unwrap(); |
| |
| for i in 0..11 { |
| std::fs::write(dir.join(format!("placeholder.{}", i)), vec![i; 8192]).unwrap(); |
| } |
| |
| let current_exe = std::env::current_exe().unwrap(); |
| |
| let test_data_dir = Utf8PathBuf::from_path_buf( |
| current_exe.parent().unwrap().join("make-fuchsia-vol_test_data"), |
| ) |
| .unwrap(); |
| |
| { |
| let f = File::create(test_data_dir.join("product_bundles.json")).unwrap(); |
| let pbs: [ProductBundleMetadata; 0] = []; |
| serde_json::to_writer(&f, &pbs).unwrap(); |
| } |
| |
| const IMAGE_SIZE: usize = 67108864; |
| |
| let image_path = dir.join("image"); |
| run(TopLevel::from_args( |
| &["make-fuchsia-vol"], |
| &[ |
| image_path.as_str(), |
| "--abr-size", |
| "8192", |
| "--resize", |
| &format!("{}", IMAGE_SIZE), |
| "--seed", |
| "test_compare_golden", |
| "--efi-size", |
| "40000000", |
| "--bootloader", |
| dir.join("placeholder.0").as_str(), |
| "--zbi", |
| dir.join("placeholder.1").as_str(), |
| "--zedboot", |
| dir.join("placeholder.2").as_str(), |
| "--cmdline", |
| dir.join("placeholder.3").as_str(), |
| "--sparse-fvm", |
| dir.join("placeholder.4").as_str(), |
| "--zircon-a", |
| dir.join("placeholder.5").as_str(), |
| "--vbmeta-a", |
| dir.join("placeholder.6").as_str(), |
| "--zircon-b", |
| dir.join("placeholder.7").as_str(), |
| "--vbmeta-b", |
| dir.join("placeholder.8").as_str(), |
| "--zircon-r", |
| dir.join("placeholder.9").as_str(), |
| "--vbmeta-r", |
| dir.join("placeholder.10").as_str(), |
| "--fuchsia-build-dir", |
| test_data_dir.as_str(), |
| "--product-bundle-name", |
| "my-product-bundle", |
| ], |
| ) |
| .unwrap()) |
| .expect("run failed"); |
| |
| compare_golden(&test_data_dir, &image_path); |
| } |
| |
| #[test] |
| fn test_with_product_bundle_path() { |
| let tmp = tempfile::tempdir().unwrap(); |
| let dir = Utf8Path::from_path(tmp.path()).unwrap(); |
| |
| for i in 0..11 { |
| std::fs::write(dir.join(format!("placeholder.{}", i)), vec![i; 8192]).unwrap(); |
| } |
| |
| let current_exe = std::env::current_exe().unwrap(); |
| |
| let test_data_dir = Utf8PathBuf::from_path_buf( |
| current_exe.parent().unwrap().join("make-fuchsia-vol_test_data"), |
| ) |
| .unwrap(); |
| |
| const IMAGE_SIZE: usize = 67108864; |
| |
| let image_path = dir.join("image"); |
| run(TopLevel::from_args( |
| &["make-fuchsia-vol"], |
| &[ |
| image_path.as_str(), |
| "--abr-size", |
| "8192", |
| "--resize", |
| &format!("{}", IMAGE_SIZE), |
| "--seed", |
| "test_compare_golden", |
| "--efi-size", |
| "40000000", |
| "--bootloader", |
| dir.join("placeholder.0").as_str(), |
| "--zbi", |
| dir.join("placeholder.1").as_str(), |
| "--zedboot", |
| dir.join("placeholder.2").as_str(), |
| "--cmdline", |
| dir.join("placeholder.3").as_str(), |
| "--sparse-fvm", |
| dir.join("placeholder.4").as_str(), |
| "--zircon-a", |
| dir.join("placeholder.5").as_str(), |
| "--vbmeta-a", |
| dir.join("placeholder.6").as_str(), |
| "--zircon-b", |
| dir.join("placeholder.7").as_str(), |
| "--vbmeta-b", |
| dir.join("placeholder.8").as_str(), |
| "--zircon-r", |
| dir.join("placeholder.9").as_str(), |
| "--vbmeta-r", |
| dir.join("placeholder.10").as_str(), |
| "--fuchsia-build-dir", |
| test_data_dir.as_str(), |
| "--product-bundle", |
| test_data_dir.as_str(), |
| ], |
| ) |
| .unwrap()) |
| .expect("run failed"); |
| |
| compare_golden(&test_data_dir, &image_path); |
| } |
| |
| /// Calls `make-fuchsia-vol` with common args plus a QEMU commandline string. |
| /// |
| /// Returns a (temp_directory, image_path) tuple on success. |
| fn make_vol_with_commandline(commandline: &str) -> Result<(TempDir, Utf8PathBuf), Error> { |
| let tmp = tempfile::tempdir().unwrap(); |
| let dir = Utf8Path::from_path(tmp.path()).unwrap(); |
| let image_path = dir.join("image"); |
| |
| for i in 0..11 { |
| std::fs::write(dir.join(format!("placeholder.{}", i)), vec![i; 8192]).unwrap(); |
| } |
| |
| let current_exe = std::env::current_exe().unwrap(); |
| |
| let test_data_dir = Utf8PathBuf::from_path_buf( |
| current_exe.parent().unwrap().join("make-fuchsia-vol_test_data"), |
| ) |
| .unwrap(); |
| |
| const IMAGE_SIZE: usize = 67108864; |
| |
| // Same args as the above tests, but with `--qemu-commandline` also. |
| run(TopLevel::from_args( |
| &["make-fuchsia-vol"], |
| &[ |
| image_path.as_str(), |
| "--abr-size", |
| "8192", |
| "--resize", |
| &format!("{}", IMAGE_SIZE), |
| "--seed", |
| "test_compare_golden", |
| "--efi-size", |
| "40000000", |
| "--bootloader", |
| dir.join("placeholder.0").as_str(), |
| "--zbi", |
| dir.join("placeholder.1").as_str(), |
| "--zedboot", |
| dir.join("placeholder.2").as_str(), |
| "--cmdline", |
| dir.join("placeholder.3").as_str(), |
| "--sparse-fvm", |
| dir.join("placeholder.4").as_str(), |
| "--zircon-a", |
| dir.join("placeholder.5").as_str(), |
| "--vbmeta-a", |
| dir.join("placeholder.6").as_str(), |
| "--zircon-b", |
| dir.join("placeholder.7").as_str(), |
| "--vbmeta-b", |
| dir.join("placeholder.8").as_str(), |
| "--zircon-r", |
| dir.join("placeholder.9").as_str(), |
| "--vbmeta-r", |
| dir.join("placeholder.10").as_str(), |
| "--fuchsia-build-dir", |
| test_data_dir.as_str(), |
| "--product-bundle", |
| test_data_dir.as_str(), |
| "--qemu-commandline", |
| commandline, |
| ], |
| ) |
| .unwrap())?; |
| |
| Ok((tmp, image_path)) |
| } |
| |
| #[test] |
| fn test_qemu_commandline() { |
| let (_tmp, image_path) = make_vol_with_commandline("boot_mode=fastboot").unwrap(); |
| |
| // Find the partition location from the GPT. |
| let disk = read_disk(&image_path).unwrap(); |
| let partition = disk |
| .partitions() |
| .iter() |
| .find(|p| p.1.name == QEMU_COMMANDLINE_PARTITION_NAME) |
| .unwrap() |
| .1; |
| |
| // Make sure the partition contains our expected contents. |
| let file = File::open(&image_path).unwrap(); |
| let mut buffer = [0u8; 19]; |
| assert!(file |
| .read_exact_at(&mut buffer, partition.bytes_start(*disk.logical_block_size()).unwrap()) |
| .is_ok()); |
| assert_eq!(&buffer, b"boot_mode=fastboot\0"); |
| } |
| |
| #[test] |
| fn test_qemu_commandline_too_long() { |
| // The commandline plus a nul-terminator needs to fit in the partition, so trying to |
| // provide contents equal to the partition size should fail. |
| let too_long_commandline = vec![b'a'; QEMU_COMMANDLINE_PARTITION_SIZE as usize]; |
| assert!(make_vol_with_commandline(from_utf8(&too_long_commandline).unwrap()).is_err()); |
| } |
| } |