mod builder;
mod layout;
pub(crate) mod overlay;
mod provider;
pub use builder::RootfsBuilder;
pub use layout::{GuestLayout, GUEST_WORKDIR};
pub use provider::{default_provider, CopyProvider, OverlayProvider, RootfsProvider};
use std::path::Path;
pub struct AttachedRootfs {
path: std::path::PathBuf,
detach_on_drop: bool,
}
impl AttachedRootfs {
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for AttachedRootfs {
fn drop(&mut self) {
if self.detach_on_drop {
unmount_box_rootfs(&self.path);
}
}
}
pub fn attach_persistent_rootfs(
box_dir: &Path,
) -> a3s_box_core::error::Result<Option<AttachedRootfs>> {
#[cfg(target_os = "macos")]
{
let image = box_dir.join("rootfs-apfs-v2.sparseimage");
if !image.is_file() {
return Ok(None);
}
let rootfs = box_dir.join("rootfs");
let was_mounted = is_mountpoint(&rootfs);
let path = provider::CaseSensitiveApfsProvider.prepare_empty(box_dir)?;
Ok(Some(AttachedRootfs {
path,
detach_on_drop: !was_mounted,
}))
}
#[cfg(not(target_os = "macos"))]
{
let _ = box_dir;
Ok(None)
}
}
pub fn unmount_box_overlay(merged: &Path) {
for _ in 0..8 {
if !is_mountpoint(merged) {
break;
}
if overlay::overlay_unmount(merged).is_err() {
break;
}
}
}
#[cfg(unix)]
pub(crate) fn is_mountpoint(path: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
match (std::fs::metadata(path), std::fs::metadata(path.join(".."))) {
(Ok(here), Ok(parent)) => here.dev() != parent.dev(),
_ => false,
}
}
#[cfg(not(unix))]
pub(crate) fn is_mountpoint(_path: &Path) -> bool {
false
}
pub fn unmount_box_rootfs(rootfs: &Path) {
#[cfg(target_os = "macos")]
{
let mountpoint = if rootfs.file_name().is_some_and(|name| name == ".a3s-rootfs") {
rootfs.parent().unwrap_or(rootfs)
} else {
rootfs
};
if !is_mountpoint(mountpoint) {
return;
}
match std::process::Command::new("hdiutil")
.arg("detach")
.arg("-quiet")
.arg(mountpoint)
.status()
{
Ok(status) if status.success() => {}
Ok(status) => tracing::warn!(
path = %mountpoint.display(),
?status,
"Failed to detach case-sensitive rootfs image"
),
Err(error) => tracing::warn!(
path = %mountpoint.display(),
%error,
"Failed to run hdiutil detach"
),
}
}
#[cfg(not(target_os = "macos"))]
let _ = rootfs;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_path_is_not_mountpoint() {
let temp = tempfile::tempdir().unwrap();
let missing = temp.path().join("missing");
assert!(!is_mountpoint(&missing));
}
#[test]
fn unmount_overlay_noops_for_non_mountpoint() {
let temp = tempfile::tempdir().unwrap();
let merged = temp.path().join("merged");
std::fs::create_dir(&merged).unwrap();
unmount_box_overlay(&merged);
assert!(merged.exists());
}
}