//! Docker-based kernel build support. //! //! Provides a real, working Dockerfile for building a minimal Linux kernel //! from source inside Docker. This enables reproducible, CI-friendly kernel //! builds without requiring a local toolchain. use std::path::{Path, PathBuf}; use std::process::Command; use crate::config::MICROVM_KERNEL_CONFIG; use crate::error::KernelError; /// Default Linux kernel version to build. pub const DEFAULT_KERNEL_VERSION: &str = "6.8.12"; /// Generate the Dockerfile content for building a Linux kernel. /// /// The Dockerfile: /// 1. Starts from Alpine 3.19 (small, fast package install) /// 2. Installs the full GCC build toolchain + kernel build dependencies /// 3. Downloads the specified kernel version from kernel.org /// 4. Copies the RVF microVM kernel config /// 5. Runs `make olddefconfig` to fill in defaults, then builds bzImage /// 6. Multi-stage build: final image contains only the bzImage pub fn generate_dockerfile(kernel_version: &str) -> String { format!( r#"# RVF MicroVM Kernel Builder # Builds a minimal Linux kernel for Firecracker / QEMU microvm # Generated by rvf-kernel FROM alpine:3.19 AS builder # Install kernel build dependencies RUN apk add --no-cache \ build-base \ linux-headers \ bc \ flex \ bison \ elfutils-dev \ openssl-dev \ perl \ python3 \ cpio \ gzip \ wget \ xz # Download and extract kernel source ARG KERNEL_VERSION={kernel_version} RUN wget -q "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${{KERNEL_VERSION}}.tar.xz" && \ tar xf "linux-${{KERNEL_VERSION}}.tar.xz" && \ rm "linux-${{KERNEL_VERSION}}.tar.xz" # Copy kernel configuration COPY kernel.config "linux-${{KERNEL_VERSION}}/.config" WORKDIR "/linux-${{KERNEL_VERSION}}" # Apply config defaults and build RUN make olddefconfig && \ make -j"$(nproc)" bzImage # Extract just the bzImage in a scratch stage FROM scratch COPY --from=builder "/linux-{kernel_version}/arch/x86/boot/bzImage" /bzImage "# ) } /// Context directory structure for a Docker kernel build. pub struct DockerBuildContext { /// Path to the build context directory. pub context_dir: PathBuf, /// Kernel version being built. pub kernel_version: String, } impl DockerBuildContext { /// Prepare a Docker build context directory with the Dockerfile and kernel config. /// /// Creates: /// - `/Dockerfile` /// - `/kernel.config` pub fn prepare(context_dir: &Path, kernel_version: Option<&str>) -> Result { let version = kernel_version.unwrap_or(DEFAULT_KERNEL_VERSION); std::fs::create_dir_all(context_dir)?; // Write Dockerfile let dockerfile = generate_dockerfile(version); std::fs::write(context_dir.join("Dockerfile"), dockerfile)?; // Write kernel config std::fs::write(context_dir.join("kernel.config"), MICROVM_KERNEL_CONFIG)?; Ok(Self { context_dir: context_dir.to_path_buf(), kernel_version: version.to_string(), }) } /// Execute the Docker build and extract the resulting bzImage. /// /// Requires Docker to be installed and accessible. The build may take /// 10-30 minutes depending on CPU and network speed. /// /// Returns the bzImage bytes on success. pub fn build(&self) -> Result, KernelError> { let image_tag = format!("rvf-kernel-build:{}", self.kernel_version); // Build the Docker image let build_status = Command::new("docker") .args([ "build", "-t", &image_tag, "--build-arg", &format!("KERNEL_VERSION={}", self.kernel_version), ".", ]) .current_dir(&self.context_dir) .status() .map_err(|e| KernelError::DockerBuildFailed(format!("failed to run docker: {e}")))?; if !build_status.success() { return Err(KernelError::DockerBuildFailed(format!( "docker build exited with status {}", build_status ))); } // Clean up any leftover container from a previous run let _ = Command::new("docker") .args(["rm", "-f", "rvf-kernel-extract"]) .output(); // Create a temporary container to copy out the bzImage. // The image is FROM scratch (no shell), so we pass a dummy // entrypoint that won't be executed — docker create only // creates the container filesystem, it doesn't run anything. let create_output = Command::new("docker") .args([ "create", "--name", "rvf-kernel-extract", "--entrypoint", "", &image_tag, "/bzImage", ]) .output() .map_err(|e| KernelError::DockerBuildFailed(format!("docker create failed: {e}")))?; if !create_output.status.success() { let stderr = String::from_utf8_lossy(&create_output.stderr); return Err(KernelError::DockerBuildFailed(format!( "docker create failed: {stderr}" ))); } let bzimage_path = self.context_dir.join("bzImage"); let cp_status = Command::new("docker") .args([ "cp", "rvf-kernel-extract:/bzImage", &bzimage_path.to_string_lossy(), ]) .status() .map_err(|e| KernelError::DockerBuildFailed(format!("docker cp failed: {e}")))?; // Clean up the temporary container (best-effort) let _ = Command::new("docker") .args(["rm", "rvf-kernel-extract"]) .status(); if !cp_status.success() { return Err(KernelError::DockerBuildFailed( "docker cp failed to extract bzImage".into(), )); } let bzimage = std::fs::read(&bzimage_path)?; if bzimage.is_empty() { return Err(KernelError::DockerBuildFailed( "extracted bzImage is empty".into(), )); } Ok(bzimage) } } #[cfg(test)] mod tests { use super::*; #[test] fn dockerfile_contains_required_steps() { let dockerfile = generate_dockerfile(DEFAULT_KERNEL_VERSION); // Must start from Alpine assert!(dockerfile.contains("FROM alpine:3.19 AS builder")); // Must install build dependencies assert!(dockerfile.contains("build-base")); assert!(dockerfile.contains("flex")); assert!(dockerfile.contains("bison")); assert!(dockerfile.contains("elfutils-dev")); assert!(dockerfile.contains("openssl-dev")); // Must download kernel source assert!(dockerfile.contains("cdn.kernel.org")); assert!(dockerfile.contains(DEFAULT_KERNEL_VERSION)); // Must copy config assert!(dockerfile.contains("COPY kernel.config")); // Must run make assert!(dockerfile.contains("make olddefconfig")); assert!(dockerfile.contains("make -j")); assert!(dockerfile.contains("bzImage")); // Must use multi-stage build assert!(dockerfile.contains("FROM scratch")); assert!(dockerfile.contains("COPY --from=builder")); } #[test] fn dockerfile_uses_custom_version() { let dockerfile = generate_dockerfile("6.9.1"); assert!(dockerfile.contains("6.9.1")); } #[test] fn prepare_creates_files() { let dir = tempfile::TempDir::new().unwrap(); let ctx = DockerBuildContext::prepare(dir.path(), None).unwrap(); assert!(dir.path().join("Dockerfile").exists()); assert!(dir.path().join("kernel.config").exists()); assert_eq!(ctx.kernel_version, DEFAULT_KERNEL_VERSION); // Verify kernel.config content let config = std::fs::read_to_string(dir.path().join("kernel.config")).unwrap(); assert!(config.contains("CONFIG_64BIT=y")); assert!(config.contains("CONFIG_VIRTIO_PCI=y")); } #[test] fn prepare_with_custom_version() { let dir = tempfile::TempDir::new().unwrap(); let ctx = DockerBuildContext::prepare(dir.path(), Some("6.6.30")).unwrap(); assert_eq!(ctx.kernel_version, "6.6.30"); let dockerfile = std::fs::read_to_string(dir.path().join("Dockerfile")).unwrap(); assert!(dockerfile.contains("6.6.30")); } }