Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Refactor init into test-distro #1160

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/scripts/find_kernels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env python3

import os
import glob
import sys
from typing import List

def find_kernels(directory: str) -> List[str]:
return glob.glob(f"{directory}/**/vmlinuz-*", recursive=True)

def find_modules_directory(directory: str, kernel: str) -> str:
matches = glob.glob(f"{directory}/**/modules/{kernel}", recursive=True)
if len(matches) == 0:
print(f"ERROR! No modules directory found for kernel {kernel}")
sys.exit(1)
return matches[0]

def main() -> None:
images = find_kernels('test/.tmp')
modules = []

for image in images:
image_name = os.path.basename(image).replace('vmlinuz-', '')
module_dir = find_modules_directory('test/.tmp', image_name)
modules.append(module_dir)

if len(images) != len(modules):
print(f"IMAGES={images}")
print(f"MODULES={modules}")
print("ERROR! len images != len modules")
sys.exit(1)

args = ' '.join(f"-i {image} -m {module}" for image, module in zip(images, modules))
print(args)

if __name__ == "__main__":
main()
40 changes: 34 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -60,8 +60,8 @@ jobs:
if: github.event_name != 'pull_request' && github.repository_owner == 'aya-rs'
with:
branch: create-pull-request/public-api
commit-message: 'public-api: regenerate'
title: 'public-api: regenerate'
commit-message: "public-api: regenerate"
title: "public-api: regenerate"
body: |
**Automated changes**
@@ -228,7 +228,7 @@ jobs:
run: |
set -euxo pipefail
sudo apt update
sudo apt -y install lynx qemu-system-{arm,x86}
sudo apt -y install lynx qemu-system-{arm,x86} musl-tools
echo /usr/lib/llvm-15/bin >> $GITHUB_PATH
- name: Install prerequisites
@@ -240,6 +240,10 @@ jobs:
# The tar shipped on macOS doesn't support --wildcards, so we need GNU tar.
#
# The clang shipped on macOS doesn't support BPF, so we need LLVM from brew.
#
# We need a musl C toolchain to compile our `test-distro` since some of
# our dependencies have build scripts that compile C code (i.e xz2).
# This is provided by `brew install filosottile/musl-cross/musl-cross`.
run: |
set -euxo pipefail
brew update
@@ -250,6 +254,8 @@ jobs:
echo $(brew --prefix curl)/bin >> $GITHUB_PATH
echo $(brew --prefix gnu-tar)/libexec/gnubin >> $GITHUB_PATH
echo $(brew --prefix llvm)/bin >> $GITHUB_PATH
brew install filosottile/musl-cross/musl-cross
ln -s "$(brew --prefix musl-cross)/bin/x86_64-linux-musl-gcc" /usr/local/bin/musl-gcc
- uses: dtolnay/rust-toolchain@nightly
with:
@@ -307,18 +313,40 @@ jobs:
- name: Extract debian kernels
run: |
set -euxo pipefail
# Remove old images and modules.
rm -rf test/.tmp/boot test/.tmp/lib
# The wildcard '**/boot/*' extracts kernel images and config.
# The wildcard '**/modules/*' extracts kernel modules.
# Modules are required since not all parts of the kernel we want to
# test are built-in.
find test/.tmp -name '*.deb' -print0 | xargs -t -0 -I {} \
sh -c "dpkg --fsys-tarfile {} | tar -C test/.tmp --wildcards --extract '*vmlinuz*' --file -"
sh -c "dpkg --fsys-tarfile {} | tar -C test/.tmp \
--wildcards --extract '**/boot/*' '**/modules/*' --file -"
- name: Run local integration tests
if: runner.os == 'Linux'
run: cargo xtask integration-test local

- name: Run virtualized integration tests
if: runner.os == 'Linux'
run: |
set -euxo pipefail
ARGS=$(./.github/scripts/find_kernels.py)
cargo xtask integration-test vm --cache-dir test/.tmp \
--github-api-token ${{ secrets.GITHUB_TOKEN }} \
${ARGS}
- name: Run virtualized integration tests
if: runner.os == 'macOS'
env:
# This sets the linker to the one installed by FiloSottile/musl-cross.
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: x86_64-linux-musl-gcc
run: |
set -euxo pipefail
find test/.tmp -name 'vmlinuz-*' -print0 | xargs -t -0 \
cargo xtask integration-test vm --cache-dir test/.tmp --github-api-token ${{ secrets.GITHUB_TOKEN }}
ARGS=$(./.github/scripts/find_kernels.py)
cargo xtask integration-test vm --cache-dir test/.tmp \
--github-api-token ${{ secrets.GITHUB_TOKEN }} \
${ARGS}
# Provides a single status check for the entire build workflow.
# This is used for merge automation, like Mergify, since GH actions
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ members = [
"aya-log-parser",
"aya-obj",
"aya-tool",
"init",
"test-distro",
"test/integration-common",
"test/integration-test",
"xtask",
@@ -33,7 +33,7 @@ default-members = [
"aya-log-parser",
"aya-obj",
"aya-tool",
"init",
"test-distro",
"test/integration-common",
# test/integration-test is omitted; including it in this list causes `cargo test` to run its
# tests, and that doesn't work unless they've been built with `cargo xtask`.
@@ -73,6 +73,7 @@ diff = { version = "0.1.13", default-features = false }
env_logger = { version = "0.11", default-features = false }
epoll = { version = "4.3.3", default-features = false }
futures = { version = "0.3.28", default-features = false }
glob = { version = "0.3.0", default-features = false }
hashbrown = { version = "0.15.0", default-features = false }
indoc = { version = "2.0", default-features = false }
libc = { version = "0.2.105", default-features = false }
@@ -99,8 +100,10 @@ test-log = { version = "0.2.13", default-features = false }
testing_logger = { version = "0.1.1", default-features = false }
thiserror = { version = "2.0.3", default-features = false }
tokio = { version = "1.24.0", default-features = false }
walkdir = { version = "2", default-features = false }
which = { version = "7.0.0", default-features = false }
xdpilone = { version = "1.0.5", default-features = false }
xz2 = { version = "0.1.7", default-features = false }

[profile.release.package.integration-ebpf]
debug = 2
13 changes: 0 additions & 13 deletions init/Cargo.toml

This file was deleted.

37 changes: 37 additions & 0 deletions test-distro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "test-distro"
version = "0.1.0"
publish = false
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
edition.workspace = true

[[bin]]
name = "init"
path = "src/init.rs"

[[bin]]
name = "modprobe"
path = "src/modprobe.rs"

[[bin]]
name = "depmod"
path = "src/depmod.rs"

[dependencies]
anyhow = { workspace = true, features = ["std"] }
object = { workspace = true, features = ["elf", "read_core", "std"] }
clap = { workspace = true, default-features = true, features = ["derive"] }
nix = { workspace = true, features = [
"user",
"fs",
"mount",
"reboot",
"kmod",
"feature",
] }
glob = { workspace = true }
xz2 = { workspace = true }
walkdir = { workspace = true }
122 changes: 122 additions & 0 deletions test-distro/src/depmod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//! depmod is used to build the modules.alias file to assist with loading
//! kernel modules.
//!
//! This implementation is incredibly naive and is only designed to work within
//! the constraints of the test environment. Not for production use.
use std::{
fs::File,
io::{BufWriter, Read, Write as _},
path::PathBuf,
};

use anyhow::Context as _;
use clap::Parser;
use object::{Object, ObjectSection, ObjectSymbol};
use test_distro::resolve_modules_dir;
use walkdir::WalkDir;
use xz2::read::XzDecoder;

#[derive(Parser)]
struct Args {
#[clap(long, short)]
base_dir: Option<PathBuf>,
}

fn main() -> anyhow::Result<()> {
let Args { base_dir } = Parser::parse();

let modules_dir = if let Some(base_dir) = base_dir {
base_dir
} else {
resolve_modules_dir().context("Failed to resolve modules dir")?
};

let modules_alias = modules_dir.join("modules.alias");
let f = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&modules_alias)
.with_context(|| format!("failed to open: {}", modules_alias.display()))?;
let mut output = BufWriter::new(&f);
for entry in WalkDir::new(modules_dir) {
let entry = entry.context("failed to read entry in walkdir")?;
if entry.file_type().is_file() {
let path = entry.path();
if let Some(extension) = path.extension() {
if extension != "ko" && extension != "xz" {
continue;
}
let module_name = path
.file_stem()
.ok_or(anyhow::anyhow!("failed to get file stem"))?
.to_os_string()
.into_string()
.map_err(|_| anyhow::anyhow!("failed to convert to string"))?
.replace(".ko", "");
let mut f = File::open(path)
.with_context(|| format!("failed to open: {}", path.display()))?;
let stat = f
.metadata()
.with_context(|| format!("Failed to get metadata for {}", path.display()))?;
if extension == "xz" {
let mut decoder = XzDecoder::new(f);
let mut decompressed = Vec::with_capacity(stat.len() as usize * 2);
decoder.read_to_end(&mut decompressed)?;
read_aliases_from_module(&decompressed, &module_name, &mut output)
} else {
let mut buf = Vec::with_capacity(stat.len() as usize);
f.read_to_end(&mut buf)
.with_context(|| format!("Failed to read: {}", path.display()))?;
read_aliases_from_module(&buf, &module_name, &mut output)
}
.with_context(|| {
format!("Failed to read aliases from module {}", path.display())
})?;
}
}
}

Ok(())
}

fn read_aliases_from_module(
contents: &[u8],
module_name: &str,
output: &mut BufWriter<&File>,
) -> Result<(), anyhow::Error> {
let obj = object::read::File::parse(contents).context("not an object file")?;

let (section_idx, data) = obj
.sections()
.filter_map(|s| {
if let Ok(name) = s.name() {
if name == ".modinfo" {
if let Ok(data) = s.data() {
return Some((s.index(), data));
}
}
}
None
})
.next()
.context("no .modinfo section")?;

obj.symbols()
.try_for_each(|s| -> Result<(), anyhow::Error> {
let name = s.name().context("failed to get symbol name")?;
if name.contains("alias") && s.section_index() == Some(section_idx) {
let start = s.address() as usize;
let end = start + s.size() as usize;
let sym_data = &data[start..end];
let cstr = std::ffi::CStr::from_bytes_with_nul(sym_data)
.context("failed to convert to cstr")?;
let sym_str = cstr.to_str().context("failed to convert to str")?;
let alias = sym_str.replace("alias=", "");
writeln!(output, "alias {} {}", alias, module_name).expect("write");
}
Ok(())
})?;
Ok(())
}
8 changes: 8 additions & 0 deletions init/src/main.rs → test-distro/src/init.rs
Original file line number Diff line number Diff line change
@@ -57,6 +57,14 @@ fn run() -> anyhow::Result<()> {
data: None,
target_mode: Some(RXRXRX),
},
Mount {
source: "dev",
target: "/dev",
fstype: "devtmpfs",
flags: nix::mount::MsFlags::empty(),
data: None,
target_mode: None,
},
Mount {
source: "sysfs",
target: "/sys",
30 changes: 30 additions & 0 deletions test-distro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use std::path::PathBuf;

use anyhow::Context as _;
use nix::sys::utsname::uname;

/// Kernel modules are in `/lib/modules`.
/// They may be in the root of this directory,
/// or in subdirectory named after the kernel release.
pub fn resolve_modules_dir() -> anyhow::Result<PathBuf> {
let modules_dir = PathBuf::from("/lib/modules");
let stat = modules_dir
.metadata()
.with_context(|| format!("{} doesn't exist", modules_dir.display()))?;
if stat.is_dir() {
return Ok(modules_dir);
}

let utsname = uname().context("failed to get kernel release")?;
let release = utsname.release();
let modules_dir = modules_dir.join(release);
let stat = modules_dir
.metadata()
.with_context(|| format!("{} doesn't exist", modules_dir.display()))?;
anyhow::ensure!(
stat.is_dir(),
"{} is not a directory",
modules_dir.display()
);
Ok(modules_dir)
}
147 changes: 147 additions & 0 deletions test-distro/src/modprobe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//! modprobe is used to load kernel modules into the kernel.
//!
//! This implementation is incredibly naive and is only designed to work within
//! the constraints of the test environment. Not for production use.
use std::{
ffi::CString,
fs::File,
io::{BufRead as _, Read as _},
path::Path,
};

use anyhow::{anyhow, bail, Context as _};
use clap::Parser;
use glob::glob;
use nix::kmod::init_module;
use test_distro::resolve_modules_dir;

macro_rules! output {
($quiet:expr, $($arg:tt)*) => {
if !$quiet {
println!($($arg)*);
}
};
}

#[derive(Parser)]
struct Args {
/// Suppress all output and don't return an error code.
#[clap(short, long, default_value = "false")]
quiet: bool,

/// The name of the module to load.
/// This can be either an alias like `net-sched-sch-ingress` or a module
/// name like `sch_ingress`.
name: String,
}

fn main() {
let Args { quiet, name } = Parser::parse();
if let Err(e) = try_main(quiet, name) {
if !quiet {
eprintln!("{}", e);
std::process::exit(1);
} else {
std::process::exit(0);
}
}
}

fn try_main(quiet: bool, name: String) -> anyhow::Result<()> {
let modules_dir = resolve_modules_dir()?;

output!(quiet, "resolving alias for module: {}", name);
let module = resolve_alias(quiet, &modules_dir, &name)?;

let pattern = format!(
"{}/kernel/**/{}.ko*",
modules_dir
.to_str()
.ok_or(anyhow!("Failed to convert modules_dir to string"))?,
module
);
let module_path = glob(&pattern)
.with_context(|| format!("Failed to glob: {}", pattern))?
.filter_map(Result::ok)
.next()
.ok_or(anyhow!("Module not found: {}", module))?;

output!(quiet, "Loading module: {}", module_path.display());
let mut f = File::open(&module_path)
.with_context(|| format!("Failed to open module: {}", module_path.display()))?;

let stat = f.metadata().with_context(|| {
format!(
"Failed to get metadata for module: {}",
module_path.display()
)
})?;

let extension = module_path.as_path().extension().ok_or(anyhow!(
"Module has no extension: {}",
module_path.display()
))?;

let contents = if extension == "xz" {
output!(quiet, "Decompressing module");
let mut decompressed = Vec::with_capacity(stat.len() as usize * 2);
xz2::read::XzDecoder::new(f).read_to_end(&mut decompressed)?;
decompressed
} else {
let mut contents: Vec<u8> = Vec::with_capacity(stat.len() as usize);
f.read_to_end(&mut contents)?;
contents
};

if contents[0..4] != [0x7f, 0x45, 0x4c, 0x46] {
bail!("Module is not an valid ELF file");
}

let res = init_module(&contents, &CString::new("").unwrap());
if let Err(e) = res {
if e == nix::errno::Errno::EEXIST {
bail!("Module already loaded");
}
bail!("Failed to load module: {}", e);
}
output!(quiet, "Module loaded successfully");
Ok(())
}

fn resolve_alias(quiet: bool, module_dir: &Path, name: &str) -> anyhow::Result<String> {
let modules_alias = module_dir.join("modules.alias");
output!(
quiet,
"opening modules.alias file: {}",
modules_alias.display()
);
let alias_file = File::open(&modules_alias).with_context(|| {
format!(
"Failed to open modules.alias file: {}",
modules_alias.display()
)
})?;
let alias_file = std::io::BufReader::new(alias_file);

for line in alias_file.lines() {
let line = line?;
if line.starts_with("alias ") {
let mut parts = line.split_whitespace();
let prefix = parts.next(); // skip "alias "
if prefix != Some("alias") {
bail!("Alias line incorrect prefix: {}", line);
}
let alias = parts
.next()
.with_context(|| format!("Alias line missing alias: {}", line))?;
let module = parts
.next()
.with_context(|| format!("Alias line missing module: {}", line))?;
if alias == name {
return Ok(module.to_string());
}
}
}
bail!("Alias not found: {}", name)
}
1 change: 0 additions & 1 deletion test/integration-test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -32,7 +32,6 @@ pub const TEST: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/test")
pub const TWO_PROGS: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/two_progs"));
pub const XDP_SEC: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/xdp_sec"));
pub const UPROBE_COOKIE: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/uprobe_cookie"));

#[cfg(test)]
mod tests;
#[cfg(test)]
14 changes: 13 additions & 1 deletion test/integration-test/src/tests/smoke.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
use aya::{
programs::{Extension, TracePoint, Xdp, XdpFlags},
programs::{tc, Extension, TracePoint, Xdp, XdpFlags},
util::KernelVersion,
Ebpf, EbpfLoader,
};
use test_log::test;

use crate::utils::NetNsGuard;

#[test]
fn modprobe() {
// This very simple looking test is actually quite complex.
// The call to tc::qdisc_add_clsact() causes the linux kernel to call into
// `__request_module()`, which via the usermodehelper calls out into the
// `/sbin/modprobe` to load the required kernel module.
// In order for this test to pass, all of that machinery must work
// correctly within the test environment.
let _netns = NetNsGuard::new();
tc::qdisc_add_clsact("lo").unwrap();
}

#[test]
fn xdp() {
let kernel_version = KernelVersion::current().unwrap();
1 change: 1 addition & 0 deletions xtask/Cargo.toml
Original file line number Diff line number Diff line change
@@ -27,3 +27,4 @@ syn = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["rt"] }
which = { workspace = true }
walkdir = { workspace = true }
186 changes: 138 additions & 48 deletions xtask/src/run.rs
Original file line number Diff line number Diff line change
@@ -3,16 +3,18 @@ use std::{
fmt::Write as _,
fs::{copy, create_dir_all, OpenOptions},
io::{BufRead as _, BufReader, Write as _},
path::PathBuf,
os::unix::prelude::OsStrExt as _,
path::{Path, PathBuf},
process::{Child, ChildStdin, Command, Output, Stdio},
sync::{Arc, Mutex},
thread,
};

use anyhow::{anyhow, bail, Context as _, Result};
use anyhow::{anyhow, bail, Context, Result};
use base64::engine::Engine as _;
use cargo_metadata::{Artifact, CompilerMessage, Message, Target};
use clap::Parser;
use walkdir::WalkDir;
use xtask::{Errors, AYA_BUILD_INTEGRATION_BPF};

#[derive(Parser)]
@@ -40,15 +42,27 @@ enum Environment {
/// 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/
/// --recursive http://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>,
/// | 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(short, long, required = true)]
image: Vec<PathBuf>,

/// The location of the kernel modules for the selected kernel image(s)
///
/// These are found either in `lib/modules/` or `usr/lib/modules/` at
/// the location where you extracted the kernel image.
#[clap(short, long, required = true)]
modules: Vec<PathBuf>,
},
}

@@ -69,8 +83,7 @@ where
let mut cmd = Command::new("cargo");
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]);
cmd.args(["--target", target]);
}
f(&mut cmd);

@@ -181,7 +194,8 @@ pub fn run(opts: Options) -> Result<()> {
Environment::VM {
cache_dir,
github_api_token,
kernel_image,
image,
modules,
} => {
// The user has asked us to run the tests on a VM. This is involved; strap in.
//
@@ -200,6 +214,13 @@ pub fn run(opts: Options) -> Result<()> {
// 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.

anyhow::ensure!(
image.len() == modules.len(),
"{} images, {} modules provided. arguments should match",
image.len(),
modules.len(),
);
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
@@ -260,12 +281,12 @@ pub fn run(opts: Options) -> Result<()> {
}

let mut errors = Vec::new();
for kernel_image in kernel_image {
for (kernel_image, modules_dir) in image.iter().zip(modules.iter()) {
// Guess the guest architecture.
let mut cmd = Command::new("file");
let output = cmd
.arg("--brief")
.arg(&kernel_image)
.arg(kernel_image)
.output()
.with_context(|| format!("failed to run {cmd:?}"))?;
let Output { status, .. } = &output;
@@ -298,21 +319,12 @@ pub fn run(opts: Options) -> Result<()> {

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"])
let test_distro: Vec<(String, PathBuf)> = build(Some(&target), |cmd| {
cmd.args(["--package", "test-distro", "--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:?}"),
};
.context("building test-distro package failed")?
.into_iter()
.collect();

let binaries = binaries(Some(&target))?;

@@ -337,22 +349,106 @@ pub fn run(opts: Options) -> Result<()> {
let Child { stdin, .. } = &mut gen_init_cpio_child;
let mut stdin = stdin.take().unwrap();

use std::os::unix::ffi::OsStrExt as _;
macro_rules! writeln_stdin {
($($arg:tt)*) => {
writeln!(stdin, $($arg)*).expect("write")
}
}

macro_rules! write_stdin {
($out_path:expr) => {
for bytes in [
"dir ".as_bytes(),
$out_path.as_bytes(),
" ".as_bytes(),
"755 0 0\n".as_bytes(),
] {
stdin.write_all(bytes).expect("write");
}
};
($out_path:expr, $in_path:expr, $mode:expr) => {
for bytes in [
"file ".as_bytes(),
$out_path.as_bytes(),
" ".as_bytes(),
$in_path.as_bytes(),
" ".as_bytes(),
$mode.as_bytes(),
"\n".as_bytes(),
] {
stdin.write_all(bytes).expect("write");
}
};
}

// 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");
// file /init path-to-init 755 0 0
// dir /bin 755 0 0
// file /bin/foo path-to-foo 755 0 0
// file /bin/bar path-to-bar 755 0 0
writeln_stdin!("dir /bin 755 0 0");
writeln_stdin!("dir /sbin 755 0 0");
writeln_stdin!("dir /lib 755 0 0");
writeln_stdin!("dir /lib/modules 755 0 0");

test_distro.iter().for_each(|(name, path)| {
if name == "init" {
write_stdin!("/init", path.as_os_str(), "755 0 0");
} else {
let out_path = Path::new("/sbin").join(name);
write_stdin!(out_path.as_os_str(), path.as_os_str(), "755 0 0");
}
});

// At this point we need to make a slight detour!
// Preparing the `modules.alias` file inside the VM as part of
// `/init` is slow. It's faster to prepare it here.
Command::new("cargo")
.args([
"run",
"--package",
"test-distro",
"--bin",
"depmod",
"--release",
"--",
"-b",
])
.arg(modules_dir)
.status()
.context("failed to run depmod")?;

// 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) {
let entry = entry.context("read_dir failed")?;
let path = entry.path();
let metadata = entry.metadata().context("metadata failed")?;
if metadata.file_type().is_dir() {
let out_path = Path::new("/lib/modules").join(
path.strip_prefix(modules_dir).with_context(|| {
format!(
"strip prefix {} failed for {}",
path.display(),
modules_dir.display()
)
})?,
);
write_stdin!(out_path.as_os_str());
} else if metadata.file_type().is_file() {
let out_path = Path::new("/lib/modules").join(
path.strip_prefix(modules_dir).with_context(|| {
format!(
"strip prefix {} failed for {}",
path.display(),
modules_dir.display()
)
})?,
);
write_stdin!(out_path.as_os_str(), path.as_os_str(), "644 0 0");
}
}

for (profile, binaries) in binaries {
@@ -362,17 +458,11 @@ pub fn run(opts: Options) -> Result<()> {
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");
}
let out_path = Path::new("/bin").join(&name);
write_stdin!(out_path.as_os_str(), path.as_os_str(), "755 0 0");
}
}

// Must explicitly close to signal EOF.
drop(stdin);

@@ -417,7 +507,7 @@ pub 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