260 lines
8.4 KiB
Rust
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"));
|
|
}
|
|
}
|