vmrunner 0.0.4

micro-vm runner for testcases that require root or invasive IO
# vmrunner

VM-backed Rust tests with libkrun.

## Requirements

Generic:

* `libkrun` and `libkrunfw`
* Rust 1.85 or newer
* a Linux root qcow2 image or HTTP(S) image URL
* a full Rust guest target, for example `aarch64-unknown-linux-gnu`
* `curl` when `root_qcow2` uses an HTTP(S) URL

Linux:

* `/dev/kvm` access
* `unshare(1)` in `PATH` with `--user --map-root-user --net --fork`
* user and network namespaces enabled
* `/dev/net/tun` access for `TunTap`

macOS:

* macOS 14 or newer on Apple Silicon
* Apple Hypervisor.framework
* `gvproxy` for host or internet uplinks
* Zig when `CC_LINUX` is unset

## Setup

macOS:

```sh
brew tap libkrun/krun
brew trust libkrun/krun
brew install libkrun/krun/libkrun libkrun/krun/gvproxy zig rustup
rustup toolchain install stable
```

Linux: install `libkrun`, `libkrunfw`, `rustup`, `util-linux`, `curl`, and optional `zig` with your distro package manager.

Guest target examples:

* Apple Silicon or Linux `aarch64`: `aarch64-unknown-linux-gnu`
* Linux `x86_64`: `x86_64-unknown-linux-gnu`

FreeBSD guests are temporarily disabled. Use Linux guest targets for now.

`vmrunner` rejects guest targets whose Rust target arch does not match the host arch. If `guest_target` is omitted, the macro uses `<host-arch>-unknown-linux-musl`.

```sh
rustup target add aarch64-unknown-linux-gnu
export KRUN_INIT_GUEST_TARGET=aarch64-unknown-linux-gnu
export CC_LINUX="zig cc -target aarch64-unknown-linux-gnu"
```

Host target map:

```rust,no_run
#[vmrunner::test(
    root_qcow2 = "https://cloud-images.ubuntu.com/daily/server/releases/26.04/release-20260720/ubuntu-26.04-server-cloudimg-arm64.img",
    guest_target = [
        "aarch64-apple-darwin" | "aarch64-unknown-linux-gnu" => "aarch64-unknown-linux-musl",
        "x86_64-unknown-linux-gnu" => "x86_64-unknown-linux-musl",
    ],
)]
fn mapped(setup: vmrunner::TestSetup) -> anyhow::Result<()> {
    Ok(())
}
```

No map match prints a warning and picks a target with the same arch when possible. Otherwise it falls back to `<host-arch>-unknown-linux-musl`.

## Ubuntu cloud image example

URL roots are downloaded once to `target/vmrunner-qcow2-imag/<file>`.

```sh
rustup target add aarch64-unknown-linux-gnu
export KRUN_INIT_GUEST_TARGET=aarch64-unknown-linux-gnu
export CC_LINUX="zig cc -target aarch64-unknown-linux-gnu"
cargo run -p vmrunner --bin build-krun-init-blob -- \
  --target aarch64-unknown-linux-gnu \
  --root-qcow2 https://cloud-images.ubuntu.com/daily/server/releases/26.04/release-20260720/ubuntu-26.04-server-cloudimg-arm64.img
```

For x86_64 hosts use:

* `https://cloud-images.ubuntu.com/daily/server/releases/26.04/release-20260720/ubuntu-26.04-server-cloudimg-amd64.img`
* `guest_target = "x86_64-unknown-linux-gnu"`

Image metadata and cache helpers are exported by `vmrunner`:

* `EXTERNAL_QCOW_FILESYSTEMS`: structured entries with `name`, `arch`, `url`, and `digest`
* `QCOW2_IMAGE_CACHE_DIR`, `qcow2_image_cache_path_with_sha256`, `ensure_qcow2_image_cached_with_sha256`

Known URLs are cached under `target/vmrunner-qcow2-imag/sha256/<digest>/`. FreeBSD `.qcow2.xz` metadata remains exported for cache helpers, but FreeBSD guests are disabled for now.

Image smoke tests use a `libtest-mimic` custom harness and run through Cargo:

```sh
cargo test --test distro_vms -- --nocapture
```

Ubuntu and Fedora pass on the current x86_64 host. The FreeBSD trial is registered but ignored while FreeBSD guests are disabled.

Ubuntu cloud images normally put `/` on the first partition, so use `/dev/vda1`.

```rust,no_run
#[vmrunner::test(
    root_qcow2 = "https://cloud-images.ubuntu.com/daily/server/releases/26.04/release-20260720/ubuntu-26.04-server-cloudimg-arm64.img",
    guest_target = "aarch64-unknown-linux-gnu"
)]
async fn ubuntu_true(setup: vmrunner::TestSetup) -> anyhow::Result<()> {
    let node = vmrunner::Node::new(&[], "/usr/bin/true");
    let running = vmrunner::TestCase::new(&[&node])
        .root_qcow2(setup.root_qcow2_path())
        .guest_init(setup.guest_init_path())
        .root_device(vmrunner::RootDevice::virtio_first_partition())
        .launch()
        .await?;
    assert_eq!(running.wait().await?, vec![0]);
    Ok(())
}
```

## How it works

* `#[vmrunner::test]` requires `root_qcow2`; `guest_target` can be omitted, fixed, or host-mapped.
* HTTP(S) `root_qcow2` values are cached under `target/vmrunner-qcow2-imag`.
* Missing `init.krun` sidecars are built before Linux namespace setup.
* Linux tests always re-exec through `unshare` before VM launch.
* `TestCase` creates a writable qcow2 overlay per VM node with `imago`.
* libkrun starts `init.krun`, mounts the qcow2 root disk, then direct-execs the requested guest command.
* `krun-init-blob` stays out of the normal dependency graph. Bump it in `krun-init-build-package/Cargo.toml`.

GPU support is opt-in with `--features gpu`.