[assembly] Assemble the ZBI

1. Generate the merkle root for the base package
2. Create a ZbiBuilder
3. Pass the command-line arguments (with the merkle) to ZbiBuilder
4. Pass the kernel to ZbiBuilder
5. Pass the bootfs files to ZbiBuilder
6. Build the ZBI with ZbiBuilder

Test: fx test zbi_test ffx_assembly_test
Bug: 73175
Change-Id: Ia770e3c2d65894a2298ce4f6599fc70931d0a072
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/521135
Reviewed-by: Aaron Wood <aaronwood@google.com>
Commit-Queue: Aidan Wolter <awolter@google.com>
Fuchsia-Auto-Submit: Aidan Wolter <awolter@google.com>
diff --git a/src/developer/ffx/plugins/assembly/BUILD.gn b/src/developer/ffx/plugins/assembly/BUILD.gn
index 87c8bee..320757c 100644
--- a/src/developer/ffx/plugins/assembly/BUILD.gn
+++ b/src/developer/ffx/plugins/assembly/BUILD.gn
@@ -21,8 +21,11 @@
   deps = [
     "//src/lib/assembly/base_package",
     "//src/lib/assembly/vbmeta",
+    "//src/lib/assembly/zbi",
     "//src/lib/zerocopy",
     "//src/sys/pkg/lib/far/rust:fuchsia-archive",
+    "//src/sys/pkg/lib/fuchsia-hash",
+    "//src/sys/pkg/lib/fuchsia-merkle",
     "//src/sys/pkg/lib/fuchsia-pkg",
     "//third_party/rust_crates:hex",
     "//third_party/rust_crates:serde",
diff --git a/src/developer/ffx/plugins/assembly/src/operations/image.rs b/src/developer/ffx/plugins/assembly/src/operations/image.rs
index 3b9dc11..52c3bdb 100644
--- a/src/developer/ffx/plugins/assembly/src/operations/image.rs
+++ b/src/developer/ffx/plugins/assembly/src/operations/image.rs
@@ -4,26 +4,34 @@
 
 pub mod config;
 
-use anyhow::Result;
+use anyhow::{Context, Result};
 use assembly_base_package::BasePackageBuilder;
 use config::Config;
 use ffx_assembly_args::ImageArgs;
-use ffx_core::ffx_bail;
+use ffx_core::{ffx_bail, ffx_error};
+use fuchsia_hash::Hash;
+use fuchsia_merkle::MerkleTree;
 use fuchsia_pkg::PackageManifest;
-use std::fs::File;
+use std::fs::{File, OpenOptions};
 use std::io::BufReader;
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
+use zbi::ZbiBuilder;
 
 pub fn assemble(args: ImageArgs) -> Result<()> {
     let config = read_config(&args.config)?;
-    let _base_package = construct_base_package(&args.gendir, &config)?;
+    let base_package = construct_base_package(&args.gendir, &config)?;
+    let base_merkle = MerkleTree::from_reader(&base_package)
+        .context("Failed to calculate the base merkle")?
+        .root();
+    println!("Base merkle: {}", base_merkle);
+    let _zbi = construct_zbi(&args.gendir, &config, Some(base_merkle))?;
+
     Ok(())
 }
 
 fn read_config(config_path: &String) -> Result<Config> {
     let mut config = File::open(config_path)?;
-    let config = Config::from_reader(&mut config)
-        .or_else(|e| ffx_bail!("Failed to read the image config: {}", e))?;
+    let config = Config::from_reader(&mut config).context("Failed to read the image config")?;
     println!("Config indicated version: {}", config.version);
     Ok(config)
 }
@@ -42,7 +50,11 @@
         let pkg_manifest = pkg_manifest_from_path(pkg_manifest_path);
         base_pkg_builder.add_cache_package(pkg_manifest);
     }
-    let mut base_package = File::create("base.far")
+    let mut base_package = OpenOptions::new()
+        .read(true)
+        .write(true)
+        .create(true)
+        .open("base.far")
         .or_else(|e| ffx_bail!("Failed to create the base package file: {}", e))?;
     base_pkg_builder
         .build(gendir, &mut base_package)
@@ -51,8 +63,58 @@
     Ok(base_package)
 }
 
-fn pkg_manifest_from_path(path: &String) -> PackageManifest {
-    let manifest_file = std::fs::File::open(path).unwrap();
+fn pkg_manifest_from_path(path: &str) -> PackageManifest {
+    let manifest_file = File::open(path).unwrap();
     let pkg_manifest_reader = BufReader::new(manifest_file);
     serde_json::from_reader(pkg_manifest_reader).unwrap()
 }
+
+fn construct_zbi(gendir: &PathBuf, config: &Config, base_merkle: Option<Hash>) -> Result<File> {
+    let mut zbi_builder = ZbiBuilder::default();
+
+    // Add the kernel image.
+    zbi_builder.set_kernel(&config.kernel_image);
+
+    // Instruct devmgr that a /system volume is required.
+    zbi_builder.add_boot_arg("devmgr.require-system=true");
+
+    // If a base merkle is supplied, then add the boot arguments for startup up pkgfs with the
+    // merkle of the Base Package.
+    if let Some(base_merkle) = base_merkle {
+        // Specify how to launch pkgfs: bin/pkgsvr <base-merkle>
+        zbi_builder.add_boot_arg(&format!("zircon.system.pkgfs.cmd=bin/pkgsvr+{}", base_merkle));
+
+        // Add the pkgfs blobs to the boot arguments, so that pkgfs can be bootstrapped out of blobfs,
+        // before the blobfs service is available.
+        let pkgfs_manifest: PackageManifest = config
+            .base_packages
+            .iter()
+            .map(String::as_str)
+            .map(pkg_manifest_from_path)
+            .find(|m| m.name() == "pkgfs")
+            .ok_or_else(|| ffx_error!("Failed to find pkgfs in the base packages"))?;
+
+        pkgfs_manifest.into_blobs().into_iter().filter(|b| b.path != "meta/").for_each(|b| {
+            zbi_builder.add_boot_arg(&format!("zircon.system.pkgfs.file.{}={}", b.path, b.merkle));
+        });
+    }
+
+    // Add the command line.
+    for cmd in &config.kernel_cmdline {
+        zbi_builder.add_cmdline_arg(cmd);
+    }
+
+    // Add the BootFS files.
+    for bootfs_entry in &config.bootfs_files {
+        zbi_builder.add_bootfs_file(&bootfs_entry.source, &bootfs_entry.destination);
+    }
+
+    // Build and return the ZBI.
+    zbi_builder.build(gendir, Path::new("myfuchsia.zbi"))?;
+    let zbi = OpenOptions::new()
+        .read(true)
+        .open("myfuchsia.zbi")
+        .or_else(|e| ffx_bail!("Failed to open the zbi: {}", e))?;
+    println!("ZBI: myfuchsia.zbi");
+    Ok(zbi)
+}
diff --git a/src/developer/ffx/plugins/assembly/src/operations/image/config.rs b/src/developer/ffx/plugins/assembly/src/operations/image/config.rs
index 1c744cd..6b4bcba 100644
--- a/src/developer/ffx/plugins/assembly/src/operations/image/config.rs
+++ b/src/developer/ffx/plugins/assembly/src/operations/image/config.rs
@@ -16,7 +16,7 @@
     pub meta_packages: Vec<String>,
     pub kernel_image: String,
     pub kernel_cmdline: Vec<String>,
-    pub bootfs_files: Vec<String>,
+    pub bootfs_files: Vec<BootFsEntry>,
     pub vbmeta_key: String,
     pub vbmeta_key_metadata: String,
     pub version: String,
@@ -24,6 +24,12 @@
 }
 
 #[derive(Deserialize, Serialize)]
+pub(crate) struct BootFsEntry {
+    pub source: String,
+    pub destination: String,
+}
+
+#[derive(Deserialize, Serialize)]
 pub(crate) struct BoardConfig {
     pub name: String,
     pub bootloader: String,
@@ -70,7 +76,12 @@
               meta_packages: ["package5", "package6"],
               kernel_image: "path/to/kernel",
               kernel_cmdline: ["arg1", "arg2"],
-              bootfs_files: ["file1", "file2"],
+              bootfs_files: [
+                {
+                    source: "path/to/source",
+                    destination: "path/to/destination",
+                },
+              ],
               vbmeta_key: "key",
               vbmeta_key_metadata: "metadata",
               version: "0.1.2",
diff --git a/src/lib/assembly/BUILD.gn b/src/lib/assembly/BUILD.gn
index 741734e7..0d4c5e7 100644
--- a/src/lib/assembly/BUILD.gn
+++ b/src/lib/assembly/BUILD.gn
@@ -7,5 +7,6 @@
   deps = [
     "base_package:tests",
     "vbmeta:tests",
+    "zbi:tests",
   ]
 }
diff --git a/src/lib/assembly/zbi/BUILD.gn b/src/lib/assembly/zbi/BUILD.gn
new file mode 100644
index 0000000..02f69a5
--- /dev/null
+++ b/src/lib/assembly/zbi/BUILD.gn
@@ -0,0 +1,23 @@
+# 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.
+
+import("//build/rust/rustc_library.gni")
+
+rustc_library("zbi") {
+  version = "0.1.0"
+  with_unit_tests = true
+  deps = [
+    "//third_party/rust_crates:anyhow",
+    "//third_party/rust_crates:tempfile",
+  ]
+  sources = [
+    "src/lib.rs",
+    "src/zbi.rs",
+  ]
+}
+
+group("tests") {
+  testonly = true
+  deps = [ ":zbi_test($host_toolchain)" ]
+}
diff --git a/src/lib/assembly/zbi/src/lib.rs b/src/lib/assembly/zbi/src/lib.rs
new file mode 100644
index 0000000..517dc31
--- /dev/null
+++ b/src/lib/assembly/zbi/src/lib.rs
@@ -0,0 +1,11 @@
+// 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.
+
+#![deny(missing_docs)]
+
+//! Library for constructing the ZBI.
+
+mod zbi;
+
+pub use zbi::ZbiBuilder;
diff --git a/src/lib/assembly/zbi/src/zbi.rs b/src/lib/assembly/zbi/src/zbi.rs
new file mode 100644
index 0000000..28455fe
--- /dev/null
+++ b/src/lib/assembly/zbi/src/zbi.rs
@@ -0,0 +1,217 @@
+// 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::{Error, Result};
+use std::collections::BTreeMap;
+use std::fs::File;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+/// Builder for the Zircon Boot Image (ZBI), which takes in a kernel, BootFS, boot args, and kernel
+/// command line.
+#[derive(Default)]
+pub struct ZbiBuilder {
+    kernel: Option<String>,
+    bootfs_files: BTreeMap<String, String>,
+    bootargs: Vec<String>,
+    cmdline: Vec<String>,
+}
+
+impl ZbiBuilder {
+    /// Set the kernel to be used.
+    pub fn set_kernel(&mut self, kernel: &str) {
+        self.kernel = Some(kernel.to_string());
+    }
+
+    /// Add a BootFS file to the ZBI.
+    pub fn add_bootfs_file(&mut self, source: &str, destination: &str) {
+        if self.bootfs_files.contains_key(&destination.to_string()) {
+            println!("Found duplicate bootfs destination: {}", destination);
+            return;
+        }
+        self.bootfs_files.insert(destination.to_string(), source.to_string());
+    }
+
+    /// 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());
+    }
+
+    /// Build the ZBI.
+    pub fn build(&self, gendir: &PathBuf, output: &Path) -> Result<()> {
+        // Create the BootFS manifest file that lists all the files to insert
+        // into BootFS.
+        let mut bootfs_manifest_path = gendir.clone();
+        bootfs_manifest_path.push("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(&mut bootfs_manifest)?;
+
+        // Create the boot args file.
+        let mut boot_args_path = gendir.clone();
+        boot_args_path.push("boot_args.txt");
+        let mut boot_args = File::create(&boot_args_path)
+            .map_err(|e| Error::new(e).context("failed to create the boot args"))?;
+        self.write_boot_args(&mut boot_args)?;
+
+        // Run the zbi tool to construct the ZBI.
+        let zbi_args = self.build_zbi_args(&bootfs_manifest_path, &boot_args_path, output)?;
+        let status = Command::new("host_x64/zbi")
+            .args(&zbi_args)
+            .status()
+            .expect("Failed to run the zbi tool");
+        if !status.success() {
+            anyhow::bail!("zbi exited with status: {}", status);
+        }
+
+        Ok(())
+    }
+
+    fn write_bootfs_manifest(&self, out: &mut impl Write) -> Result<()> {
+        for (destination, source) in &self.bootfs_files {
+            write!(out, "{}={}\n", destination, source)?;
+        }
+        Ok(())
+    }
+
+    fn write_boot_args(&self, out: &mut impl Write) -> Result<()> {
+        for arg in &self.bootargs {
+            write!(out, "{}\n", arg)?;
+        }
+        Ok(())
+    }
+
+    fn build_zbi_args(
+        &self,
+        bootfs_manifest_path: &Path,
+        boot_args_path: &Path,
+        output_path: &Path,
+    ) -> Result<Vec<String>> {
+        // Ensure the supplied kernel is a valid file.
+        let kernel_path;
+        if let Some(kernel) = &self.kernel {
+            kernel_path = Path::new(kernel);
+        } else {
+            anyhow::bail!("No kernel image supplied");
+        }
+
+        let mut args: Vec<String> = Vec::new();
+        args.push("--type=container".to_string());
+        args.push(kernel_path.to_string_lossy().into_owned());
+        args.push("--files".to_string());
+        args.push(bootfs_manifest_path.to_string_lossy().into_owned());
+        args.push("--type=image_args".to_string());
+        args.push(format!("--entry={}", boot_args_path.to_string_lossy()));
+        args.push("--type=cmdline".to_string());
+        for cmd in &self.cmdline {
+            args.push(format!("--entry={}", cmd));
+        }
+        args.push(format!("--output={}", output_path.to_string_lossy()));
+        Ok(args)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn bootfs_manifest() {
+        let mut builder = ZbiBuilder::default();
+        let mut output: Vec<u8> = Vec::new();
+
+        builder.write_bootfs_manifest(&mut output).unwrap();
+        assert_eq!(output, b"");
+
+        output.clear();
+        builder.add_bootfs_file("path/to/file2", "bin/file2");
+        builder.add_bootfs_file("path/to/file1", "lib/file1");
+        builder.write_bootfs_manifest(&mut output).unwrap();
+        assert_eq!(output, b"bin/file2=path/to/file2\nlib/file1=path/to/file1\n");
+    }
+
+    #[test]
+    fn boot_args() {
+        let mut builder = ZbiBuilder::default();
+        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 bootfs_manifest = Path::new("bootfs");
+        let boot_args = Path::new("bootargs");
+        let output = Path::new("output");
+        let builder = ZbiBuilder::default();
+
+        // We should fail without a kernel.
+        assert!(builder.build_zbi_args(bootfs_manifest, boot_args, output).is_err());
+    }
+
+    #[test]
+    fn zbi_args_with_kernel() {
+        let bootfs_manifest = Path::new("bootfs");
+        let boot_args = Path::new("bootargs");
+        let output = Path::new("output");
+        let mut builder = ZbiBuilder::default();
+
+        builder.set_kernel("path/to/kernel");
+        let args = builder.build_zbi_args(bootfs_manifest, boot_args, output).unwrap();
+        assert_eq!(
+            args,
+            [
+                "--type=container",
+                "path/to/kernel",
+                "--files",
+                "bootfs",
+                "--type=image_args",
+                "--entry=bootargs",
+                "--type=cmdline",
+                "--output=output",
+            ]
+        );
+    }
+
+    #[test]
+    fn zbi_args_with_cmdline() {
+        let bootfs_manifest = Path::new("bootfs");
+        let boot_args = Path::new("bootargs");
+        let output = Path::new("output");
+        let mut builder = ZbiBuilder::default();
+
+        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_manifest, boot_args, output).unwrap();
+        assert_eq!(
+            args,
+            [
+                "--type=container",
+                "path/to/kernel",
+                "--files",
+                "bootfs",
+                "--type=image_args",
+                "--entry=bootargs",
+                "--type=cmdline",
+                "--entry=cmd-arg1",
+                "--entry=cmd-arg2",
+                "--output=output",
+            ]
+        );
+    }
+}