Development Guide
Prerequisites
Section titled “Prerequisites”- Rust 1.85+ (Edition 2024) — install via rustup
- macOS or Linux — macOS for development via Apple Container (26+) or libkrun (pre-26); Linux for native
/dev/kvm - Nix (optional) — only needed for building microVM images
Run the bootstrap script on a fresh machine:
./ops/bootstrap/dev-setup.shBuilding and Running
Section titled “Building and Running”# Buildjust build
# Run CLIjust run -- --help
# Dev mode (auto-bootstraps the dev VM + Firecracker)just run -- dev
# Release build (stripped, LTO)just release-buildTesting
Section titled “Testing”# Run all tests with nextestjust test
# Test a single cratejust test-crate mvm-core
# Run tests matching a filterjust test-filter "test_snapshot"
# Full CI gate (lint + test)just ciTest Organization
Section titled “Test Organization”| Location | Type | What it tests |
|---|---|---|
crates/*/src/**/*.rs (#[cfg(test)]) | Unit tests | Internal functions within the crate |
crates/*/tests/*.rs | Integration tests | Public API of each crate |
tests/cli.rs | Binary tests | CLI arg parsing, help output, subcommand structure |
Testing Conventions
Section titled “Testing Conventions”- Unit tests go in
#[cfg(test)] mod tests {}at the bottom of the source file - CLI binary tests go in root
tests/cli.rs - Use
#[serde(default)]when adding fields to structs used in test fixtures
Gated E2E: the core-demo regression guard
Section titled “Gated E2E: the core-demo regression guard”crates/mvm-cli/tests/core_demo_e2e.rs exercises the whole dev up → compile → up → vsock ping spine end-to-end. It boots the persistent builder VM, lowers examples/python/hello-app/app.py to a flake, builds + boots the workload microVM, and waits for the guest agent to answer over vsock. Default-skips so it doesn’t fire on routine cargo test runs; gate is MVM_E2E_SMOKE=1:
# Local run — requires libkrun + libkrunfw + gvproxy on macOS, or# native /dev/kvm on Linux. Threads `--hypervisor` per host.MVM_E2E_SMOKE=1 cargo test -p mvm-cli --test core_demo_e2e -- --nocaptureThe lane mirrored at .github/workflows/ci.yml::core-demo-e2e runs the same command on a self-hosted runner labelled [self-hosted, macOS, ARM64, libkrun], gated on the MACOS_LIBKRUN_AVAILABLE repo variable. GitHub-hosted macOS runners cannot serve this lane (no nested HVF, no libkrun) — it stays opt-in until a self-hosted runner is wired.
The same gated convention covers sdks/python/tests/test_sandbox_exec.py, which exercises Sandbox.exec(*argv) -> ExecResult against a real microVM. Default-skips on pytest; opt-in with MVM_E2E_SMOKE=1 python -m pytest sdks/python/tests/test_sandbox_exec.py.
Linting and Formatting
Section titled “Linting and Formatting”just fmt # Format all codejust clippy # Lint (zero warnings required)just lint # Both format check + clippyStyle Rules
Section titled “Style Rules”- Edition 2024:
usestatements don’t needextern crate; let chains supported - No
clippy::too_many_arguments: never suppress this lint — refactor into a params struct - No
format!()informat!()named args: extract to a variable first - Cross-crate imports: always use
mvm_core::,mvm_runtime::, etc.
Architecture Principles
Section titled “Architecture Principles”Multi-Backend
Section titled “Multi-Backend”mvmctl’s supported local microVM hosts are native Linux with /dev/kvm and macOS Apple Silicon. Firecracker is the Linux baseline; Apple Container and libkrun-backed components cover Apple Silicon macOS. Docker remains a Tier 3 convenience fallback, not a microVM isolation boundary. WSL2 nested KVM and a Hyper-V managed Linux builder are future backend work.
Host vs. VM
Section titled “Host vs. VM”All Linux build operations run inside the builder VM on macOS:
// On Linux this runs directly on the host; on supported macOS hosts it// routes into the libkrun-backed builder VM.mvm_runtime::shell::run_in_vm("ip link add br-tenant-1 type bridge")?;On native Linux, run_in_vm executes directly on the host. On supported macOS Apple Silicon hosts, it delegates into the builder VM.
Key Patterns
Section titled “Key Patterns”- Idempotent operations: every setup step checks if already done before acting
- Config drive for metadata: instance metadata delivered via read-only ext4 disk
- Vsock over SSH: guest communication uses vsock, not sshd (all backends)
- Same rootfs everywhere: Nix-built ext4 images work on all backends
Adding New Types
Section titled “Adding New Types”When adding fields to structs in serialized state:
- Add
#[serde(default)]to the new field for backward compatibility cargo test --workspaceto find all broken test constructions- Fix each one
- Add a unit test for the new behavior
Developer Workflow Commands
Section titled “Developer Workflow Commands”Beyond the standard build/test/lint cycle, mvmctl provides commands for managing the dev environment:
# First-time setup (installs deps, creates the dev VM, default network)just run -- init
# Image catalog — browse and build images from Nix templatesjust run -- image list # browse bundled catalogjust run -- image search http # search by name/tagjust run -- image fetch minimal # build from catalog entry
# Named dev networksjust run -- network create isolated # create a named networkjust run -- network list # list all networksjust run -- up --flake . --network isolated # attach VM to a network
# Interactive console (PTY-over-vsock, no SSH)just run -- console myvm # interactive shelljust run -- console myvm --command "uname -a" # one-shot exec
# Cache and diagnosticsjust run -- cache info # show cache dir and disk usagejust run -- cache prune # clean stale temp filesjust run -- security status # security posture evaluationjust run -- doctor # dependency checksConsole Access
Section titled “Console Access”microVMs have no SSH. Interactive access is via mvmctl console which uses PTY-over-vsock:
- Authenticated via the existing Ed25519 vsock protocol
- Dev-mode only (
access.consolemust betruein the guest security policy) - Single session per VM, 15-minute idle timeout
- Supports both Firecracker and Apple Container backends
XDG Directory Layout
Section titled “XDG Directory Layout”Dev tool state uses XDG-compliant paths (override with MVM_CACHE_DIR, MVM_CONFIG_DIR, etc.):
| Path | Purpose |
|---|---|
~/.cache/mvm/ | Build artifacts, images, VM runtime state |
~/.config/mvm/ | User config (config.toml) |
~/.local/state/mvm/ | Logs, audit trail |
~/.local/share/mvm/ | Templates, network definitions, VM name registry |
Legacy ~/.mvm/ paths are auto-detected as fallback.
| Workflow | Trigger | What it does |
|---|---|---|
ci.yml | Push to main/feat/*, PRs | check, fmt, clippy, test (macOS + Linux), audit |
release.yml | Tags matching v* | Builds 4 platform binaries, creates GitHub Release |
publish-crates.yml | Release published | Publishes to crates.io in dependency order |
pages.yml | Push to main | Deploys docs to GitHub Pages |
Release Process
Section titled “Release Process”# 1. Bump version in root Cargo.toml [workspace.package]# 2. Update CHANGELOG.md# 3. Commit and taggit add -A && git commit -m "release: v0.3.0"git tag v0.3.0
# 4. Push (triggers release.yml)git push && git push --tagsThe deploy guard (scripts/deploy-guard.sh) validates the tag matches the workspace version before publishing.