1pub 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 pub fn auth(mut self, auth: RegistryAuth) -> Self {
112 self.auth = auth;
113 self
114 }
115
116 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 pub fn inject_file(mut self, file: InjectFile) -> Self {
129 self.inject_files.push(file);
130 self
131 }
132
133 pub fn init_script(mut self, path: impl Into<PathBuf>) -> Self {
136 self.init_script = Some(path.into());
137 self
138 }
139
140 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 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}