# 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) -> color_eyre::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) -> color_eyre::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`.