use std::path::{Path, PathBuf};
use a3s_box_core::error::{BoxError, Result};
use super::provider::RootfsProvider;
pub(crate) struct CaseSensitiveApfsProvider;
impl CaseSensitiveApfsProvider {
const IMAGE_STEM: &'static str = "rootfs-apfs-v2";
pub(super) const IMAGE_NAME: &'static str = "rootfs-apfs-v2.sparseimage";
const DATA_DIR: &'static str = ".a3s-rootfs";
pub(super) fn image_path(box_dir: &Path) -> PathBuf {
box_dir.join(Self::IMAGE_NAME)
}
fn clone_image(source: &Path, destination: &Path) -> Result<()> {
let output = std::process::Command::new("cp")
.arg("-c")
.arg(source)
.arg(destination)
.output()
.map_err(|error| {
BoxError::BuildError(format!("Failed to start APFS clone: {error}"))
})?;
if !output.status.success() {
return Err(BoxError::BuildError(format!(
"Failed to clone cached APFS rootfs {}: {}",
source.display(),
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(())
}
fn mount(&self, box_dir: &Path) -> Result<PathBuf> {
use std::process::Command;
std::fs::create_dir_all(box_dir).map_err(|error| {
BoxError::BuildError(format!(
"Failed to create box directory {}: {error}",
box_dir.display()
))
})?;
let rootfs = box_dir.join("rootfs");
std::fs::create_dir_all(&rootfs).map_err(|error| {
BoxError::BuildError(format!(
"Failed to create APFS mountpoint {}: {error}",
rootfs.display()
))
})?;
if super::is_mountpoint(&rootfs) {
return Self::data_dir(&rootfs);
}
let image = Self::image_path(box_dir);
if !image.exists() {
let stem = box_dir.join(Self::IMAGE_STEM);
let output = Command::new("hdiutil")
.args([
"create",
"-quiet",
"-size",
"64g",
"-type",
"SPARSE",
"-fs",
"Case-sensitive APFS",
"-volname",
"A3SRootfs",
])
.arg(&stem)
.output()
.map_err(|error| {
BoxError::BuildError(format!("Failed to start hdiutil create: {error}"))
})?;
if !output.status.success() {
return Err(BoxError::BuildError(format!(
"Failed to create case-sensitive APFS rootfs image: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
}
let output = Command::new("hdiutil")
.args([
"attach",
"-quiet",
"-nobrowse",
"-owners",
"on",
"-mountpoint",
])
.arg(&rootfs)
.arg(&image)
.output()
.map_err(|error| {
BoxError::BuildError(format!("Failed to start hdiutil attach: {error}"))
})?;
if !output.status.success() {
return Err(BoxError::BuildError(format!(
"Failed to mount case-sensitive APFS rootfs image {}: {}",
image.display(),
String::from_utf8_lossy(&output.stderr).trim()
)));
}
if !super::is_mountpoint(&rootfs) {
return Err(BoxError::BuildError(format!(
"hdiutil did not mount the rootfs image at {}",
rootfs.display()
)));
}
Self::data_dir(&rootfs)
}
fn data_dir(mountpoint: &Path) -> Result<PathBuf> {
let data = mountpoint.join(Self::DATA_DIR);
std::fs::create_dir_all(&data).map_err(|error| {
BoxError::BuildError(format!(
"Failed to create APFS rootfs data directory {}: {error}",
data.display()
))
})?;
Ok(data)
}
}
impl RootfsProvider for CaseSensitiveApfsProvider {
fn prepare(&self, box_dir: &Path, cache_dir: &Path) -> Result<PathBuf> {
let image = Self::image_path(box_dir);
if cache_dir.is_file() && !image.exists() {
std::fs::create_dir_all(box_dir).map_err(BoxError::IoError)?;
Self::clone_image(cache_dir, &image)?;
}
let rootfs = self.mount(box_dir)?;
if cache_dir.is_file() {
return Ok(rootfs);
}
if std::fs::read_dir(&rootfs)
.map_err(|error| BoxError::BuildError(error.to_string()))?
.next()
.is_none()
{
crate::cache::layer_cache::copy_dir_recursive(cache_dir, &rootfs)?;
} else {
tracing::info!(path = %rootfs.display(), "Reusing persistent APFS rootfs");
}
Ok(rootfs)
}
fn prepare_empty(&self, box_dir: &Path) -> Result<PathBuf> {
self.mount(box_dir)
}
fn cleanup(&self, box_dir: &Path, persistent: bool) -> Result<()> {
super::unmount_box_rootfs(&box_dir.join("rootfs"));
if !persistent {
let image = Self::image_path(box_dir);
if image.exists() {
std::fs::remove_file(&image).map_err(|error| {
BoxError::BuildError(format!(
"Failed to remove rootfs image {}: {error}",
image.display()
))
})?;
}
}
Ok(())
}
fn name(&self) -> &'static str {
"case-sensitive-apfs"
}
}