|
@@ -1,47 +1,66 @@
|
|
|
use std::{
|
|
|
+ env::consts::{ARCH, OS},
|
|
|
ffi::OsString,
|
|
|
fmt::Write as _,
|
|
|
- io::BufReader,
|
|
|
- path::PathBuf,
|
|
|
- process::{Child, Command, Stdio},
|
|
|
+ fs::{copy, create_dir_all, metadata, File},
|
|
|
+ io::{BufRead as _, BufReader, ErrorKind, Write as _},
|
|
|
+ path::{Path, PathBuf},
|
|
|
+ process::{Child, Command, Output, Stdio},
|
|
|
};
|
|
|
|
|
|
use anyhow::{anyhow, bail, Context as _, Result};
|
|
|
use cargo_metadata::{Artifact, CompilerMessage, Message, Target};
|
|
|
use clap::Parser;
|
|
|
-use xtask::AYA_BUILD_INTEGRATION_BPF;
|
|
|
+use xtask::{exec, AYA_BUILD_INTEGRATION_BPF};
|
|
|
|
|
|
-#[derive(Debug, Parser)]
|
|
|
-pub struct BuildOptions {
|
|
|
- /// Arguments to pass to `cargo build`.
|
|
|
- #[clap(long)]
|
|
|
- pub cargo_arg: Vec<OsString>,
|
|
|
+#[derive(Parser)]
|
|
|
+enum Environment {
|
|
|
+ /// Runs the integration tests locally.
|
|
|
+ Local {
|
|
|
+ /// The command used to wrap your application.
|
|
|
+ #[clap(short, long, default_value = "sudo -E")]
|
|
|
+ runner: String,
|
|
|
+ },
|
|
|
+ /// Runs the integration tests in a VM.
|
|
|
+ VM {
|
|
|
+ /// The kernel images to use.
|
|
|
+ ///
|
|
|
+ /// You can download some images with:
|
|
|
+ ///
|
|
|
+ /// wget --accept-regex '.*/linux-image-[0-9\.-]+-cloud-.*-unsigned*' \
|
|
|
+ /// --recursive ftp://ftp.us.debian.org/debian/pool/main/l/linux/
|
|
|
+ ///
|
|
|
+ /// You can then extract them with:
|
|
|
+ ///
|
|
|
+ /// find . -name '*.deb' -print0 \
|
|
|
+ /// | xargs -0 -I {} sh -c "dpkg --fsys-tarfile {} \
|
|
|
+ /// | tar --wildcards --extract '*vmlinuz*' --file -"
|
|
|
+ #[clap(required = true)]
|
|
|
+ kernel_image: Vec<PathBuf>,
|
|
|
+ },
|
|
|
}
|
|
|
|
|
|
-#[derive(Debug, Parser)]
|
|
|
+#[derive(Parser)]
|
|
|
pub struct Options {
|
|
|
- #[command(flatten)]
|
|
|
- pub build_options: BuildOptions,
|
|
|
- /// The command used to wrap your application.
|
|
|
- #[clap(short, long, default_value = "sudo -E")]
|
|
|
- pub runner: String,
|
|
|
+ #[clap(subcommand)]
|
|
|
+ environment: Environment,
|
|
|
/// Arguments to pass to your application.
|
|
|
- #[clap(last = true)]
|
|
|
- pub run_args: Vec<OsString>,
|
|
|
+ #[clap(global = true, last = true)]
|
|
|
+ run_args: Vec<OsString>,
|
|
|
}
|
|
|
|
|
|
-/// Build the project
|
|
|
-pub fn build(opts: BuildOptions) -> Result<Vec<(String, PathBuf)>> {
|
|
|
- let BuildOptions { cargo_arg } = opts;
|
|
|
+pub fn build<F>(target: Option<&str>, f: F) -> Result<Vec<(String, PathBuf)>>
|
|
|
+where
|
|
|
+ F: FnOnce(&mut Command) -> &mut Command,
|
|
|
+{
|
|
|
+ // Always use rust-lld and -Zbuild-std in case we're cross-compiling.
|
|
|
let mut cmd = Command::new("cargo");
|
|
|
- cmd.env(AYA_BUILD_INTEGRATION_BPF, "true")
|
|
|
- .args([
|
|
|
- "build",
|
|
|
- "--tests",
|
|
|
- "--message-format=json",
|
|
|
- "--package=integration-test",
|
|
|
- ])
|
|
|
- .args(cargo_arg);
|
|
|
+ cmd.args(["build", "--message-format=json"]);
|
|
|
+ if let Some(target) = target {
|
|
|
+ let config = format!("target.{target}.linker = \"rust-lld\"");
|
|
|
+ cmd.args(["--target", target, "--config", &config]);
|
|
|
+ }
|
|
|
+ f(&mut cmd);
|
|
|
|
|
|
let mut child = cmd
|
|
|
.stdout(Stdio::piped())
|
|
@@ -83,40 +102,405 @@ pub fn build(opts: BuildOptions) -> Result<Vec<(String, PathBuf)>> {
|
|
|
Ok(executables)
|
|
|
}
|
|
|
|
|
|
-/// Build and run the project
|
|
|
+#[derive(Debug)]
|
|
|
+struct Errors(Vec<anyhow::Error>);
|
|
|
+
|
|
|
+impl std::fmt::Display for Errors {
|
|
|
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
+ let Self(errors) = self;
|
|
|
+ for (i, error) in errors.iter().enumerate() {
|
|
|
+ if i != 0 {
|
|
|
+ writeln!(f)?;
|
|
|
+ }
|
|
|
+ write!(f, "{:?}", error)?;
|
|
|
+ }
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl std::error::Error for Errors {}
|
|
|
+
|
|
|
+/// Build and run the project.
|
|
|
pub fn run(opts: Options) -> Result<()> {
|
|
|
let Options {
|
|
|
- build_options,
|
|
|
- runner,
|
|
|
+ environment,
|
|
|
run_args,
|
|
|
} = opts;
|
|
|
|
|
|
- let binaries = build(build_options).context("error while building userspace application")?;
|
|
|
- let mut args = runner.trim().split_terminator(' ');
|
|
|
- let runner = args.next().ok_or(anyhow!("no first argument"))?;
|
|
|
- let args = args.collect::<Vec<_>>();
|
|
|
-
|
|
|
- let mut failures = String::new();
|
|
|
- for (name, binary) in binaries {
|
|
|
- let mut cmd = Command::new(runner);
|
|
|
- let cmd = cmd
|
|
|
- .args(args.iter())
|
|
|
- .arg(binary)
|
|
|
- .args(run_args.iter())
|
|
|
- .arg("--test-threads=1");
|
|
|
-
|
|
|
- println!("{name} running {cmd:?}");
|
|
|
-
|
|
|
- let status = cmd
|
|
|
- .status()
|
|
|
- .with_context(|| format!("failed to run {cmd:?}"))?;
|
|
|
- if status.code() != Some(0) {
|
|
|
- writeln!(&mut failures, "{name} failed: {status:?}").context("String write failed")?
|
|
|
- }
|
|
|
+ type Binary = (String, PathBuf);
|
|
|
+ fn binaries(target: Option<&str>) -> Result<Vec<(&str, Vec<Binary>)>> {
|
|
|
+ ["dev", "release"]
|
|
|
+ .into_iter()
|
|
|
+ .map(|profile| {
|
|
|
+ let binaries = build(target, |cmd| {
|
|
|
+ cmd.env(AYA_BUILD_INTEGRATION_BPF, "true").args([
|
|
|
+ "--package",
|
|
|
+ "integration-test",
|
|
|
+ "--tests",
|
|
|
+ "--profile",
|
|
|
+ profile,
|
|
|
+ ])
|
|
|
+ })?;
|
|
|
+ anyhow::Ok((profile, binaries))
|
|
|
+ })
|
|
|
+ .collect()
|
|
|
}
|
|
|
- if failures.is_empty() {
|
|
|
- Ok(())
|
|
|
- } else {
|
|
|
- Err(anyhow!("failures:\n{}", failures))
|
|
|
+
|
|
|
+ // Use --test-threads=1 to prevent tests from interacting with shared
|
|
|
+ // kernel state due to the lack of inter-test isolation.
|
|
|
+ let default_args = [OsString::from("--test-threads=1")];
|
|
|
+ let run_args = default_args.iter().chain(run_args.iter());
|
|
|
+
|
|
|
+ match environment {
|
|
|
+ Environment::Local { runner } => {
|
|
|
+ let mut args = runner.trim().split_terminator(' ');
|
|
|
+ let runner = args.next().ok_or(anyhow!("no first argument"))?;
|
|
|
+ let args = args.collect::<Vec<_>>();
|
|
|
+
|
|
|
+ let binaries = binaries(None)?;
|
|
|
+
|
|
|
+ let mut failures = String::new();
|
|
|
+ for (profile, binaries) in binaries {
|
|
|
+ for (name, binary) in binaries {
|
|
|
+ let mut cmd = Command::new(runner);
|
|
|
+ let cmd = cmd.args(args.iter()).arg(binary).args(run_args.clone());
|
|
|
+
|
|
|
+ println!("{profile}:{name} running {cmd:?}");
|
|
|
+
|
|
|
+ let status = cmd
|
|
|
+ .status()
|
|
|
+ .with_context(|| format!("failed to run {cmd:?}"))?;
|
|
|
+ if status.code() != Some(0) {
|
|
|
+ writeln!(&mut failures, "{profile}:{name} failed: {status:?}")
|
|
|
+ .context("String write failed")?
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if failures.is_empty() {
|
|
|
+ Ok(())
|
|
|
+ } else {
|
|
|
+ Err(anyhow!("failures:\n{}", failures))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Environment::VM { kernel_image } => {
|
|
|
+ // The user has asked us to run the tests on a VM. This is involved; strap in.
|
|
|
+ //
|
|
|
+ // We need tools to build the initramfs; we use gen_init_cpio from the Linux repository,
|
|
|
+ // taking care to cache it.
|
|
|
+ //
|
|
|
+ // Then we iterate the kernel images, using the `file` program to guess the target
|
|
|
+ // architecture. We then build the init program and our test binaries for that
|
|
|
+ // architecture, and use gen_init_cpio to build an initramfs containing the test
|
|
|
+ // binaries. We're almost ready to run the VM.
|
|
|
+ //
|
|
|
+ // We consult our OS, our architecture, and the target architecture to determine if
|
|
|
+ // hardware acceleration is available, and then start QEMU with the provided kernel
|
|
|
+ // image and the initramfs we built.
|
|
|
+ //
|
|
|
+ // We consume the output of QEMU, looking for the output of our init program. This is
|
|
|
+ // the only way to distinguish success from failure. We batch up the errors across all
|
|
|
+ // VM images and report to the user. The end.
|
|
|
+ let cache_dir = Path::new("test/.tmp");
|
|
|
+ create_dir_all(cache_dir).context("failed to create cache dir")?;
|
|
|
+ let gen_init_cpio = cache_dir.join("gen_init_cpio");
|
|
|
+ if !gen_init_cpio
|
|
|
+ .try_exists()
|
|
|
+ .context("failed to check existence of gen_init_cpio")?
|
|
|
+ {
|
|
|
+ let mut curl = Command::new("curl");
|
|
|
+ curl.args([
|
|
|
+ "-sfSL",
|
|
|
+ "https://raw.githubusercontent.com/torvalds/linux/master/usr/gen_init_cpio.c",
|
|
|
+ ]);
|
|
|
+ let mut curl_child = curl
|
|
|
+ .stdout(Stdio::piped())
|
|
|
+ .spawn()
|
|
|
+ .with_context(|| format!("failed to spawn {curl:?}"))?;
|
|
|
+ let Child { stdout, .. } = &mut curl_child;
|
|
|
+ let curl_stdout = stdout.take().unwrap();
|
|
|
+
|
|
|
+ let mut clang = Command::new("clang");
|
|
|
+ let clang = exec(
|
|
|
+ clang
|
|
|
+ .args(["-g", "-O2", "-x", "c", "-", "-o"])
|
|
|
+ .arg(&gen_init_cpio)
|
|
|
+ .stdin(curl_stdout),
|
|
|
+ );
|
|
|
+
|
|
|
+ let output = curl_child
|
|
|
+ .wait_with_output()
|
|
|
+ .with_context(|| format!("failed to wait for {curl:?}"))?;
|
|
|
+ let Output { status, .. } = &output;
|
|
|
+ if status.code() != Some(0) {
|
|
|
+ bail!("{curl:?} failed: {output:?}")
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check the result of clang *after* checking curl; in case the download failed,
|
|
|
+ // only curl's output will be useful.
|
|
|
+ clang?;
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut errors = Vec::new();
|
|
|
+ for kernel_image in kernel_image {
|
|
|
+ // Guess the guest architecture.
|
|
|
+ let mut cmd = Command::new("file");
|
|
|
+ let output = cmd
|
|
|
+ .arg("--brief")
|
|
|
+ .arg(&kernel_image)
|
|
|
+ .output()
|
|
|
+ .with_context(|| format!("failed to run {cmd:?}"))?;
|
|
|
+ let Output { status, .. } = &output;
|
|
|
+ if status.code() != Some(0) {
|
|
|
+ bail!("{cmd:?} failed: {output:?}")
|
|
|
+ }
|
|
|
+ let Output { stdout, .. } = output;
|
|
|
+
|
|
|
+ // Now parse the output of the file command, which looks something like
|
|
|
+ //
|
|
|
+ // - Linux kernel ARM64 boot executable Image, little-endian, 4K pages
|
|
|
+ //
|
|
|
+ // - Linux kernel x86 boot executable bzImage, version 6.1.0-10-cloud-amd64 [..]
|
|
|
+
|
|
|
+ let stdout = String::from_utf8(stdout)
|
|
|
+ .with_context(|| format!("invalid UTF-8 in {cmd:?} stdout"))?;
|
|
|
+ let (_, stdout) = stdout
|
|
|
+ .split_once("Linux kernel")
|
|
|
+ .ok_or_else(|| anyhow!("failed to parse {cmd:?} stdout: {stdout}"))?;
|
|
|
+ let (guest_arch, _) = stdout
|
|
|
+ .split_once("boot executable")
|
|
|
+ .ok_or_else(|| anyhow!("failed to parse {cmd:?} stdout: {stdout}"))?;
|
|
|
+ let guest_arch = guest_arch.trim();
|
|
|
+
|
|
|
+ let (guest_arch, machine, cpu) = match guest_arch {
|
|
|
+ "ARM64" => ("aarch64", Some("virt"), Some("cortex-a57")),
|
|
|
+ "x86" => ("x86_64", Some("q35"), Some("qemu64")),
|
|
|
+ guest_arch => (guest_arch, None, None),
|
|
|
+ };
|
|
|
+
|
|
|
+ let target = format!("{guest_arch}-unknown-linux-musl");
|
|
|
+
|
|
|
+ // Build our init program. The contract is that it will run anything it finds in /bin.
|
|
|
+ let init = build(Some(&target), |cmd| {
|
|
|
+ cmd.args(["--package", "init", "--profile", "release"])
|
|
|
+ })
|
|
|
+ .context("building init program failed")?;
|
|
|
+
|
|
|
+ let init = match &*init {
|
|
|
+ [(name, init)] => {
|
|
|
+ if name != "init" {
|
|
|
+ bail!("expected init program to be named init, found {name}")
|
|
|
+ }
|
|
|
+ init
|
|
|
+ }
|
|
|
+ init => bail!("expected exactly one init program, found {init:?}"),
|
|
|
+ };
|
|
|
+
|
|
|
+ let binaries = binaries(Some(&target))?;
|
|
|
+
|
|
|
+ let tmp_dir = tempfile::tempdir().context("tempdir failed")?;
|
|
|
+
|
|
|
+ let initrd_image = tmp_dir.path().join("qemu-initramfs.img");
|
|
|
+ let initrd_image_file = File::create(&initrd_image).with_context(|| {
|
|
|
+ format!("failed to create {} for writing", initrd_image.display())
|
|
|
+ })?;
|
|
|
+
|
|
|
+ let mut gen_init_cpio = Command::new(&gen_init_cpio);
|
|
|
+ let mut gen_init_cpio_child = gen_init_cpio
|
|
|
+ .arg("-")
|
|
|
+ .stdin(Stdio::piped())
|
|
|
+ .stdout(initrd_image_file)
|
|
|
+ .spawn()
|
|
|
+ .with_context(|| format!("failed to spawn {gen_init_cpio:?}"))?;
|
|
|
+ let Child { stdin, .. } = &mut gen_init_cpio_child;
|
|
|
+ let mut stdin = stdin.take().unwrap();
|
|
|
+
|
|
|
+ use std::os::unix::ffi::OsStrExt as _;
|
|
|
+
|
|
|
+ // Send input into gen_init_cpio which looks something like
|
|
|
+ //
|
|
|
+ // file /init path-to-init 0755 0 0
|
|
|
+ // dir /bin 0755 0 0
|
|
|
+ // file /bin/foo path-to-foo 0755 0 0
|
|
|
+ // file /bin/bar path-to-bar 0755 0 0
|
|
|
+
|
|
|
+ for bytes in [
|
|
|
+ "file /init ".as_bytes(),
|
|
|
+ init.as_os_str().as_bytes(),
|
|
|
+ " 0755 0 0\n".as_bytes(),
|
|
|
+ "dir /bin 0755 0 0\n".as_bytes(),
|
|
|
+ ] {
|
|
|
+ stdin.write_all(bytes).expect("write");
|
|
|
+ }
|
|
|
+
|
|
|
+ for (profile, binaries) in binaries {
|
|
|
+ for (name, binary) in binaries {
|
|
|
+ let name = format!("{}-{}", profile, name);
|
|
|
+ let path = tmp_dir.path().join(&name);
|
|
|
+ copy(&binary, &path).with_context(|| {
|
|
|
+ format!("copy({}, {}) failed", binary.display(), path.display())
|
|
|
+ })?;
|
|
|
+ for bytes in [
|
|
|
+ "file /bin/".as_bytes(),
|
|
|
+ name.as_bytes(),
|
|
|
+ " ".as_bytes(),
|
|
|
+ path.as_os_str().as_bytes(),
|
|
|
+ " 0755 0 0\n".as_bytes(),
|
|
|
+ ] {
|
|
|
+ stdin.write_all(bytes).expect("write");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Must explicitly close to signal EOF.
|
|
|
+ drop(stdin);
|
|
|
+
|
|
|
+ let output = gen_init_cpio_child
|
|
|
+ .wait_with_output()
|
|
|
+ .with_context(|| format!("failed to wait for {gen_init_cpio:?}"))?;
|
|
|
+ let Output { status, .. } = &output;
|
|
|
+ if status.code() != Some(0) {
|
|
|
+ bail!("{gen_init_cpio:?} failed: {output:?}")
|
|
|
+ }
|
|
|
+
|
|
|
+ copy(&initrd_image, "/tmp/initrd.img").context("copy failed")?;
|
|
|
+
|
|
|
+ let mut qemu = Command::new(format!("qemu-system-{guest_arch}"));
|
|
|
+ if let Some(machine) = machine {
|
|
|
+ qemu.args(["-machine", machine]);
|
|
|
+ }
|
|
|
+ if guest_arch == ARCH {
|
|
|
+ match OS {
|
|
|
+ "linux" => match metadata("/dev/kvm") {
|
|
|
+ Ok(metadata) => {
|
|
|
+ use std::os::unix::fs::FileTypeExt as _;
|
|
|
+ if metadata.file_type().is_char_device() {
|
|
|
+ qemu.args(["-accel", "kvm"]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Err(error) => {
|
|
|
+ if error.kind() != ErrorKind::NotFound {
|
|
|
+ Err(error).context("failed to check existence of /dev/kvm")?;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "macos" => {
|
|
|
+ qemu.args(["-accel", "hvf"]);
|
|
|
+ }
|
|
|
+ os => bail!("unsupported OS: {os}"),
|
|
|
+ }
|
|
|
+ } else if let Some(cpu) = cpu {
|
|
|
+ qemu.args(["-cpu", cpu]);
|
|
|
+ }
|
|
|
+ let console = OsString::from("ttyS0");
|
|
|
+ let kernel_args = std::iter::once(("console", &console))
|
|
|
+ .chain(run_args.clone().map(|run_arg| ("init.arg", run_arg)))
|
|
|
+ .enumerate()
|
|
|
+ .fold(OsString::new(), |mut acc, (i, (k, v))| {
|
|
|
+ if i != 0 {
|
|
|
+ acc.push(" ");
|
|
|
+ }
|
|
|
+ acc.push(k);
|
|
|
+ acc.push("=");
|
|
|
+ acc.push(v);
|
|
|
+ acc
|
|
|
+ });
|
|
|
+ qemu.args(["-no-reboot", "-nographic", "-m", "512M", "-smp", "2"])
|
|
|
+ .arg("-append")
|
|
|
+ .arg(kernel_args)
|
|
|
+ .arg("-kernel")
|
|
|
+ .arg(&kernel_image)
|
|
|
+ .arg("-initrd")
|
|
|
+ .arg(&initrd_image);
|
|
|
+ if guest_arch == "aarch64" {
|
|
|
+ match OS {
|
|
|
+ "linux" => {
|
|
|
+ let mut cmd = Command::new("locate");
|
|
|
+ let output = cmd
|
|
|
+ .arg("QEMU_EFI.fd")
|
|
|
+ .output()
|
|
|
+ .with_context(|| format!("failed to run {cmd:?}"))?;
|
|
|
+ let Output { status, .. } = &output;
|
|
|
+ if status.code() != Some(0) {
|
|
|
+ bail!("{qemu:?} failed: {output:?}")
|
|
|
+ }
|
|
|
+ let Output { stdout, .. } = output;
|
|
|
+ let bios = String::from_utf8(stdout)
|
|
|
+ .with_context(|| format!("failed to parse output of {cmd:?}"))?;
|
|
|
+ qemu.args(["-bios", bios.trim()]);
|
|
|
+ }
|
|
|
+ "macos" => {
|
|
|
+ let mut cmd = Command::new("brew");
|
|
|
+ let output = cmd
|
|
|
+ .args(["list", "qemu", "-1", "-v"])
|
|
|
+ .output()
|
|
|
+ .with_context(|| format!("failed to run {cmd:?}"))?;
|
|
|
+ let Output { status, .. } = &output;
|
|
|
+ if status.code() != Some(0) {
|
|
|
+ bail!("{qemu:?} failed: {output:?}")
|
|
|
+ }
|
|
|
+ let Output { stdout, .. } = output;
|
|
|
+ let output = String::from_utf8(stdout)
|
|
|
+ .with_context(|| format!("failed to parse output of {cmd:?}"))?;
|
|
|
+ const NAME: &str = "edk2-aarch64-code.fd";
|
|
|
+ let bios = output.lines().find(|line| line.contains(NAME)).ok_or_else(
|
|
|
+ || anyhow!("failed to find {NAME} in output of {cmd:?}: {output}"),
|
|
|
+ )?;
|
|
|
+ qemu.args(["-bios", bios.trim()]);
|
|
|
+ }
|
|
|
+ os => bail!("unsupported OS: {os}"),
|
|
|
+ };
|
|
|
+ }
|
|
|
+ let mut qemu_child = qemu
|
|
|
+ .stdout(Stdio::piped())
|
|
|
+ .spawn()
|
|
|
+ .with_context(|| format!("failed to spawn {qemu:?}"))?;
|
|
|
+ let Child { stdout, .. } = &mut qemu_child;
|
|
|
+ let stdout = stdout.take().unwrap();
|
|
|
+ let stdout = BufReader::new(stdout);
|
|
|
+
|
|
|
+ let mut outcome = None;
|
|
|
+ for line in stdout.lines() {
|
|
|
+ let line =
|
|
|
+ line.with_context(|| format!("failed to read line from {qemu:?}"))?;
|
|
|
+ println!("{}", line);
|
|
|
+ // The init program will print "init: success" or "init: failure" to indicate
|
|
|
+ // the outcome of running the binaries it found in /bin.
|
|
|
+ if let Some(line) = line.strip_prefix("init: ") {
|
|
|
+ let previous = match line {
|
|
|
+ "success" => outcome.replace(Ok(())),
|
|
|
+ "failure" => outcome.replace(Err(())),
|
|
|
+ line => bail!("unexpected init output: {}", line),
|
|
|
+ };
|
|
|
+ if let Some(previous) = previous {
|
|
|
+ bail!("multiple exit status: previous={previous:?}, current={line}");
|
|
|
+ }
|
|
|
+ // Try to get QEMU to exit on kernel panic; otherwise it might hang indefinitely.
|
|
|
+ if line.contains("end Kernel panic") {
|
|
|
+ qemu_child.kill().context("failed to kill {qemu:?}")?;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let output = qemu_child
|
|
|
+ .wait_with_output()
|
|
|
+ .with_context(|| format!("failed to wait for {qemu:?}"))?;
|
|
|
+ let Output { status, .. } = &output;
|
|
|
+ if status.code() != Some(0) {
|
|
|
+ bail!("{qemu:?} failed: {output:?}")
|
|
|
+ }
|
|
|
+
|
|
|
+ let outcome = outcome.ok_or(anyhow!("init did not exit"))?;
|
|
|
+ match outcome {
|
|
|
+ Ok(()) => {}
|
|
|
+ Err(()) => {
|
|
|
+ errors.push(anyhow!("VM binaries failed on {}", kernel_image.display()))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if errors.is_empty() {
|
|
|
+ Ok(())
|
|
|
+ } else {
|
|
|
+ Err(Errors(errors).into())
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|