blob: fed395dc6bee3f798182fb8b8c77908d8d8fa7e8 [file] [log] [blame]
// 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 anyhow::{anyhow, Context, Error, Result};
use assembly_config_schema::BoardDriverArguments;
use assembly_tool::Tool;
use assembly_util::BootfsDestination;
use camino::{Utf8Path, Utf8PathBuf};
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Write;
use tracing::debug;
use zerocopy::AsBytes;
use crate::zbi_items::{ZbiBoardInfo, ZbiPlatformId};
/// Builder for the Zircon Boot Image (ZBI), which takes in a kernel, BootFS, boot args, and kernel
/// command line.
pub struct ZbiBuilder {
/// The zbi host tool.
tool: Box<dyn Tool>,
kernel: Option<Utf8PathBuf>,
// Map from file destination in the ZBI to the path of the source file on the host.
bootfs_files: BTreeMap<String, Utf8PathBuf>,
bootargs: Vec<String>,
cmdline: Vec<String>,
board_driver_arguments: Option<BoardDriverArguments>,
// A ramdisk to add to the ZBI.
ramdisk: Option<Utf8PathBuf>,
// A devicetree binary to add to the ZBI
devicetree: Option<Utf8PathBuf>,
/// optional compression to use.
compression: Option<String>,
/// optional output manifest file
output_manifest: Option<Utf8PathBuf>,
}
impl ZbiBuilder {
/// Construct a new ZbiBuilder that uses the zbi |tool|.
pub fn new(tool: Box<dyn Tool>) -> Self {
Self {
tool,
kernel: None,
bootfs_files: BTreeMap::default(),
bootargs: Vec::default(),
cmdline: Vec::default(),
board_driver_arguments: None,
ramdisk: None,
devicetree: None,
compression: None,
output_manifest: None,
}
}
/// Set the kernel to be used.
pub fn set_kernel(&mut self, kernel: impl Into<Utf8PathBuf>) {
self.kernel = Some(kernel.into());
}
/// Add a file to the bootfs as a merkle root identified blob.
pub fn add_bootfs_blob(
&mut self,
source: impl Into<Utf8PathBuf>,
merkle_root: impl std::fmt::Display,
) {
// Every file that is part of a package included in the bootfs image
// will exist under a `blob` directory, and will be identified by
// its merkle root.
let bootfs_path = format!("blob/{}", merkle_root);
self.bootfs_files.insert(bootfs_path.to_string(), source.into());
}
/// Add a BootFS file to the ZBI.
pub fn add_bootfs_file(
&mut self,
source: impl Into<Utf8PathBuf>,
destination: impl AsRef<str>,
) {
if self.bootfs_files.contains_key(destination.as_ref()) {
println!("Found duplicate bootfs destination: {}", destination.as_ref());
return;
}
self.bootfs_files.insert(destination.as_ref().to_string(), source.into());
}
/// Add a boot argument to the ZBI.
pub fn add_boot_arg(&mut self, arg: &str) {
self.bootargs.push(arg.to_string());
}
/// Add a kernel command line argument.
pub fn add_cmdline_arg(&mut self, arg: &str) {
self.cmdline.push(arg.to_string());
}
/// Add optional board driver arguments to be added as ZBI items.
pub fn set_board_driver_arguments(&mut self, args: BoardDriverArguments) {
self.board_driver_arguments = Some(args);
}
/// Add a ramdisk to the ZBI.
pub fn add_ramdisk(&mut self, source: impl Into<Utf8PathBuf>) {
self.ramdisk = Some(source.into());
}
/// Add a devicetree binary to the ZBI.
pub fn add_devicetree(&mut self, source: impl Into<Utf8PathBuf>) {
self.devicetree = Some(source.into());
}
/// Set the compression to use with the ZBI.
pub fn set_compression(&mut self, compress: impl ToString) {
self.compression = Some(compress.to_string());
}
/// Set the path to an optional JSON output manifest to produce.
pub fn set_output_manifest(&mut self, manifest: impl Into<Utf8PathBuf>) {
self.output_manifest = Some(manifest.into());
}
/// Build the ZBI.
pub fn build(self, gendir: impl AsRef<Utf8Path>, output: impl AsRef<Utf8Path>) -> Result<()> {
// Create the additional_boot_args.
// TODO(https://fxbug.dev/42157396): Switch to the boot args file once we are no longer
// comparing to the GN build.
let additional_boot_args_path = gendir.as_ref().join("additional_boot_args.txt");
let mut additional_boot_args = File::create(&additional_boot_args_path)
.map_err(|e| Error::new(e).context("failed to create the additional boot args"))?;
self.write_boot_args(&mut additional_boot_args)?;
let (platform_id_path, board_info_path) =
if let Some(BoardDriverArguments { vendor_id, product_id, revision, ref name }) =
self.board_driver_arguments
{
let platform_id = ZbiPlatformId::new(vendor_id, product_id, name)?;
let board_info = ZbiBoardInfo { revision };
let platform_id_path = gendir.as_ref().join("platform_id.bin");
let board_info_path = gendir.as_ref().join("board_info.bin");
std::fs::write(&platform_id_path, platform_id.as_bytes())
.with_context(|| format!("writing platform_id to: {}", platform_id_path))?;
std::fs::write(&board_info_path, board_info.as_bytes())
.with_context(|| format!("writing board_info to: {}", platform_id_path))?;
(Some(platform_id_path), Some(board_info_path))
} else {
(None, None)
};
// Create the BootFS manifest file that lists all the files to insert
// into BootFS.
let bootfs_manifest_path = gendir.as_ref().join("bootfs_files.list");
let mut bootfs_manifest = File::create(&bootfs_manifest_path)
.map_err(|e| Error::new(e).context("failed to create the bootfs manifest"))?;
self.write_bootfs_manifest(additional_boot_args_path, &mut bootfs_manifest)?;
// Run the zbi tool to construct the ZBI.
let zbi_args = self.build_zbi_args(
&bootfs_manifest_path,
None::<Utf8PathBuf>,
platform_id_path,
board_info_path,
output,
)?;
debug!("ZBI command args: {:?}", zbi_args);
self.tool.run(&zbi_args)
}
fn write_bootfs_manifest(
&self,
additional_boot_args_path: impl Into<Utf8PathBuf>,
out: &mut impl Write,
) -> Result<()> {
let mut bootfs_files = self.bootfs_files.clone();
bootfs_files.insert(
BootfsDestination::AdditionalBootArgs.to_string(),
additional_boot_args_path.into(),
);
for (destination, source) in bootfs_files {
write!(out, "{}", destination)?;
write!(out, "=")?;
// TODO(fxbug.dev76135): Use the zbi tool's set of valid inputs instead of constraining
// to valid UTF-8.
writeln!(out, "{}", source)?;
}
Ok(())
}
fn write_boot_args(&self, out: &mut impl Write) -> Result<()> {
for arg in &self.bootargs {
writeln!(out, "{}", arg)?;
}
Ok(())
}
fn build_zbi_args(
&self,
bootfs_manifest_path: impl AsRef<Utf8Path>,
boot_args_path: Option<impl AsRef<Utf8Path>>,
platform_id_path: Option<impl AsRef<Utf8Path>>,
board_info_path: Option<impl AsRef<Utf8Path>>,
output_path: impl AsRef<Utf8Path>,
) -> Result<Vec<String>> {
// Ensure a kernel is supplied.
let kernel = &self.kernel.as_ref().ok_or(anyhow!("No kernel image supplied"))?;
let mut args: Vec<String> = Vec::new();
// Add the kernel itself, first, to make a bootable ZBI.
args.push("--type=container".to_string());
args.push(kernel.to_string());
// Then, add the kernel cmdline args.
args.push("--type=cmdline".to_string());
for cmd in &self.cmdline {
args.push(format!("--entry={}", cmd));
}
if let Some(platform_id_path) = platform_id_path {
args.push("--type=platform_id".to_string());
args.push(platform_id_path.as_ref().to_string());
}
if let Some(board_info_path) = board_info_path {
args.push("--type=drv_board_info".to_string());
args.push(board_info_path.as_ref().to_string());
}
if let Some(devicetree_path) = &self.devicetree {
args.push("--type=devicetree".to_string());
args.push(devicetree_path.to_string());
}
// Then, add the bootfs files.
args.push("--files".to_string());
args.push(bootfs_manifest_path.as_ref().to_string());
// Instead of supplying the additional_boot_args.txt file, we could use boot args. This is disabled
// by default, in order to allow for binary diffing the ZBI to the in-tree built ZBI.
if let Some(boot_args_path) = boot_args_path {
let boot_args_path = boot_args_path.as_ref().to_string();
args.push("--type=image_args".to_string());
args.push(format!("--entry={}", boot_args_path));
}
// Add the ramdisk if needed.
if let Some(ramdisk) = &self.ramdisk {
args.push("--type=ramdisk".to_string());
if let Some(compression) = &self.compression {
args.push(format!("--compress={}", compression));
}
args.push(ramdisk.to_string());
}
// Set the compression level for bootfs files.
if let Some(compression) = &self.compression {
args.push(format!("--compressed={}", compression));
}
// Set the output file to write.
args.push("--output".into());
args.push(output_path.as_ref().to_string());
// Create an output manifest that describes the contents of the built ZBI.
if let Some(output_manifest) = &self.output_manifest {
args.push("--json-output".into());
args.push(output_manifest.to_string());
}
Ok(args)
}
}
#[cfg(test)]
mod tests {
use super::*;
use assembly_tool::testing::FakeToolProvider;
use assembly_tool::ToolProvider;
#[test]
fn bootfs_manifest_additional_boot_args_only() {
let tools = FakeToolProvider::default();
let zbi_tool = tools.get_tool("zbi").unwrap();
let mut builder = ZbiBuilder::new(zbi_tool);
let mut output: Vec<u8> = Vec::new();
builder.write_bootfs_manifest("additional_boot_args.txt", &mut output).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert_eq!(
output_str,
"config/additional_boot_args=additional_boot_args.txt\n".to_string()
);
let mut output: Vec<u8> = Vec::new();
builder.add_bootfs_file("path/to/file2", "bin/file2");
builder.add_bootfs_file("path/to/file1", "lib/file1");
builder.write_bootfs_manifest("additional_boot_args.txt", &mut output).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert_eq!(
output_str,
"bin/file2=path/to/file2\nconfig/additional_boot_args=additional_boot_args.txt\nlib/file1=path/to/file1\n"
.to_string()
);
}
#[test]
fn bootfs_manifest() {
let tools = FakeToolProvider::default();
let zbi_tool = tools.get_tool("zbi").unwrap();
let mut builder = ZbiBuilder::new(zbi_tool);
let mut output: Vec<u8> = Vec::new();
builder.add_bootfs_file("path/to/file2", "bin/file2");
builder.add_bootfs_file("path/to/file1", "lib/file1");
builder.add_bootfs_blob("path/to/file1", "my_merkle");
builder.write_bootfs_manifest("additional_boot_args.txt", &mut output).unwrap();
assert_eq!(
output,
b"bin/file2=path/to/file2\nblob/my_merkle=path/to/file1\nconfig/additional_boot_args=additional_boot_args.txt\nlib/file1=path/to/file1\n"
);
}
#[test]
fn boot_args() {
let tools = FakeToolProvider::default();
let zbi_tool = tools.get_tool("zbi").unwrap();
let mut builder = ZbiBuilder::new(zbi_tool);
let mut output: Vec<u8> = Vec::new();
builder.write_boot_args(&mut output).unwrap();
assert_eq!(output, b"");
output.clear();
builder.add_boot_arg("boot-arg1");
builder.add_boot_arg("boot-arg2");
builder.write_boot_args(&mut output).unwrap();
assert_eq!(output, b"boot-arg1\nboot-arg2\n");
}
#[test]
fn zbi_args_missing_kernel() {
let tools = FakeToolProvider::default();
let zbi_tool = tools.get_tool("zbi").unwrap();
let builder = ZbiBuilder::new(zbi_tool);
assert!(builder
.build_zbi_args("bootfs", Some("bootargs"), None::<String>, None::<String>, "output")
.is_err());
}
#[test]
fn zbi_args_with_kernel() {
let tools = FakeToolProvider::default();
let zbi_tool = tools.get_tool("zbi").unwrap();
let mut builder = ZbiBuilder::new(zbi_tool);
builder.set_kernel("path/to/kernel");
let args = builder
.build_zbi_args("bootfs", Some("bootargs"), None::<String>, None::<String>, "output")
.unwrap();
assert_eq!(
args,
[
"--type=container",
"path/to/kernel",
"--type=cmdline",
"--files",
"bootfs",
"--type=image_args",
"--entry=bootargs",
"--output",
"output",
]
);
}
#[test]
fn zbi_args_with_kernel_with_devicetree() {
let tools = FakeToolProvider::default();
let zbi_tool = tools.get_tool("zbi").unwrap();
let mut builder = ZbiBuilder::new(zbi_tool);
builder.set_kernel("path/to/kernel");
builder.add_devicetree("path/to/devicetree");
let args = builder
.build_zbi_args("bootfs", Some("bootargs"), None::<String>, None::<String>, "output")
.unwrap();
assert_eq!(
args,
[
"--type=container",
"path/to/kernel",
"--type=cmdline",
"--type=devicetree",
"path/to/devicetree",
"--files",
"bootfs",
"--type=image_args",
"--entry=bootargs",
"--output",
"output",
]
);
}
#[test]
fn zbi_args_with_cmdline() {
let tools = FakeToolProvider::default();
let zbi_tool = tools.get_tool("zbi").unwrap();
let mut builder = ZbiBuilder::new(zbi_tool);
builder.set_kernel("path/to/kernel");
builder.add_cmdline_arg("cmd-arg1");
builder.add_cmdline_arg("cmd-arg2");
let args = builder
.build_zbi_args("bootfs", Some("bootargs"), None::<String>, None::<String>, "output")
.unwrap();
assert_eq!(
args,
[
"--type=container",
"path/to/kernel",
"--type=cmdline",
"--entry=cmd-arg1",
"--entry=cmd-arg2",
"--files",
"bootfs",
"--type=image_args",
"--entry=bootargs",
"--output",
"output",
]
);
}
#[test]
fn zbi_args_without_boot_args() {
let tools = FakeToolProvider::default();
let zbi_tool = tools.get_tool("zbi").unwrap();
let mut builder = ZbiBuilder::new(zbi_tool);
builder.set_kernel("path/to/kernel");
builder.add_cmdline_arg("cmd-arg1");
builder.add_cmdline_arg("cmd-arg2");
let args = builder
.build_zbi_args("bootfs", None::<String>, None::<String>, None::<String>, "output")
.unwrap();
assert_eq!(
args,
[
"--type=container",
"path/to/kernel",
"--type=cmdline",
"--entry=cmd-arg1",
"--entry=cmd-arg2",
"--files",
"bootfs",
"--output",
"output",
]
);
}
#[test]
fn zbi_args_with_compression() {
let tools = FakeToolProvider::default();
let zbi_tool = tools.get_tool("zbi").unwrap();
let mut builder = ZbiBuilder::new(zbi_tool);
builder.set_kernel("path/to/kernel");
builder.add_cmdline_arg("cmd-arg1");
builder.add_cmdline_arg("cmd-arg2");
builder.set_compression("zstd.max");
let args = builder
.build_zbi_args("bootfs", None::<String>, None::<String>, None::<String>, "output")
.unwrap();
assert_eq!(
args,
[
"--type=container",
"path/to/kernel",
"--type=cmdline",
"--entry=cmd-arg1",
"--entry=cmd-arg2",
"--files",
"bootfs",
"--compressed=zstd.max",
"--output",
"output",
]
);
}
#[test]
fn zbi_args_with_manifest() {
let tools = FakeToolProvider::default();
let zbi_tool = tools.get_tool("zbi").unwrap();
let mut builder = ZbiBuilder::new(zbi_tool);
builder.set_kernel("path/to/kernel");
builder.add_cmdline_arg("cmd-arg1");
builder.add_cmdline_arg("cmd-arg2");
builder.set_output_manifest("path/to/manifest");
let args = builder
.build_zbi_args("bootfs", None::<String>, None::<String>, None::<String>, "output")
.unwrap();
assert_eq!(
args,
[
"--type=container",
"path/to/kernel",
"--type=cmdline",
"--entry=cmd-arg1",
"--entry=cmd-arg2",
"--files",
"bootfs",
"--output",
"output",
"--json-output",
"path/to/manifest",
]
);
}
#[test]
fn zbi_args_with_board_driver_args() {
let tools = FakeToolProvider::default();
let zbi_tool = tools.get_tool("zbi").unwrap();
let mut builder = ZbiBuilder::new(zbi_tool);
builder.set_kernel("path/to/kernel");
builder.set_output_manifest("path/to/manifest");
let args = builder
.build_zbi_args(
"bootfs",
None::<String>,
Some("gen/platform_id_path"),
Some("gen/board_info_path"),
"output",
)
.unwrap();
assert_eq!(
args,
[
"--type=container",
"path/to/kernel",
"--type=cmdline",
"--type=platform_id",
"gen/platform_id_path",
"--type=drv_board_info",
"gen/board_info_path",
"--files",
"bootfs",
"--output",
"output",
"--json-output",
"path/to/manifest",
]
);
}
}