Skip to main content

a3s_box_runtime/rootfs/
builder.rs

1//! Rootfs builder for guest VM.
2//!
3//! Creates a minimal rootfs containing:
4//! - Basic directory structure
5//! - Essential configuration files
6
7use std::fs;
8use std::path::PathBuf;
9
10use a3s_box_core::error::{BoxError, Result};
11
12use super::layout::GuestLayout;
13
14/// Builder for creating guest rootfs.
15pub struct RootfsBuilder {
16    /// Target rootfs directory.
17    rootfs_path: PathBuf,
18
19    /// Guest layout configuration.
20    layout: GuestLayout,
21}
22
23impl RootfsBuilder {
24    /// Create a new rootfs builder.
25    pub fn new(rootfs_path: impl Into<PathBuf>) -> Self {
26        Self {
27            rootfs_path: rootfs_path.into(),
28            layout: GuestLayout::default(),
29        }
30    }
31
32    /// Set a custom guest layout.
33    pub fn with_layout(mut self, layout: GuestLayout) -> Self {
34        self.layout = layout;
35        self
36    }
37
38    /// Build the rootfs.
39    pub fn build(&self) -> Result<()> {
40        tracing::info!(
41            rootfs = %self.rootfs_path.display(),
42            "Building guest rootfs"
43        );
44
45        // Create base directory
46        fs::create_dir_all(&self.rootfs_path).map_err(|e| {
47            BoxError::BuildError(format!(
48                "Failed to create rootfs directory {}: {}",
49                self.rootfs_path.display(),
50                e
51            ))
52        })?;
53
54        // Create directory structure
55        self.create_directories()?;
56
57        // Create essential files
58        self.create_essential_files()?;
59
60        tracing::info!("Guest rootfs built successfully");
61        Ok(())
62    }
63
64    /// Create the directory structure.
65    fn create_directories(&self) -> Result<()> {
66        for dir in self.layout.required_dirs() {
67            let full_path = self.rootfs_path.join(dir.trim_start_matches('/'));
68            fs::create_dir_all(&full_path).map_err(|e| {
69                BoxError::BuildError(format!(
70                    "Failed to create directory {}: {}",
71                    full_path.display(),
72                    e
73                ))
74            })?;
75            tracing::debug!(dir = %full_path.display(), "Created directory");
76        }
77        Ok(())
78    }
79
80    /// Create essential configuration files.
81    fn create_essential_files(&self) -> Result<()> {
82        // /etc/passwd - minimal user database
83        let passwd_content =
84            "root:x:0:0:root:/root:/bin/sh\nnobody:x:65534:65534:nobody:/:/bin/false\n";
85        self.write_file("etc/passwd", passwd_content)?;
86
87        // /etc/group - minimal group database
88        let group_content = "root:x:0:\nnogroup:x:65534:\n";
89        self.write_file("etc/group", group_content)?;
90
91        // /etc/hosts - basic hosts file
92        let hosts_content = "127.0.0.1\tlocalhost\n::1\t\tlocalhost\n";
93        self.write_file("etc/hosts", hosts_content)?;
94
95        // /etc/resolv.conf - DNS configuration (can be overridden)
96        let resolv_content = "nameserver 8.8.8.8\nnameserver 8.8.4.4\n";
97        self.write_file("etc/resolv.conf", resolv_content)?;
98
99        // /etc/nsswitch.conf - name service switch configuration
100        let nsswitch_content = "passwd: files\ngroup: files\nhosts: files dns\n";
101        self.write_file("etc/nsswitch.conf", nsswitch_content)?;
102
103        Ok(())
104    }
105
106    /// Write a file to the rootfs.
107    fn write_file(&self, relative_path: &str, content: &str) -> Result<()> {
108        let full_path = self.rootfs_path.join(relative_path);
109
110        // Ensure parent directory exists
111        if let Some(parent) = full_path.parent() {
112            fs::create_dir_all(parent).map_err(|e| {
113                BoxError::BuildError(format!("Failed to create parent directory: {}", e))
114            })?;
115        }
116
117        fs::write(&full_path, content).map_err(|e| {
118            BoxError::BuildError(format!("Failed to write {}: {}", full_path.display(), e))
119        })?;
120
121        tracing::debug!(path = %full_path.display(), "Created file");
122        Ok(())
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use tempfile::TempDir;
130
131    #[test]
132    fn test_rootfs_builder_creates_directories() {
133        let temp_dir = TempDir::new().unwrap();
134        let rootfs_path = temp_dir.path().join("rootfs");
135
136        let builder = RootfsBuilder::new(&rootfs_path);
137        builder.build().unwrap();
138
139        // Check directories were created
140        assert!(rootfs_path.join("dev").exists());
141        assert!(rootfs_path.join("proc").exists());
142        assert!(rootfs_path.join("sys").exists());
143        assert!(rootfs_path.join("tmp").exists());
144        assert!(rootfs_path.join("etc").exists());
145        assert!(rootfs_path.join("workspace").exists());
146    }
147
148    #[test]
149    fn test_rootfs_builder_creates_essential_files() {
150        let temp_dir = TempDir::new().unwrap();
151        let rootfs_path = temp_dir.path().join("rootfs");
152
153        let builder = RootfsBuilder::new(&rootfs_path);
154        builder.build().unwrap();
155
156        // Check files were created
157        assert!(rootfs_path.join("etc/passwd").exists());
158        assert!(rootfs_path.join("etc/group").exists());
159        assert!(rootfs_path.join("etc/hosts").exists());
160        assert!(rootfs_path.join("etc/resolv.conf").exists());
161
162        // Check content
163        let passwd = fs::read_to_string(rootfs_path.join("etc/passwd")).unwrap();
164        assert!(passwd.contains("root:x:0:0"));
165    }
166
167    #[test]
168    fn test_rootfs_builder_essential_files_content() {
169        let temp_dir = TempDir::new().unwrap();
170        let rootfs_path = temp_dir.path().join("rootfs");
171
172        RootfsBuilder::new(&rootfs_path).build().unwrap();
173
174        // Verify /etc/passwd content
175        let passwd = fs::read_to_string(rootfs_path.join("etc/passwd")).unwrap();
176        assert!(passwd.contains("root:x:0:0:root:/root:/bin/sh"));
177        assert!(passwd.contains("nobody:x:65534:65534"));
178
179        // Verify /etc/group content
180        let group = fs::read_to_string(rootfs_path.join("etc/group")).unwrap();
181        assert!(group.contains("root:x:0:"));
182        assert!(group.contains("nogroup:x:65534:"));
183
184        // Verify /etc/hosts content
185        let hosts = fs::read_to_string(rootfs_path.join("etc/hosts")).unwrap();
186        assert!(hosts.contains("127.0.0.1"));
187        assert!(hosts.contains("localhost"));
188
189        // Verify /etc/resolv.conf content
190        let resolv = fs::read_to_string(rootfs_path.join("etc/resolv.conf")).unwrap();
191        assert!(resolv.contains("nameserver"));
192
193        // Verify /etc/nsswitch.conf content
194        let nsswitch = fs::read_to_string(rootfs_path.join("etc/nsswitch.conf")).unwrap();
195        assert!(nsswitch.contains("passwd: files"));
196        assert!(nsswitch.contains("hosts: files dns"));
197    }
198
199    #[test]
200    fn test_rootfs_builder_with_custom_layout() {
201        let temp_dir = TempDir::new().unwrap();
202        let rootfs_path = temp_dir.path().join("rootfs");
203
204        let custom_layout = GuestLayout {
205            workspace_dir: "/work",
206            tmp_dir: "/tmp",
207            run_dir: "/run",
208        };
209
210        let builder = RootfsBuilder::new(&rootfs_path).with_layout(custom_layout);
211        builder.build().unwrap();
212
213        // Verify custom directories were created
214        assert!(rootfs_path.join("work").exists());
215    }
216
217    #[test]
218    fn test_rootfs_builder_idempotent() {
219        let temp_dir = TempDir::new().unwrap();
220        let rootfs_path = temp_dir.path().join("rootfs");
221
222        let builder = RootfsBuilder::new(&rootfs_path);
223
224        // Build twice should succeed
225        builder.build().unwrap();
226        builder.build().unwrap();
227
228        // Verify everything still exists
229        assert!(rootfs_path.join("etc/passwd").exists());
230        assert!(rootfs_path.join("dev").exists());
231    }
232}