8000 feat(minvmd): source libkrun from the upstream package by norrietaylor · Pull Request #466 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a544123
feat(minvmd): extend libkrun support to Linux/KVM
norrietaylor Jun 14, 2026
d6a1de5
feat(minvmd): add /dev/kvm capability check to daemon start path
norrietaylor Jun 15, 2026
9114c71
ci(minvmd): run Linux/KVM e2e on GitHub-hosted runners
norrietaylor Jun 17, 2026
dec7a88
ci(minvmd): fix libkrunfw build flags and drop hosted arm64 leg
norrietaylor Jun 17, 2026
8077ad3
ci(minvmd): install libprotobuf-dev for the materialize build
norrietaylor Jun 17, 2026
5d2182f
ci(minvmd): cache libkrun via restore+save so red runs don't rebuild it
norrietaylor Jun 17, 2026
87db08b
fix(minvmd): load the x86_64 bzImage as ImageGz, not Elf
norrietaylor Jun 17, 2026
c97f77b
ci(minvmd): drop the obsolete bridge_e2e step
norrietaylor Jun 17, 2026
2a10f6c
Merge branch 'main' into minvmd-linux-kvm
norrietaylor Jun 17, 2026
3006732
ci(minvmd): harden libkrun idempotency + daemon-state assertions
norrietaylor Jun 17, 2026
cf50eee
ci(minvmd): bump libkrun cache key for the version stamp
norrietaylor Jun 17, 2026
25ca0c5
ci(minvmd): poll for Running in the daemon lifecycle check
norrietaylor Jun 17, 2026
cd246b2
ci(minvmd): don't abort lifecycle check on stopped-status exit code
norrietaylor Jun 17, 2026
335c841
feat(minvmd): source libkrun from the upstream package
norrietaylor Jun 18, 2026
0c813bd
docs(minvmd): add Linux/KVM local build + e2e guide
norrietaylor Jun 18, 2026
48bceb0
feat(minimal2): enable minvmd auto-spawn on Linux
norrietaylor Jun 18, 2026
88e08f0
Merge remote-tracking branch 'origin/main' into minvmd-libkrun-upstream
norrietaylor Jun 18, 2026
e40a67a
chore(minvmd): remove unused from-source libkrun build script
norrietaylor Jun 19, 2026
a322992
docs(minvmd): split README e2e command so boot_e2e is not skipped
norrietaylor Jun 19, 2026
cae7c50
docs(minvmd): reconcile spec platform contract — Linux is a real backend
norrietaylor Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 20 additions & 37 deletions .github/workflows/ci-linux-kvm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ name: CI (Linux KVM)
# minvmd Linux/KVM end-to-end on a GitHub-hosted runner (#410, #411).
#
# Split from ci.yml and PATH-SCOPED so it runs only when the minvmd surface
# changes: it builds libkrun (incl. libkrunfw, which compiles a kernel) from
# source, so it is far heavier than a normal lane and should not burden unrelated
# PRs. Unlike the earlier self-hosted incarnation it needs no preprovisioned
# runner — KVM is available on GitHub-hosted x86_64 Linux runners, and libkrun is
# built + cached here.
# changes. It needs no preprovisioned runner — KVM is available on GitHub-hosted
# x86_64 Linux runners, and libkrun is materialized from the upstream package
# cache (a cache fetch keyed by the pinned locked_commit, NOT a from-source
# build) alongside the guest kernel + rootfs.
#
# x86_64 ONLY: GitHub-hosted arm64 runners (ubuntu-24.04-arm) do not expose
# /dev/kvm, so the Linux/KVM e2e can only run on x86_64. aarch64 + libkrun boot
Expand Down Expand Up @@ -43,7 +42,7 @@ on:
env:
CARGO_TERM_COLOR: always

# Cancel superseded runs so the heavy libkrun build isn't queued on stale commits.
# Cancel superseded runs so the e2e job isn't queued on stale commits.
concurrency:
group: ci-linux-kvm-${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
Expand All @@ -67,16 +66,15 @@ jobs:
# protoc + libprotobuf-dev: the `minimal` CLI (materialize) build. The
# well-known protos (google/protobuf/*.proto) live in libprotobuf-dev,
# which protobuf-compiler only pulls via recommends — so name it
# explicitly since we use --no-install-recommends. The rest: libkrunfw's
# kernel compile (flex/bison/bc/libelf/libssl/pyelftools/cpio) and
# libkrun's bindgen + link (clang/libclang/pkg-config/patchelf).
# explicitly since we use --no-install-recommends. cpio: packs the guest
# initramfs (scripts/build-initramfs.sh). git: source fetches during the
# CLI build. libkrun no longer builds from source here, so its old kernel
# /bindgen toolchain (flex/bison/bc/libelf/clang/patchelf/…) is gone.
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
protobuf-compiler libprotobuf-dev \
build-essential flex bison bc libelf-dev libssl-dev \
python3 python3-pyelftools cpio \
clang libclang-dev pkg-config patchelf git
cpio git

- name: Enable /dev/kvm access
# GitHub-hosted x86_64 Linux runners expose /dev/kvm but the runner user
Expand Down Expand Up @@ -106,35 +104,14 @@ jobs:
echo "MINVMD_INITRAMFS=$RUNNER_TEMP/initramfs.cpio"
} >> "$GITHUB_ENV"

- name: Restore libkrun cache (libkrunfw v5.5.0 + libkrun v1.19.0)
id: krun-cache
# Keyed on the pinned versions: the kernel compile only runs on a miss.
# Bump the suffix when scripts/build-libkrun.sh changes pins.
uses: actions/cache/restore@v5
with:
path: ${{ env.KRUN_PREFIX }}
key: x86_64-libkrun-fw5.5.0-krun1.19.0-blk-stamped

- name: Build libkrun from source (cached)
run: ./scripts/build-libkrun.sh "$KRUN_PREFIX"

- name: Save libkrun cache
# Save right after the build so the expensive kernel compile is cached
# even when a later step (e2e/bench) fails — the default post-job save is
# skipped on job failure. Skipped on a cache hit (keys are immutable).
if: steps.krun-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ env.KRUN_PREFIX }}
key: x86_64-libkrun-fw5.5.0-krun1.19.0-blk-stamped

- name: Export libkrun link + runtime paths
# LIBKRUN_PREFIX: minvmd build.rs link-search dir (it emits an rpath to
# it too). LD_LIBRARY_PATH: libkrun.so -> libkrunfw.so.5 at runtime.
# it too). LD_LIBRARY_PATH: libkrun.so -> libkrunfw.so.5 at runtime. Both
# point at the flat prefix that the materialize step below populates.
run: |
{
echo "LIBKRUN_PREFIX=$KRUN_PREFIX/lib64"
echo "LD_LIBRARY_PATH=$KRUN_PREFIX/lib64${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
echo "LIBKRUN_PREFIX=$KRUN_PREFIX"
echo "LD_LIBRARY_PATH=$KRUN_PREFIX${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
} >> "$GITHUB_ENV"

- name: Install cross
Expand All @@ -154,6 +131,12 @@ jobs:
key: ${{ runner.os }}-x86_64-minvmd-kvm-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-x86_64-minvmd-kvm-

- name: Materialize libkrun prefix (upstream package)
# Cache fetch of the upstream libkrun package (libkrun.so + libkrunfw.so.5),
# extracted into $KRUN_PREFIX. Runs after the cargo cache restore so the
# `minimal` CLI build it drives is incremental.
run: ./scripts/fetch-libkrun.sh "$KRUN_PREFIX" x86_64

- name: Materialize guest kernel (raw Image)
run: ./scripts/fetch-artifact.sh virtio-kernel "$RUNNER_TEMP/vmlinuz" x86_64

Expand Down
12 changes: 11 additions & 1 deletion .minimal/minimal.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[upstream] # Source of software & tooling
repo = "https://github.com/gominimal/pkgs"
branch = "main"
locked_commit = "560b0d09914758441770363ed60823dff6c921b4"
locked_commit = "087287e4b6f448eaf3d0ca7198dad384a0778b6d"

[stack]
use = "rust"
Expand Down Expand Up @@ -55,6 +55,16 @@ type = "raw-file"
packages = ["microvm-rootfs"]
path = "usr/share/microvm-rootfs/rootfs.img"

# libkrun KVM backend (shared lib + its libkrunfw firmware) from the upstream
# libkrun package. Materialized as an OCI image whose layers carry the full
# runtime closure; scripts/fetch-libkrun.sh extracts just libkrun.so* and
# libkrunfw.so* into a link/runtime prefix. This replaces the former from-source
# build — libkrunfw compiles a guest kernel, far too heavy to rebuild in CI on
# every run.
[outputs.libkrun]
type = "oci-image"
packages = ["libkrun"]

[tasks.vim]
profile = "demo"
state_key = ""
Expand Down
131 changes: 80 additions & 51 deletions crates/minimal2/src/autospawn.rs
Original file line number Diff line number Diff line change
@@ -1,55 +1,79 @@
//! Auto-spawn logic for minvmd on macOS (R4.5).
//! Auto-spawn logic for minvmd (R4.5).
//!
//! On macOS, checks the minvmd state before connecting to the UDS. If minvmd
//! is not running, spawns `minvmd run --detach` and waits for the UDS to become
//! available.
//!
//! On Linux, this module is a no-op.
//! Before connecting to the minvmd UDS, the CLI reads `state.toml`. If minvmd is
//! not running it spawns `minvmd run --detach` and waits (with a timeout) for the
//! UDS to become available. This runs on both macOS (Hypervisor.framework) and
//! Linux (KVM); on any other target it is a no-op.

use std::io;
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
use std::process::Command;
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
use std::thread;
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
use std::time::{Duration, Instant};

#[cfg(any(target_os = "macos", target_os = "linux"))]
use minvmd::lifecycle::Lifecycle;

/// Default timeout in seconds to wait for the UDS when spawning minvmd (R4.5).
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
const DEFAULT_SPAWN_TIMEOUT_SECS: u64 = 8;

/// Poll interval while waiting for a shutting-down minvmd to reach a terminal
/// state before deciding whether to spawn.
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
const STOPPING_POLL_MS: u64 = 100;

/// Max time to wait for an in-progress `minvmd stop` (SIGTERM → SIGKILL, ~5 s)
/// to finish before giving up with a clear error.
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
const STOPPING_WAIT_SECS: u64 = 6;

/// Check if minvmd needs to be spawned, and spawn it if necessary.
/// What to do given the lifecycle read from `state.toml`.
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[derive(Debug, PartialEq, Eq)]
enum Decision {
/// minvmd is already up (or coming up); nothing to do.
AlreadyRunning,
/// minvmd is shutting down; wait for it to reach a terminal state first.
WaitForStopping,
/// minvmd is not running; spawn it.
Spawn,
}

/// Pure mapping from a lifecycle state to the action the CLI should take. Kept
/// separate from the I/O so it can be exhaustively unit-tested. `Lifecycle` is
/// `#[non_exhaustive]`, so any future state is treated conservatively as "spawn".
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn classify(lifecycle: Lifecycle) -> Decision {
match lifecycle {
Lifecycle::Running | Lifecycle::Starting => Decision::AlreadyRunning,
Lifecycle::Stopping => Decision::WaitForStopping,
Lifecycle::Stopped | Lifecycle::NotProvisioned => Decision::Spawn,
_ => Decision::Spawn,
}
}

/// Check whether minvmd needs to be spawned, and spawn it if necessary (R4.5).
///
/// On macOS (R4.5):
/// - Reads the minvmd state from `state.toml`
/// - If not running, spawns `minvmd run --detach` with a timeout
/// - Returns an error if spawn fails
/// - Reads the minvmd state from `state.toml`.
/// - If already running/starting, returns immediately.
/// - If stopping, waits for the shutdown to finish, then spawns.
/// - Otherwise spawns `minvmd run --detach` with a timeout.
///
/// On Linux, this is a no-op since minvmd does not exist on Linux.
#[cfg(target_os = "macos")]
/// On targets with no minvmd backend this is a no-op (see below).
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn ensure_minvmd_running() -> io::Result<()> {
let state_dir = minvmd::state::StateDir::new(minvmd::state::StateDir::default_path())?;

// Read current state (R4.5: check state.toml before connecting)
let state = state_dir.read_state()?;

// If already running or starting, no need to spawn
match state.lifecycle {
minvmd::lifecycle::Lifecycle::Running | minvmd::lifecycle::Lifecycle::Starting => {
// Read current state (R4.5: check state.toml before connecting).
match classify(state_dir.read_state()?.lifecycle) {
Decision::AlreadyRunning => {
tracing::debug!("minvmd already running or starting");
return Ok(());
}
minvmd::lifecycle::Lifecycle::Stopping => {
Decision::WaitForStopping => {
// `minvmd stop` can take up to ~5 s (SIGTERM → SIGKILL escalation),
// so a fixed short sleep usually leaves the daemon still Stopping.
// Spawning then would make the new `minvmd run` bail on its own
Expand All @@ -59,32 +83,27 @@ pub fn ensure_minvmd_running() -> io::Result<()> {
let deadline = Instant::now() + Duration::from_secs(STOPPING_WAIT_SECS);
loop {
thread::sleep(Duration::from_millis(STOPPING_POLL_MS));
match state_dir.read_state()?.lifecycle {
match classify(state_dir.read_state()?.lifecycle) {
// Something else restarted it in the meantime.
minvmd::lifecycle::Lifecycle::Running
| minvmd::lifecycle::Lifecycle::Starting => return Ok(()),
Decision::AlreadyRunning => return Ok(()),
// Shutdown completed; fall through to spawn.
minvmd::lifecycle::Lifecycle::Stopped
| minvmd::lifecycle::Lifecycle::NotProvisioned => break,
_ if Instant::now() >= deadline => {
Decision::Spawn => break,
Decision::WaitForStopping if Instant::now() >= deadline => {
return Err(io::Error::other(
"minvmd is still stopping after waiting; try again shortly",
));
}
// Still stopping (or a future state); keep polling.
_ => continue,
// Still stopping; keep polling.
Decision::WaitForStopping => continue,
}
}
}
minvmd::lifecycle::Lifecycle::Stopped | minvmd::lifecycle::Lifecycle::NotProvisioned => {
// Not running; will spawn below
Decision::Spawn => {
// Not running; will spawn below.
}
// Lifecycle is #[non_exhaustive]; treat any future state conservatively
// as "not running" and fall through to spawn.
_ => {}
}

// Not running; spawn minvmd run --detach with timeout (R4.5)
// Not running; spawn minvmd run --detach with timeout (R4.5).
tracing::info!(
"spawning minvmd run --detach with timeout {}",
DEFAULT_SPAWN_TIMEOUT_SECS
Expand All @@ -109,23 +128,33 @@ pub fn ensure_minvmd_running() -> io::Result<()> {
Ok(())
}

/// On Linux, auto-spawn is a no-op since minvmd is not available.
#[cfg(target_os = "linux")]
/// On targets without a minvmd backend (e.g. Windows), auto-spawn is a no-op.
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
pub fn ensure_minvmd_running() -> io::Result<()> {
tracing::debug!("ensure_minvmd_running is a no-op on Linux");
tracing::debug!("ensure_minvmd_running is a no-op on this platform");
Ok(())
}

#[cfg(test)]
#[cfg(any(target_os = "macos", target_os = "linux"))]
mod tests {
use super::{Decision, classify};
use minvmd::lifecycle::Lifecycle;

#[test]
fn running_or_starting_needs_no_spawn() {
assert_eq!(classify(Lifecycle::Running), Decision::AlreadyRunning);
assert_eq!(classify(Lifecycle::Starting), Decision::AlreadyRunning);
}

#[test]
fn stopped_or_not_provisioned_spawns() {
assert_eq!(classify(Lifecycle::Stopped), Decision::Spawn);
assert_eq!(classify(Lifecycle::NotProvisioned), Decision::Spawn);
}

#[test]
#[cfg(target_os = "linux")]
fn test_autospawn_noop_on_linux() {
// On Linux, ensure_minvmd_running should be a no-op and always succeed
let result = super::ensure_minvmd_running();
assert!(
result.is_ok(),
"ensure_minvmd_running should always succeed on Linux"
);
fn stopping_waits_for_terminal_state() {
assert_eq!(classify(Lifecycle::Stopping), Decision::WaitForStopping);
}
}
12 changes: 6 additions & 6 deletions crates/minimal2/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ async fn main() -> Result<(), ()> {

match cli.command {
Command::Ls => {
// Ensure minvmd is running before listing (R4.5). On macOS this
// auto-spawns minvmd; on Linux `ensure_minvmd_running` is a no-op,
// so `ls` falls through to the placeholder `[]` below.
// Ensure minvmd is running before listing (R4.5). On macOS and Linux
// this auto-spawns minvmd (`minvmd run --detach`); on other targets
// `ensure_minvmd_running` is a no-op.
//
// TODO(#311): minimal targets Linux too — add the Linux backend and
// real session listing over the minimald UDS; `[]` is a placeholder
// on both platforms until then.
// TODO(#311): real session listing over the minimald UDS is still
// pending, so `ls` falls through to the placeholder `[]` below even
// once minvmd is up.
if let Err(e) = autospawn::ensure_minvmd_running() {
eprintln!("Failed to ensure minvmd is running: {}", e);
return Err(());
Expand Down
Loading
Loading
0