wifi-densepose/vendor/ruvector/crates/rvf/rvf-kernel/src/docker.rs

260 lines
8.4 KiB
Rust

//! 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:
/// - `<context_dir>/Dockerfile`
/// - `<context_dir>/kernel.config`
pub fn prepare(context_dir: &Path, kernel_version: Option<&str>) -> Result<Self, KernelError> {
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<Vec<u8>, 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"));
}
}