1use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use crate::config::MICROVM_KERNEL_CONFIG;
11use crate::error::KernelError;
12
13pub const DEFAULT_KERNEL_VERSION: &str = "6.8.12";
15
16pub 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
71pub struct DockerBuildContext {
73 pub context_dir: PathBuf,
75 pub kernel_version: String,
77}
78
79impl DockerBuildContext {
80 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 let dockerfile = generate_dockerfile(version);
95 std::fs::write(context_dir.join("Dockerfile"), dockerfile)?;
96
97 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 pub fn build(&self) -> Result<Vec<u8>, KernelError> {
113 let image_tag = format!("rvf-kernel-build:{}", self.kernel_version);
114
115 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 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 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 assert!(dockerfile.contains("FROM alpine:3.19 AS builder"));
196
197 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 assert!(dockerfile.contains("cdn.kernel.org"));
206 assert!(dockerfile.contains(DEFAULT_KERNEL_VERSION));
207
208 assert!(dockerfile.contains("COPY kernel.config"));
210
211 assert!(dockerfile.contains("make olddefconfig"));
213 assert!(dockerfile.contains("make -j"));
214 assert!(dockerfile.contains("bzImage"));
215
216 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 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}