mirror of
https://github.com/ruvnet/RuView
synced 2026-08-08 20:11:43 +00:00
Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
+259
@@ -0,0 +1,259 @@
|
||||
//! 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user