|  | @@ -1,10 +1,10 @@
 | 
	
		
			
				|  |  |  use std::{
 | 
	
		
			
				|  |  | -    ffi::OsString,
 | 
	
		
			
				|  |  | +    ffi::{OsStr, OsString},
 | 
	
		
			
				|  |  |      fmt::Write as _,
 | 
	
		
			
				|  |  |      fs::{self, OpenOptions},
 | 
	
		
			
				|  |  |      io::{BufRead as _, BufReader, Write as _},
 | 
	
		
			
				|  |  |      ops::Deref as _,
 | 
	
		
			
				|  |  | -    path::{Path, PathBuf},
 | 
	
		
			
				|  |  | +    path::{self, Path, PathBuf},
 | 
	
		
			
				|  |  |      process::{Child, ChildStdin, Command, Output, Stdio},
 | 
	
		
			
				|  |  |      sync::{Arc, Mutex},
 | 
	
		
			
				|  |  |      thread,
 | 
	
	
		
			
				|  | @@ -38,48 +38,12 @@ enum Environment {
 | 
	
		
			
				|  |  |          #[clap(long)]
 | 
	
		
			
				|  |  |          github_api_token: Option<String>,
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        /// The kernel image and modules to use.
 | 
	
		
			
				|  |  | -        ///
 | 
	
		
			
				|  |  | -        /// Format: </path/to/image/vmlinuz>:</path/to/lib/modules>
 | 
	
		
			
				|  |  | -        ///
 | 
	
		
			
				|  |  | -        /// You can download some images with:
 | 
	
		
			
				|  |  | -        ///
 | 
	
		
			
				|  |  | -        /// wget --accept-regex '.*/linux-image-[0-9\.-]+-cloud-.*-unsigned*' \
 | 
	
		
			
				|  |  | -        ///   --recursive http://ftp.us.debian.org/debian/pool/main/l/linux/
 | 
	
		
			
				|  |  | -        ///
 | 
	
		
			
				|  |  | -        /// You can then extract the images and kernel modules with:
 | 
	
		
			
				|  |  | -        ///
 | 
	
		
			
				|  |  | -        /// find . -name '*.deb' -print0 \
 | 
	
		
			
				|  |  | -        /// | xargs -0 -I {} sh -c "dpkg --fsys-tarfile {} \
 | 
	
		
			
				|  |  | -        ///   | tar --wildcards --extract '**/boot/*' '**/modules/*' --file -"
 | 
	
		
			
				|  |  | -        ///
 | 
	
		
			
				|  |  | -        /// `**/boot/*` is used to extract the kernel image and config.
 | 
	
		
			
				|  |  | -        ///
 | 
	
		
			
				|  |  | -        /// `**/modules/*` is used to extract the kernel modules.
 | 
	
		
			
				|  |  | -        ///
 | 
	
		
			
				|  |  | -        /// Modules are required since not all parts of the kernel we want to
 | 
	
		
			
				|  |  | -        /// test are built-in.
 | 
	
		
			
				|  |  | -        #[clap(required = true, value_parser=parse_image_and_modules)]
 | 
	
		
			
				|  |  | -        image_and_modules: Vec<(PathBuf, PathBuf)>,
 | 
	
		
			
				|  |  | +        /// Debian kernel archives (.deb) to boot in the VM.
 | 
	
		
			
				|  |  | +        #[clap(required = true)]
 | 
	
		
			
				|  |  | +        kernel_archives: Vec<PathBuf>,
 | 
	
		
			
				|  |  |      },
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -pub(crate) fn parse_image_and_modules(s: &str) -> Result<(PathBuf, PathBuf), std::io::Error> {
 | 
	
		
			
				|  |  | -    let mut parts = s.split(':');
 | 
	
		
			
				|  |  | -    let image = parts
 | 
	
		
			
				|  |  | -        .next()
 | 
	
		
			
				|  |  | -        .ok_or(std::io::ErrorKind::InvalidInput)
 | 
	
		
			
				|  |  | -        .map(PathBuf::from)?;
 | 
	
		
			
				|  |  | -    let modules = parts
 | 
	
		
			
				|  |  | -        .next()
 | 
	
		
			
				|  |  | -        .ok_or(std::io::ErrorKind::InvalidInput)
 | 
	
		
			
				|  |  | -        .map(PathBuf::from)?;
 | 
	
		
			
				|  |  | -    if parts.next().is_some() {
 | 
	
		
			
				|  |  | -        return Err(std::io::ErrorKind::InvalidInput.into());
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -    Ok((image, modules))
 | 
	
		
			
				|  |  | -}
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |  #[derive(Parser)]
 | 
	
		
			
				|  |  |  pub(crate) struct Options {
 | 
	
		
			
				|  |  |      #[clap(subcommand)]
 | 
	
	
		
			
				|  | @@ -212,7 +176,7 @@ pub(crate) fn run(opts: Options) -> Result<()> {
 | 
	
		
			
				|  |  |          Environment::VM {
 | 
	
		
			
				|  |  |              cache_dir,
 | 
	
		
			
				|  |  |              github_api_token,
 | 
	
		
			
				|  |  | -            image_and_modules,
 | 
	
		
			
				|  |  | +            kernel_archives,
 | 
	
		
			
				|  |  |          } => {
 | 
	
		
			
				|  |  |              // The user has asked us to run the tests on a VM. This is involved; strap in.
 | 
	
		
			
				|  |  |              //
 | 
	
	
		
			
				|  | @@ -320,13 +284,114 @@ pub(crate) fn run(opts: Options) -> Result<()> {
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +            let extraction_root = tempfile::tempdir().context("tempdir failed")?;
 | 
	
		
			
				|  |  |              let mut errors = Vec::new();
 | 
	
		
			
				|  |  | -            for (kernel_image, modules_dir) in image_and_modules {
 | 
	
		
			
				|  |  | +            for (index, archive) in kernel_archives.iter().enumerate() {
 | 
	
		
			
				|  |  | +                let archive_dir = extraction_root
 | 
	
		
			
				|  |  | +                    .path()
 | 
	
		
			
				|  |  | +                    .join(format!("kernel-archive-{index}"));
 | 
	
		
			
				|  |  | +                fs::create_dir_all(&archive_dir)
 | 
	
		
			
				|  |  | +                    .with_context(|| format!("failed to create {}", archive_dir.display()))?;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                let mut dpkg = Command::new("dpkg-deb");
 | 
	
		
			
				|  |  | +                dpkg.arg("--fsys-tarfile")
 | 
	
		
			
				|  |  | +                    .arg(archive)
 | 
	
		
			
				|  |  | +                    .stdout(Stdio::piped());
 | 
	
		
			
				|  |  | +                let mut dpkg_child = dpkg
 | 
	
		
			
				|  |  | +                    .spawn()
 | 
	
		
			
				|  |  | +                    .with_context(|| format!("failed to spawn {dpkg:?}"))?;
 | 
	
		
			
				|  |  | +                let Child { stdout, .. } = &mut dpkg_child;
 | 
	
		
			
				|  |  | +                let stdout = stdout.take().unwrap();
 | 
	
		
			
				|  |  | +                let mut archive_reader = tar::Archive::new(stdout);
 | 
	
		
			
				|  |  | +                archive_reader.unpack(&archive_dir).with_context(|| {
 | 
	
		
			
				|  |  | +                    format!(
 | 
	
		
			
				|  |  | +                        "failed to unpack archive {} to {}",
 | 
	
		
			
				|  |  | +                        archive.display(),
 | 
	
		
			
				|  |  | +                        archive_dir.display()
 | 
	
		
			
				|  |  | +                    )
 | 
	
		
			
				|  |  | +                })?;
 | 
	
		
			
				|  |  | +                let status = dpkg_child
 | 
	
		
			
				|  |  | +                    .wait()
 | 
	
		
			
				|  |  | +                    .with_context(|| format!("failed to wait for {dpkg:?}"))?;
 | 
	
		
			
				|  |  | +                if !status.success() {
 | 
	
		
			
				|  |  | +                    bail!("{dpkg:?} exited with {status}");
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                let mut kernel_images = Vec::new();
 | 
	
		
			
				|  |  | +                for entry in WalkDir::new(&archive_dir) {
 | 
	
		
			
				|  |  | +                    let entry = entry.with_context(|| {
 | 
	
		
			
				|  |  | +                        format!("failed to read entry in {}", archive_dir.display())
 | 
	
		
			
				|  |  | +                    })?;
 | 
	
		
			
				|  |  | +                    if !entry.file_type().is_file() {
 | 
	
		
			
				|  |  | +                        continue;
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    let path = entry.into_path();
 | 
	
		
			
				|  |  | +                    if let Some(file_name) = path.file_name() {
 | 
	
		
			
				|  |  | +                        match file_name.as_encoded_bytes() {
 | 
	
		
			
				|  |  | +                            // "vmlinuz-"
 | 
	
		
			
				|  |  | +                            [
 | 
	
		
			
				|  |  | +                                b'v',
 | 
	
		
			
				|  |  | +                                b'm',
 | 
	
		
			
				|  |  | +                                b'l',
 | 
	
		
			
				|  |  | +                                b'i',
 | 
	
		
			
				|  |  | +                                b'n',
 | 
	
		
			
				|  |  | +                                b'u',
 | 
	
		
			
				|  |  | +                                b'z',
 | 
	
		
			
				|  |  | +                                b'-',
 | 
	
		
			
				|  |  | +                                kernel_version @ ..,
 | 
	
		
			
				|  |  | +                            ] => {
 | 
	
		
			
				|  |  | +                                let kernel_version =
 | 
	
		
			
				|  |  | +                                    unsafe { OsStr::from_encoded_bytes_unchecked(kernel_version) }
 | 
	
		
			
				|  |  | +                                        .to_os_string();
 | 
	
		
			
				|  |  | +                                kernel_images.push((path, kernel_version))
 | 
	
		
			
				|  |  | +                            }
 | 
	
		
			
				|  |  | +                            _ => {}
 | 
	
		
			
				|  |  | +                        }
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                let (kernel_image, kernel_version) = match kernel_images.as_slice() {
 | 
	
		
			
				|  |  | +                    [kernel_image] => kernel_image,
 | 
	
		
			
				|  |  | +                    [] => bail!("no kernel images in {}", archive.display()),
 | 
	
		
			
				|  |  | +                    kernel_images => bail!(
 | 
	
		
			
				|  |  | +                        "multiple kernel images in {}: {:?}",
 | 
	
		
			
				|  |  | +                        archive.display(),
 | 
	
		
			
				|  |  | +                        kernel_images
 | 
	
		
			
				|  |  | +                    ),
 | 
	
		
			
				|  |  | +                };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                let mut modules_dirs = Vec::new();
 | 
	
		
			
				|  |  | +                for entry in WalkDir::new(&archive_dir) {
 | 
	
		
			
				|  |  | +                    let entry = entry.with_context(|| {
 | 
	
		
			
				|  |  | +                        format!("failed to read entry in {}", archive_dir.display())
 | 
	
		
			
				|  |  | +                    })?;
 | 
	
		
			
				|  |  | +                    if !entry.file_type().is_dir() {
 | 
	
		
			
				|  |  | +                        continue;
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    let path = entry.into_path();
 | 
	
		
			
				|  |  | +                    let mut components = path.components().rev();
 | 
	
		
			
				|  |  | +                    if components.next() != Some(path::Component::Normal(kernel_version)) {
 | 
	
		
			
				|  |  | +                        continue;
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    if components.next() != Some(path::Component::Normal(OsStr::new("modules"))) {
 | 
	
		
			
				|  |  | +                        continue;
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    modules_dirs.push(path);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                let modules_dir = match modules_dirs.as_slice() {
 | 
	
		
			
				|  |  | +                    [modules_dir] => modules_dir,
 | 
	
		
			
				|  |  | +                    [] => bail!("no modules directories in {}", archive.display()),
 | 
	
		
			
				|  |  | +                    modules_dirs => bail!(
 | 
	
		
			
				|  |  | +                        "multiple modules directories in {}: {:?}",
 | 
	
		
			
				|  |  | +                        archive.display(),
 | 
	
		
			
				|  |  | +                        modules_dirs
 | 
	
		
			
				|  |  | +                    ),
 | 
	
		
			
				|  |  | +                };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |                  // Guess the guest architecture.
 | 
	
		
			
				|  |  |                  let mut file = Command::new("file");
 | 
	
		
			
				|  |  |                  let output = file
 | 
	
		
			
				|  |  |                      .arg("--brief")
 | 
	
		
			
				|  |  | -                    .arg(&kernel_image)
 | 
	
		
			
				|  |  | +                    .arg(kernel_image)
 | 
	
		
			
				|  |  |                      .output()
 | 
	
		
			
				|  |  |                      .with_context(|| format!("failed to run {file:?}"))?;
 | 
	
		
			
				|  |  |                  let Output { status, .. } = &output;
 | 
	
	
		
			
				|  | @@ -441,7 +506,7 @@ pub(crate) fn run(opts: Options) -> Result<()> {
 | 
	
		
			
				|  |  |                      .arg("run")
 | 
	
		
			
				|  |  |                      .args(test_distro_args)
 | 
	
		
			
				|  |  |                      .args(["--bin", "depmod", "--", "-b"])
 | 
	
		
			
				|  |  | -                    .arg(&modules_dir)
 | 
	
		
			
				|  |  | +                    .arg(modules_dir)
 | 
	
		
			
				|  |  |                      .output()
 | 
	
		
			
				|  |  |                      .with_context(|| format!("failed to run {cargo:?}"))?;
 | 
	
		
			
				|  |  |                  let Output { status, .. } = &output;
 | 
	
	
		
			
				|  | @@ -452,12 +517,12 @@ pub(crate) fn run(opts: Options) -> Result<()> {
 | 
	
		
			
				|  |  |                  // Now our modules.alias file is built, we can recursively
 | 
	
		
			
				|  |  |                  // walk the modules directory and add all the files to the
 | 
	
		
			
				|  |  |                  // initramfs.
 | 
	
		
			
				|  |  | -                for entry in WalkDir::new(&modules_dir) {
 | 
	
		
			
				|  |  | +                for entry in WalkDir::new(modules_dir) {
 | 
	
		
			
				|  |  |                      let entry = entry.context("read_dir failed")?;
 | 
	
		
			
				|  |  |                      let path = entry.path();
 | 
	
		
			
				|  |  |                      let metadata = entry.metadata().context("metadata failed")?;
 | 
	
		
			
				|  |  |                      let out_path = Path::new("/lib/modules").join(
 | 
	
		
			
				|  |  | -                        path.strip_prefix(&modules_dir).with_context(|| {
 | 
	
		
			
				|  |  | +                        path.strip_prefix(modules_dir).with_context(|| {
 | 
	
		
			
				|  |  |                              format!(
 | 
	
		
			
				|  |  |                                  "strip prefix {} failed for {}",
 | 
	
		
			
				|  |  |                                  path.display(),
 | 
	
	
		
			
				|  | @@ -528,7 +593,7 @@ pub(crate) fn run(opts: Options) -> Result<()> {
 | 
	
		
			
				|  |  |                      .arg("-append")
 | 
	
		
			
				|  |  |                      .arg(kernel_args)
 | 
	
		
			
				|  |  |                      .arg("-kernel")
 | 
	
		
			
				|  |  | -                    .arg(&kernel_image)
 | 
	
		
			
				|  |  | +                    .arg(kernel_image)
 | 
	
		
			
				|  |  |                      .arg("-initrd")
 | 
	
		
			
				|  |  |                      .arg(&initrd_image);
 | 
	
		
			
				|  |  |                  let mut qemu_child = qemu
 |