cargo-image-runner
A generic, highly customizable embedded/kernel development runner for Rust. Build and run bootable images with support for multiple bootloaders, image formats, and boot types.
Features
- Multiple Bootloaders: Limine, GRUB, or direct boot (no bootloader)
- Multiple Image Formats: Directory (for QEMU), ISO, FAT
- Multiple Boot Types: BIOS, UEFI, or hybrid
- I/O Capture & Streaming: Capture serial output, react to patterns, send input programmatically
- Trait-Based Architecture: Easy to extend with custom bootloaders, image builders, and runners
- Builder Pattern API: Ergonomic, fluent API for programmatic use
- Template Variables: Powerful variable substitution in config files
- Test Integration: Automatic test detection and execution
- Environment Variable Overrides: Runtime configuration without editing files
- Profile System: Named configuration presets for different workflows
- CLI Arg Passthrough: Pass extra QEMU arguments via
--
Quick Start
Installation
Add to your Cargo.toml:
[]
= "0.5"
Or install as a binary:
Basic UEFI Direct Boot
The simplest setup - boots your UEFI executable directly without a bootloader:
Cargo.toml:
[]
= "uefi"
[]
= "none"
[]
= "directory"
# Configure as cargo runner
[]
= "cargo-image-runner"
Then just run:
With Limine Bootloader
For a full bootloader experience with both BIOS and UEFI support:
Cargo.toml:
[]
= "hybrid" # Supports both BIOS and UEFI
[]
= "limine"
= "limine.conf"
[]
= "v8.4.0-binary" # Use a specific Limine version
[]
= "directory"
[]
= "5"
= "quiet"
limine.conf:
timeout: {{TIMEOUT}}
/My Kernel
protocol: limine
kernel_path: boot():/boot/{{EXECUTABLE_NAME}}
cmdline: {{KERNEL_CMDLINE}}
The runner will automatically:
- Fetch Limine binaries from GitHub (cached)
- Process template variables in limine.conf
- Copy your kernel and bootloader files
- Run in QEMU with UEFI firmware
Configuration Reference
All configuration lives under [package.metadata.image-runner] in your Cargo.toml. Workspace-level defaults can be set under [workspace.metadata.image-runner].
Boot Configuration
[]
= "uefi" # Options: "bios", "uefi", "hybrid"
| Value | Description |
|---|---|
bios |
BIOS boot only |
uefi |
UEFI boot only (default) |
hybrid |
Both BIOS and UEFI support |
Bootloader Configuration
No Bootloader (Direct Boot)
[]
= "none"
Limine Bootloader
[]
= "limine"
= "limine.conf" # Path to your limine.conf (relative to workspace root)
= [] # Additional files to copy into the image
[]
= "v8.x-binary" # Git tag from limine repo (default: "v8.x-binary")
Available Limine versions: Check Limine releases for tags like v8.4.0-binary, v8.3.0-binary, etc. Always use the -binary suffix.
GRUB Bootloader
[]
= "grub"
[]
= [] # GRUB modules to include
Image Configuration
[]
= "directory" # Options: "directory", "iso", "fat"
= "custom-name.iso" # Optional: custom output path
= "MYKERNEL" # Optional: volume label (default: "BOOT")
| Format | Description | Requires Feature |
|---|---|---|
directory |
Directory structure (works with QEMU fat:rw:) |
(always available) |
iso |
ISO 9660 image | iso |
fat |
FAT filesystem image | fat |
Runner Configuration
[]
= "qemu" # Currently the only runner
[]
= "qemu-system-x86_64" # QEMU binary to use
= "q35" # Machine type
= 1024 # RAM in MB
= 1 # Number of CPU cores
= true # Enable KVM acceleration (Linux only)
= [] # Additional QEMU arguments (always applied)
Serial Configuration
[]
= "mon:stdio" # Serial mode (see table below)
= false # Separate QEMU monitor from serial port
| Mode | QEMU Flag | Description |
|---|---|---|
mon:stdio |
-serial mon:stdio |
Serial + monitor multiplexed on stdio (default) |
stdio |
-serial stdio |
Serial only on stdio |
none |
-serial none |
No serial output |
When using an I/O handler via the programmatic API, serial is automatically set to stdio with the monitor disabled, regardless of this setting.
Test Configuration
[]
= 33 # Exit code that indicates test success
= [ # Additional args for test runs only
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x4"
]
= 60 # Test timeout in seconds
Test binaries are automatically detected by examining the executable name for Cargo's hash suffix pattern (e.g., my-test-a1b2c3d4e5f6a7b8).
Run Configuration
[]
= [ # Additional args for normal (non-test) runs only
"-no-reboot",
"-serial", "stdio"
]
= false # Use GUI display
Verbose Output
= true # Enable verbose output (show build progress messages)
Template Variables
Define custom variables for use in bootloader config files:
[]
= "5"
= "quiet loglevel=3"
= "value"
Built-in variables (always available, cannot be overridden by config):
| Variable | Description |
|---|---|
{{EXECUTABLE}} |
Full path to the executable |
{{EXECUTABLE_NAME}} |
Executable filename only |
{{WORKSPACE_ROOT}} |
Project workspace root |
{{OUTPUT_DIR}} |
Output directory path |
{{IS_TEST}} |
1 if running tests, 0 otherwise |
Syntax: Use {{VAR}} or $VAR in your config files.
Variable layering (later overrides earlier):
- Config variables (
[variables]) - Environment variables (
CARGO_IMAGE_RUNNER_VAR_*) - Built-in variables (always win)
Environment Variable Overrides
Override any configuration at runtime without editing files. Useful for debugging, CI/CD, and quick experiments.
Key Config Field Overrides
| Environment Variable | Overrides | Example |
|---|---|---|
CARGO_IMAGE_RUNNER_QEMU_BINARY |
QEMU binary path | qemu-system-aarch64 |
CARGO_IMAGE_RUNNER_QEMU_MEMORY |
Memory in MB | 4096 |
CARGO_IMAGE_RUNNER_QEMU_CORES |
CPU cores | 4 |
CARGO_IMAGE_RUNNER_QEMU_MACHINE |
Machine type | virt |
CARGO_IMAGE_RUNNER_BOOT_TYPE |
Boot type | bios, uefi, hybrid |
CARGO_IMAGE_RUNNER_VERBOSE |
Verbose output | 1, true, yes |
CARGO_IMAGE_RUNNER_KVM |
KVM acceleration | 1/true/yes or 0/false/no |
CARGO_IMAGE_RUNNER_SERIAL_MODE |
Serial mode | mon:stdio, stdio, none |
Invalid values are silently ignored (the config file value is kept).
Extra QEMU Arguments
# Whitespace-separated, appended to the QEMU command line
CARGO_IMAGE_RUNNER_QEMU_ARGS="-s -S -device virtio-net"
Template Variables from Environment
# Set/override template variables: CARGO_IMAGE_RUNNER_VAR_<NAME>=<value>
CARGO_IMAGE_RUNNER_VAR_TIMEOUT=10 CARGO_IMAGE_RUNNER_VAR_DEBUG=1
The VAR_ prefix is stripped, so CARGO_IMAGE_RUNNER_VAR_TIMEOUT=10 sets the template variable TIMEOUT to 10.
Profile System
Profiles let you define named configuration presets and switch between them with an environment variable.
Defining Profiles
Add profiles under [package.metadata.image-runner.profiles.<name>]:
[]
= "uefi"
[]
= 1024
# Debug profile: more memory, GDB server, verbose
[]
= true
[]
= 4096
= ["-s", "-S"]
[]
= "1"
# CI profile: no KVM, no GUI
[]
[]
= false
[]
= "1"
Activating a Profile
CARGO_IMAGE_RUNNER_PROFILE=debug
Profile values are deep-merged into the base config:
- Object fields merge recursively (only specified keys are overridden)
- Scalars and arrays are replaced entirely
- Unspecified fields keep their base values
If the profile name doesn't exist, an error is returned listing available profiles.
Profile Sources
Profiles can be defined at both workspace and package level. Package-level profiles override workspace-level profiles with the same name.
Configuration Layering
All configuration follows a strict priority order (later overrides earlier):
Config Values
| Priority | Source |
|---|---|
| 1 (lowest) | Built-in defaults |
| 2 | Workspace metadata ([workspace.metadata.image-runner]) |
| 3 | Package metadata ([package.metadata.image-runner]) |
| 4 | Standalone TOML file (if provided via API) |
| 5 | Profile overlay (CARGO_IMAGE_RUNNER_PROFILE) |
| 6 (highest) | Individual env var overrides (CARGO_IMAGE_RUNNER_*) |
QEMU Extra Args (appended in order)
| Priority | Source |
|---|---|
| 1 (first) | extra_args from [runner.qemu] config |
| 2 | extra-args from [test] or [run] (based on mode) |
| 3 | CARGO_IMAGE_RUNNER_QEMU_ARGS env var |
| 4 (last) | CLI -- args passthrough |
All sources are appended (not replaced), so args from all layers are present.
Template Variables
| Priority | Source |
|---|---|
| 1 (lowest) | Config variables ([variables]) |
| 2 | Environment variables (CARGO_IMAGE_RUNNER_VAR_*) |
| 3 (highest) | Built-in variables (EXECUTABLE, WORKSPACE_ROOT, etc.) |
CLI Usage
# Run an executable
# Run with explicit subcommand
# Pass extra QEMU arguments via --
# Build image without running
# Check configuration (shows active profile, env overrides, QEMU settings)
# Clean build artifacts
# Show version
As a Cargo Runner
Configure in .cargo/config.toml:
[]
= "cargo-image-runner"
Then cargo run and cargo test work directly.
Programmatic API
Use cargo-image-runner as a library:
use builder;
Builder Methods
| Method | Description |
|---|---|
.from_cargo_metadata()? |
Load config from Cargo.toml (includes profiles + env overrides) |
.from_config_file(path)? |
Load from a standalone TOML file |
.with_config(config) |
Set config directly |
.executable(path) |
Set the kernel/executable path |
.workspace_root(path) |
Set workspace root |
.extra_args(vec) |
Set CLI passthrough QEMU args |
.no_bootloader() |
Use no bootloader |
.limine() |
Use Limine bootloader |
.grub() |
Use GRUB bootloader |
.directory_output() |
Output as directory |
.iso_image() |
Output as ISO |
.fat_image() |
Output as FAT image |
.qemu() |
Use QEMU runner |
.io_handler(handler) |
Set an I/O handler for serial capture/streaming |
.build()? |
Build ImageRunner (does not execute) |
.run()? |
Build and immediately execute |
.run_with_result()? |
Build, execute, and return the full RunResult |
Custom Bootloader
Implement the Bootloader trait to add custom bootloader support:
use ;
use BootType;
use ;
;
I/O Capture & Streaming
The IoHandler trait enables programmatic interaction with QEMU's serial port. Built-in handlers cover common use cases, and you can implement custom handlers for advanced scenarios.
Capture Output for Assertions
use ;
React to Output Patterns
use ;
use PatternResponder;
Custom I/O Handler
use ;
Built-in Handlers
| Handler | Description |
|---|---|
CaptureHandler |
Accumulates all serial + stderr output, available via finish() |
TeeHandler |
Captures AND echoes to the real terminal |
PatternResponder |
Matches string patterns in serial output and sends responses |
Examples
Located under examples/, each demonstrating a different configuration combination:
| Example | Boot | Bootloader | Image | Notes |
|---|---|---|---|---|
uefi-simple |
UEFI | None | Directory | Simplest possible setup |
limine-directory |
Hybrid | Limine | Directory | Fast iteration with Limine |
limine-iso |
Hybrid | Limine | ISO | Bootable ISO image |
uefi-fat |
UEFI | None | FAT | Real FAT filesystem image |
limine-fat |
UEFI | Limine | FAT | Limine with FAT image |
bios-limine-iso |
BIOS | Limine | ISO | Legacy BIOS boot |
profiles |
UEFI | None | Directory | Profile system and env var overrides |
extra-files |
Hybrid | Limine | Directory | Extra files and custom template variables |
Feature Flags
Minimize dependencies by selecting only the features you need:
[]
= { = "0.3", = false, = ["uefi", "limine", "qemu"] }
| Feature | Description | Dependencies |
|---|---|---|
uefi |
UEFI boot support | ovmf-prebuilt |
bios |
BIOS boot support | - |
limine |
Limine bootloader | git2 |
grub |
GRUB bootloader | - |
iso |
ISO image format | hadris-iso |
fat |
FAT filesystem image | fatfs, fscommon |
qemu |
QEMU runner | - |
progress |
Progress reporting | indicatif |
Default features: uefi, bios, limine, iso, qemu
Architecture
cargo-image-runner uses a trait-based pipeline architecture:
Bootloader -> ImageBuilder -> Runner
Core Traits
Bootloader - Prepares bootloader files and processes configuration:
ImageBuilder - Builds bootable images in various formats:
Runner - Executes images:
Module Map
| Module | Role |
|---|---|
core/ |
Context, ImageRunnerBuilder, ImageRunner, Error/Result |
config/ |
Config struct, ConfigLoader, env (environment variable processing) |
bootloader/ |
Bootloader trait + impls: limine, grub, none; fetcher for downloads |
image/ |
ImageBuilder trait + impls: directory, iso, fat; template processor |
runner/ |
Runner trait + impl: qemu; io module for I/O capture/streaming |
firmware/ |
UEFI firmware (ovmf) |
util/ |
Filesystem helpers (fs), hashing (hash) |
Build Artifacts
All build artifacts go to target/image-runner/:
cache/- Downloaded files (Limine binaries, OVMF firmware)output/- Built images
Troubleshooting
"limine-bios.sys not found"
Make sure you're using a binary release version like v8.4.0-binary, not a source version like v8.4.0.
"revspec not found" error
The Limine version must be a valid git tag. Check available versions at https://github.com/limine-bootloader/limine/releases
QEMU not found
Install QEMU for your platform:
- macOS:
brew install qemu - Linux:
sudo apt install qemu-system-x86orsudo dnf install qemu-system-x86 - Windows: Download from https://www.qemu.org/download/
Missing OVMF firmware
The runner automatically downloads OVMF firmware for UEFI boot. If you see firmware errors, ensure you have internet connectivity and the uefi feature is enabled.
Profile not found
If you get profile 'xyz' not found, check:
- The profile is defined under
[package.metadata.image-runner.profiles.xyz] - The
CARGO_IMAGE_RUNNER_PROFILEvalue matches the profile name exactly
Use cargo-image-runner check to see available configuration and active overrides.
Contributing
Contributions are welcome! The architecture is designed for extensibility:
- Add a bootloader: Implement the
Bootloadertrait - Add an image format: Implement the
ImageBuildertrait - Add a runner: Implement the
Runnertrait
License
MIT