a3s_box_runtime/rootfs/
mod.rs1mod builder;
11mod layout;
12pub(crate) mod overlay;
13mod provider;
14
15pub use builder::RootfsBuilder;
16pub use layout::{GuestLayout, GUEST_WORKDIR};
17pub use provider::{default_provider, CopyProvider, OverlayProvider, RootfsProvider};
18
19use std::path::Path;
20
21pub struct AttachedRootfs {
26 path: std::path::PathBuf,
27 detach_on_drop: bool,
28}
29
30impl AttachedRootfs {
31 pub fn path(&self) -> &Path {
32 &self.path
33 }
34}
35
36impl Drop for AttachedRootfs {
37 fn drop(&mut self) {
38 if self.detach_on_drop {
39 unmount_box_rootfs(&self.path);
40 }
41 }
42}
43
44pub fn attach_persistent_rootfs(
50 box_dir: &Path,
51) -> a3s_box_core::error::Result<Option<AttachedRootfs>> {
52 #[cfg(target_os = "macos")]
53 {
54 let image = box_dir.join("rootfs-apfs-v2.sparseimage");
55 if !image.is_file() {
56 return Ok(None);
57 }
58 let rootfs = box_dir.join("rootfs");
59 let was_mounted = is_mountpoint(&rootfs);
60 let path = provider::CaseSensitiveApfsProvider.prepare_empty(box_dir)?;
61 Ok(Some(AttachedRootfs {
62 path,
63 detach_on_drop: !was_mounted,
64 }))
65 }
66
67 #[cfg(not(target_os = "macos"))]
68 {
69 let _ = box_dir;
70 Ok(None)
71 }
72}
73
74pub fn unmount_box_overlay(merged: &Path) {
82 for _ in 0..8 {
83 if !is_mountpoint(merged) {
84 break;
85 }
86 if overlay::overlay_unmount(merged).is_err() {
87 break;
88 }
89 }
90}
91
92#[cfg(unix)]
94pub(crate) fn is_mountpoint(path: &Path) -> bool {
95 use std::os::unix::fs::MetadataExt;
96 match (std::fs::metadata(path), std::fs::metadata(path.join(".."))) {
97 (Ok(here), Ok(parent)) => here.dev() != parent.dev(),
98 _ => false,
99 }
100}
101
102#[cfg(not(unix))]
103pub(crate) fn is_mountpoint(_path: &Path) -> bool {
104 false
105}
106
107pub fn unmount_box_rootfs(rootfs: &Path) {
109 #[cfg(target_os = "macos")]
110 {
111 let mountpoint = if rootfs.file_name().is_some_and(|name| name == ".a3s-rootfs") {
115 rootfs.parent().unwrap_or(rootfs)
116 } else {
117 rootfs
118 };
119 if !is_mountpoint(mountpoint) {
120 return;
121 }
122 match std::process::Command::new("hdiutil")
123 .arg("detach")
124 .arg("-quiet")
125 .arg(mountpoint)
126 .status()
127 {
128 Ok(status) if status.success() => {}
129 Ok(status) => tracing::warn!(
130 path = %mountpoint.display(),
131 ?status,
132 "Failed to detach case-sensitive rootfs image"
133 ),
134 Err(error) => tracing::warn!(
135 path = %mountpoint.display(),
136 %error,
137 "Failed to run hdiutil detach"
138 ),
139 }
140 }
141
142 #[cfg(not(target_os = "macos"))]
143 let _ = rootfs;
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn missing_path_is_not_mountpoint() {
152 let temp = tempfile::tempdir().unwrap();
153 let missing = temp.path().join("missing");
154
155 assert!(!is_mountpoint(&missing));
156 }
157
158 #[test]
159 fn unmount_overlay_noops_for_non_mountpoint() {
160 let temp = tempfile::tempdir().unwrap();
161 let merged = temp.path().join("merged");
162 std::fs::create_dir(&merged).unwrap();
163
164 unmount_box_overlay(&merged);
165
166 assert!(merged.exists());
167 }
168}