Skip to main content

rvf_kernel/
docker.rs

1//! Docker-based kernel build support.
2//!
3//! Provides a real, working Dockerfile for building a minimal Linux kernel
4//! from source inside Docker. This enables reproducible, CI-friendly kernel
5//! builds without requiring a local toolchain.
6
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use crate::config::MICROVM_KERNEL_CONFIG;
11use crate::error::KernelError;
12
13/// Default Linux kernel version to build.
14pub const DEFAULT_KERNEL_VERSION: &str = "6.8.12";
15
16/// Generate the Dockerfile content for building a Linux kernel.
17///
18/// The Dockerfile:
19/// 1. Starts from Alpine 3.19 (small, fast package install)
20/// 2. Installs the full GCC build toolchain + kernel build dependencies
21/// 3. Downloads the specified kernel version from kernel.org
22/// 4. Copies the RVF microVM kernel config
23/// 5. Runs `make olddefconfig` to fill in defaults, then builds bzImage
24/// 6. Multi-stage build: final image contains only the bzImage
25pub fn generate_dockerfile(kernel_version: &str) -> String {
26    format!(
27        r#"# RVF MicroVM Kernel Builder
28# Builds a minimal Linux kernel for Firecracker / QEMU microvm
29# Generated by rvf-kernel
30
31FROM alpine:3.19 AS builder
32
33# Install kernel build dependencies
34RUN apk add --no-cache \
35    build-base \
36    linux-headers \
37    bc \
38    flex \
39    bison \
40    elfutils-dev \
41    openssl-dev \
42    perl \
43    python3 \
44    cpio \
45    gzip \
46    wget \
47    xz
48
49# Download and extract kernel source
50ARG KERNEL_VERSION={kernel_version}
51RUN wget -q "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${{KERNEL_VERSION}}.tar.xz" && \
52    tar xf "linux-${{KERNEL_VERSION}}.tar.xz" && \
53    rm "linux-${{KERNEL_VERSION}}.tar.xz"
54
55# Copy kernel configuration
56COPY kernel.config "linux-${{KERNEL_VERSION}}/.config"
57
58WORKDIR "/linux-${{KERNEL_VERSION}}"
59
60# Apply config defaults and build
61RUN make olddefconfig && \
62    make -j"$(nproc)" bzImage
63
64# Extract just the bzImage in a scratch stage
65FROM scratch
66COPY --from=builder "/linux-{kernel_version}/arch/x86/boot/bzImage" /bzImage
67"#
68    )
69}
70
71/// Context directory structure for a Docker kernel build.
72pub struct DockerBuildContext {
73    /// Path to the build context directory.
74    pub context_dir: PathBuf,
75    /// Kernel version being built.
76    pub kernel_version: String,
77}
78
79impl DockerBuildContext {
80    /// Prepare a Docker build context directory with the Dockerfile and kernel config.
81    ///
82    /// Creates:
83    /// - `<context_dir>/Dockerfile`
84    /// - `<context_dir>/kernel.config`
85    pub fn prepare(
86        context_dir: &Path,
87        kernel_version: Option<&str>,
88    ) -> Result<Self, KernelError> {
89        let version = kernel_version.unwrap_or(DEFAULT_KERNEL_VERSION);
90
91        std::fs::create_dir_all(context_dir)?;
92
93        // Write Dockerfile
94        let dockerfile = generate_dockerfile(version);
95        std::fs::write(context_dir.join("Dockerfile"), dockerfile)?;
96
97        // Write kernel config
98        std::fs::write(context_dir.join("kernel.config"), MICROVM_KERNEL_CONFIG)?;
99
100        Ok(Self {
101            context_dir: context_dir.to_path_buf(),
102            kernel_version: version.to_string(),
103        })
104    }
105
106    /// Execute the Docker build and extract the resulting bzImage.
107    ///
108    /// Requires Docker to be installed and accessible. The build may take
109    /// 10-30 minutes depending on CPU and network speed.
110    ///
111    /// Returns the bzImage bytes on success.
112    pub fn build(&self) -> Result<Vec<u8>, KernelError> {
113        let image_tag = format!("rvf-kernel-build:{}", self.kernel_version);
114
115        // Build the Docker image
116        let build_status = Command::new("docker")
117            .args([
118                "build",
119                "-t",
120                &image_tag,
121                "--build-arg",
122                &format!("KERNEL_VERSION={}", self.kernel_version),
123                ".",
124            ])
125            .current_dir(&self.context_dir)
126            .status()
127            .map_err(|e| {
128                KernelError::DockerBuildFailed(format!("failed to run docker: {e}"))
129            })?;
130
131        if !build_status.success() {
132            return Err(KernelError::DockerBuildFailed(format!(
133                "docker build exited with status {}",
134                build_status
135            )));
136        }
137
138        // Create a temporary container and copy out the bzImage
139        let create_output = Command::new("docker")
140            .args(["create", "--name", "rvf-kernel-extract", &image_tag])
141            .output()
142            .map_err(|e| {
143                KernelError::DockerBuildFailed(format!("docker create failed: {e}"))
144            })?;
145
146        if !create_output.status.success() {
147            return Err(KernelError::DockerBuildFailed(
148                "docker create failed".into(),
149            ));
150        }
151
152        let bzimage_path = self.context_dir.join("bzImage");
153        let cp_status = Command::new("docker")
154            .args([
155                "cp",
156                "rvf-kernel-extract:/bzImage",
157                &bzimage_path.to_string_lossy(),
158            ])
159            .status()
160            .map_err(|e| {
161                KernelError::DockerBuildFailed(format!("docker cp failed: {e}"))
162            })?;
163
164        // Clean up the temporary container (best-effort)
165        let _ = Command::new("docker")
166            .args(["rm", "rvf-kernel-extract"])
167            .status();
168
169        if !cp_status.success() {
170            return Err(KernelError::DockerBuildFailed(
171                "docker cp failed to extract bzImage".into(),
172            ));
173        }
174
175        let bzimage = std::fs::read(&bzimage_path)?;
176        if bzimage.is_empty() {
177            return Err(KernelError::DockerBuildFailed(
178                "extracted bzImage is empty".into(),
179            ));
180        }
181
182        Ok(bzimage)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn dockerfile_contains_required_steps() {
192        let dockerfile = generate_dockerfile(DEFAULT_KERNEL_VERSION);
193
194        // Must start from Alpine
195        assert!(dockerfile.contains("FROM alpine:3.19 AS builder"));
196
197        // Must install build dependencies
198        assert!(dockerfile.contains("build-base"));
199        assert!(dockerfile.contains("flex"));
200        assert!(dockerfile.contains("bison"));
201        assert!(dockerfile.contains("elfutils-dev"));
202        assert!(dockerfile.contains("openssl-dev"));
203
204        // Must download kernel source
205        assert!(dockerfile.contains("cdn.kernel.org"));
206        assert!(dockerfile.contains(DEFAULT_KERNEL_VERSION));
207
208        // Must copy config
209        assert!(dockerfile.contains("COPY kernel.config"));
210
211        // Must run make
212        assert!(dockerfile.contains("make olddefconfig"));
213        assert!(dockerfile.contains("make -j"));
214        assert!(dockerfile.contains("bzImage"));
215
216        // Must use multi-stage build
217        assert!(dockerfile.contains("FROM scratch"));
218        assert!(dockerfile.contains("COPY --from=builder"));
219    }
220
221    #[test]
222    fn dockerfile_uses_custom_version() {
223        let dockerfile = generate_dockerfile("6.9.1");
224        assert!(dockerfile.contains("6.9.1"));
225    }
226
227    #[test]
228    fn prepare_creates_files() {
229        let dir = tempfile::TempDir::new().unwrap();
230        let ctx = DockerBuildContext::prepare(dir.path(), None).unwrap();
231
232        assert!(dir.path().join("Dockerfile").exists());
233        assert!(dir.path().join("kernel.config").exists());
234        assert_eq!(ctx.kernel_version, DEFAULT_KERNEL_VERSION);
235
236        // Verify kernel.config content
237        let config = std::fs::read_to_string(dir.path().join("kernel.config")).unwrap();
238        assert!(config.contains("CONFIG_64BIT=y"));
239        assert!(config.contains("CONFIG_VIRTIO_PCI=y"));
240    }
241
242    #[test]
243    fn prepare_with_custom_version() {
244        let dir = tempfile::TempDir::new().unwrap();
245        let ctx = DockerBuildContext::prepare(dir.path(), Some("6.6.30")).unwrap();
246        assert_eq!(ctx.kernel_version, "6.6.30");
247
248        let dockerfile = std::fs::read_to_string(dir.path().join("Dockerfile")).unwrap();
249        assert!(dockerfile.contains("6.6.30"));
250    }
251}