Skip to content

Development Guide

  • 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:

Terminal window
./ops/bootstrap/dev-setup.sh
Terminal window
# Build
just build
# Run CLI
just run -- --help
# Dev mode (auto-bootstraps the dev VM + Firecracker)
just run -- dev
# Release build (stripped, LTO)
just release-build
Terminal window
# Run all tests with nextest
just test
# Test a single crate
just test-crate mvm-core
# Run tests matching a filter
just test-filter "test_snapshot"
# Full CI gate (lint + test)
just ci
LocationTypeWhat it tests
crates/*/src/**/*.rs (#[cfg(test)])Unit testsInternal functions within the crate
crates/*/tests/*.rsIntegration testsPublic API of each crate
tests/cli.rsBinary testsCLI arg parsing, help output, subcommand structure
  • 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

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:

Terminal window
# 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 -- --nocapture

The 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.

Terminal window
just fmt # Format all code
just clippy # Lint (zero warnings required)
just lint # Both format check + clippy
  • Edition 2024: use statements don’t need extern crate; let chains supported
  • No clippy::too_many_arguments: never suppress this lint — refactor into a params struct
  • No format!() in format!() named args: extract to a variable first
  • Cross-crate imports: always use mvm_core::, mvm_runtime::, etc.

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.

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.

  • 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

When adding fields to structs in serialized state:

  1. Add #[serde(default)] to the new field for backward compatibility
  2. cargo test --workspace to find all broken test constructions
  3. Fix each one
  4. Add a unit test for the new behavior

Beyond the standard build/test/lint cycle, mvmctl provides commands for managing the dev environment:

Terminal window
# First-time setup (installs deps, creates the dev VM, default network)
just run -- init
# Image catalog — browse and build images from Nix templates
just run -- image list # browse bundled catalog
just run -- image search http # search by name/tag
just run -- image fetch minimal # build from catalog entry
# Named dev networks
just run -- network create isolated # create a named network
just run -- network list # list all networks
just run -- up --flake . --network isolated # attach VM to a network
# Interactive console (PTY-over-vsock, no SSH)
just run -- console myvm # interactive shell
just run -- console myvm --command "uname -a" # one-shot exec
# Cache and diagnostics
just run -- cache info # show cache dir and disk usage
just run -- cache prune # clean stale temp files
just run -- security status # security posture evaluation
just run -- doctor # dependency checks

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.console must be true in the guest security policy)
  • Single session per VM, 15-minute idle timeout
  • Supports both Firecracker and Apple Container backends

Dev tool state uses XDG-compliant paths (override with MVM_CACHE_DIR, MVM_CONFIG_DIR, etc.):

PathPurpose
~/.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.

WorkflowTriggerWhat it does
ci.ymlPush to main/feat/*, PRscheck, fmt, clippy, test (macOS + Linux), audit
release.ymlTags matching v*Builds 4 platform binaries, creates GitHub Release
publish-crates.ymlRelease publishedPublishes to crates.io in dependency order
pages.ymlPush to mainDeploys docs to GitHub Pages
Terminal window
# 1. Bump version in root Cargo.toml [workspace.package]
# 2. Update CHANGELOG.md
# 3. Commit and tag
git add -A && git commit -m "release: v0.3.0"
git tag v0.3.0
# 4. Push (triggers release.yml)
git push && git push --tags

The deploy guard (scripts/deploy-guard.sh) validates the tag matches the workspace version before publishing.