Skip to main content

initramfs_builder/
lib.rs

1//! # initramfs-builder
2//!
3//! Convert Docker/OCI images to bootable initramfs for microVMs.
4//!
5//! ## Example
6//!
7//! ```no_run
8//! use initramfs_builder::{InitramfsBuilder, Compression};
9//!
10//! #[tokio::main]
11//! async fn main() -> anyhow::Result<()> {
12//!     InitramfsBuilder::new()
13//!         .image("python:3.11-alpine")
14//!         .compression(Compression::Gzip)
15//!         .exclude(&["/usr/share/doc/*", "/var/cache/*"])
16//!         .inject("./cloude-agentd", "/usr/bin/cloude-agentd")
17//!         .init_script("./init.sh")
18//!         .build("python.cpio.gz")
19//!         .await?;
20//!     Ok(())
21//! }
22//! ```
23
24pub mod error;
25pub mod image;
26pub mod initramfs;
27pub mod registry;
28
29pub use error::{BuilderError, Result};
30pub use initramfs::{compress_archive, Compression};
31pub use registry::{PullOptions, RegistryAuth, RegistryClient};
32
33use anyhow::Context;
34use image::RootfsBuilder;
35use initramfs::CpioArchive;
36use std::fs;
37use std::os::unix::fs::PermissionsExt;
38use std::path::{Path, PathBuf};
39use tracing::info;
40
41#[derive(Debug, Clone)]
42pub struct InjectFile {
43    pub src: PathBuf,
44    pub dest: PathBuf,
45    pub executable: bool,
46}
47
48impl InjectFile {
49    pub fn new(src: impl Into<PathBuf>, dest: impl Into<PathBuf>) -> Self {
50        Self {
51            src: src.into(),
52            dest: dest.into(),
53            executable: false,
54        }
55    }
56
57    pub fn executable(mut self) -> Self {
58        self.executable = true;
59        self
60    }
61}
62
63pub struct InitramfsBuilder {
64    image: Option<String>,
65    compression: Compression,
66    exclude_patterns: Vec<String>,
67    platform_os: String,
68    platform_arch: String,
69    auth: RegistryAuth,
70    inject_files: Vec<InjectFile>,
71    init_script: Option<PathBuf>,
72}
73
74impl InitramfsBuilder {
75    pub fn new() -> Self {
76        Self {
77            image: None,
78            compression: Compression::default(),
79            exclude_patterns: Vec::new(),
80            platform_os: "linux".to_string(),
81            platform_arch: "amd64".to_string(),
82            auth: RegistryAuth::default(),
83            inject_files: Vec::new(),
84            init_script: None,
85        }
86    }
87
88    pub fn image(mut self, image: &str) -> Self {
89        self.image = Some(image.to_string());
90        self
91    }
92
93    pub fn compression(mut self, compression: Compression) -> Self {
94        self.compression = compression;
95        self
96    }
97
98    pub fn exclude(mut self, patterns: &[&str]) -> Self {
99        self.exclude_patterns
100            .extend(patterns.iter().map(|s| s.to_string()));
101        self
102    }
103
104    pub fn platform(mut self, os: &str, arch: &str) -> Self {
105        self.platform_os = os.to_string();
106        self.platform_arch = arch.to_string();
107        self
108    }
109
110    /// Set authentication credentials
111    pub fn auth(mut self, auth: RegistryAuth) -> Self {
112        self.auth = auth;
113        self
114    }
115
116    /// Inject a file into the initramfs
117    ///
118    /// # Arguments
119    /// * `src` - Source path on host filesystem
120    /// * `dest` - Destination path inside initramfs (e.g., "/usr/bin/myagent")
121    pub fn inject(mut self, src: impl Into<PathBuf>, dest: impl Into<PathBuf>) -> Self {
122        self.inject_files
123            .push(InjectFile::new(src, dest).executable());
124        self
125    }
126
127    /// Inject a file with custom options
128    pub fn inject_file(mut self, file: InjectFile) -> Self {
129        self.inject_files.push(file);
130        self
131    }
132
133    /// Set a custom init script that will be placed at /init
134    /// This script runs as PID 1 when the kernel boots
135    pub fn init_script(mut self, path: impl Into<PathBuf>) -> Self {
136        self.init_script = Some(path.into());
137        self
138    }
139
140    /// Build the initramfs and write it to the output path
141    pub async fn build<P: AsRef<Path>>(self, output: P) -> anyhow::Result<BuildResult> {
142        let image = self.image.as_ref().context("No image specified")?;
143
144        info!("Building initramfs from {}", image);
145
146        let client = RegistryClient::new(self.auth);
147        let exclude_refs: Vec<&str> = self.exclude_patterns.iter().map(|s| s.as_str()).collect();
148
149        let mut rootfs_builder = RootfsBuilder::new(client)
150            .platform(&self.platform_os, &self.platform_arch)
151            .exclude(&exclude_refs);
152
153        let rootfs_path = rootfs_builder.build(image).await?;
154
155        for inject in &self.inject_files {
156            let dest_path = if inject.dest.is_absolute() {
157                rootfs_path.join(inject.dest.strip_prefix("/").unwrap_or(&inject.dest))
158            } else {
159                rootfs_path.join(&inject.dest)
160            };
161
162            if let Some(parent) = dest_path.parent() {
163                fs::create_dir_all(parent)?;
164            }
165
166            info!("Injecting {:?} -> {:?}", inject.src, inject.dest);
167            fs::copy(&inject.src, &dest_path)
168                .with_context(|| format!("Failed to inject {:?}", inject.src))?;
169
170            if inject.executable {
171                let mut perms = fs::metadata(&dest_path)?.permissions();
172                perms.set_mode(0o755);
173                fs::set_permissions(&dest_path, perms)?;
174            }
175        }
176
177        if let Some(init_src) = &self.init_script {
178            let init_dest = rootfs_path.join("init");
179            info!("Setting init script from {:?}", init_src);
180            fs::copy(init_src, &init_dest)
181                .with_context(|| format!("Failed to copy init script from {:?}", init_src))?;
182
183            // Make executable
184            let mut perms = fs::metadata(&init_dest)?.permissions();
185            perms.set_mode(0o755);
186            fs::set_permissions(&init_dest, perms)?;
187        }
188
189        info!("Creating CPIO archive from {:?}", rootfs_path);
190
191        let archive = CpioArchive::from_directory(&rootfs_path)?;
192
193        let mut cpio_data = Vec::new();
194        archive.write_to(&mut cpio_data)?;
195
196        info!(
197            "CPIO archive: {} entries, {} bytes uncompressed",
198            archive.len(),
199            cpio_data.len()
200        );
201
202        let output_size = compress_archive(&cpio_data, output.as_ref(), self.compression)?;
203
204        Ok(BuildResult {
205            entries: archive.len(),
206            uncompressed_size: cpio_data.len() as u64,
207            compressed_size: output_size,
208            compression: self.compression,
209            injected_files: self.inject_files.len(),
210            has_custom_init: self.init_script.is_some(),
211        })
212    }
213}
214
215impl Default for InitramfsBuilder {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221#[derive(Debug)]
222pub struct BuildResult {
223    pub entries: usize,
224    pub uncompressed_size: u64,
225    pub compressed_size: u64,
226    pub compression: Compression,
227    pub injected_files: usize,
228    pub has_custom_init: bool,
229}