use eyre::{Context, Result};
use mbx_cache_core::FileSnapshot;
use std::collections::BTreeMap;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(unix)]
pub(crate) fn snapshot_compiler_inputs<'a>(
paths: impl IntoIterator<Item = &'a Path>,
) -> std::io::Result<BTreeMap<PathBuf, FileSnapshot>> {
let digests = crate::session::file_digest_cache();
paths
.into_iter()
.map(|path| {
let snapshot = FileSnapshot::capture_with_cache(path, digests).map_err(|error| {
std::io::Error::new(
error.kind(),
format!(
"failed to capture compiler input snapshot for {}: {error}",
path.display()
),
)
})?;
let snapshot = snapshot.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Unsupported,
format!(
"filesystem supplied no snapshot for compiler input {}",
path.display()
),
)
})?;
Ok((path.to_path_buf(), snapshot))
})
.collect()
}
#[cfg(not(unix))]
pub(crate) fn snapshot_compiler_inputs<'a>(
_paths: impl IntoIterator<Item = &'a Path>,
) -> std::io::Result<BTreeMap<PathBuf, FileSnapshot>> {
Ok(BTreeMap::new())
}
pub fn workspace_root(start: &Path) -> std::path::PathBuf {
let mut lockfile = None;
let mut manifest = None;
for directory in start.ancestors() {
if directory.join("Cargo.lock").is_file() {
lockfile = Some(directory.to_path_buf());
}
if directory.join("Cargo.toml").is_file() {
manifest = Some(directory.to_path_buf());
}
if is_delta_worktree_root(directory) {
break;
}
}
lockfile.or(manifest).unwrap_or_else(|| start.to_path_buf())
}
pub fn is_checkout_root(directory: &Path) -> bool {
[".git", ".jj", ".hg", ".sl"]
.iter()
.any(|marker| directory.join(marker).exists())
|| is_delta_worktree_root(directory)
}
fn is_delta_worktree_root(directory: &Path) -> bool {
directory
.parent()
.is_some_and(|parent| parent.file_name() == Some(std::ffi::OsStr::new("worktrees")))
&& directory
.parent()
.and_then(Path::parent)
.is_some_and(|parent| parent.file_name() == Some(std::ffi::OsStr::new(".delta")))
}
pub fn write_atomic(path: &Path, contents: &[u8]) -> Result<()> {
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.ok_or_else(|| eyre::eyre!("path has no parent: {}", path.display()))?;
std::fs::create_dir_all(parent)
.wrap_err_with(|| format!("failed to create {}", parent.display()))?;
let mut temporary = tempfile::Builder::new()
.prefix(".mbx-")
.tempfile_in(parent)?;
temporary.write_all(contents)?;
temporary.as_file_mut().sync_all()?;
temporary
.persist(path)
.map_err(|error| error.error)
.wrap_err_with(|| format!("failed atomic write: {}", path.display()))?;
Ok(())
}
pub fn write_advisory(path: &Path, contents: &[u8]) -> Result<()> {
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.ok_or_else(|| eyre::eyre!("path has no parent: {}", path.display()))?;
std::fs::create_dir_all(parent)
.wrap_err_with(|| format!("failed to create {}", parent.display()))?;
let mut temporary = tempfile::Builder::new()
.prefix(".mbx-")
.tempfile_in(parent)?;
temporary.write_all(contents)?;
temporary
.persist(path)
.map_err(|error| error.error)
.wrap_err_with(|| format!("failed atomic write: {}", path.display()))?;
Ok(())
}
pub fn format_duration(duration: Duration) -> String {
if duration < Duration::from_millis(1) {
format!("{duration:.0?}")
} else if duration < Duration::from_secs(1) {
format!("{duration:.1?}")
} else {
format!("{duration:.2?}")
}
}
pub fn format_span(duration: Duration) -> String {
const MINUTE: u64 = 60;
const HOUR: u64 = 60 * MINUTE;
const DAY: u64 = 24 * HOUR;
let seconds = duration.as_secs();
let (amount, unit) = [(DAY, "day"), (HOUR, "hour"), (MINUTE, "minute")]
.into_iter()
.find(|(size, _)| seconds >= *size && seconds.is_multiple_of(*size))
.map_or((seconds, "second"), |(size, unit)| (seconds / size, unit));
if amount == 1 {
format!("1 {unit}")
} else {
format!("{amount} {unit}s")
}
}
pub fn parse_duration(value: &str) -> Result<Duration> {
let value = value.trim();
let (digits, multiplier) = if let Some(digits) = value.strip_suffix("ms") {
(digits, 1)
} else if let Some(digits) = value.strip_suffix('s') {
(digits, 1_000)
} else if let Some(digits) = value.strip_suffix('m') {
(digits, 60 * 1_000)
} else if let Some(digits) = value.strip_suffix('h') {
(digits, 60 * 60 * 1_000)
} else if let Some(digits) = value.strip_suffix('d') {
(digits, 24 * 60 * 60 * 1_000)
} else {
(value, 1_000)
};
let amount: u64 = digits
.trim()
.parse()
.wrap_err_with(|| format!("invalid duration: {value}"))?;
Ok(Duration::from_millis(amount.saturating_mul(multiplier)))
}
pub fn duration_ns(duration: Duration) -> u64 {
duration.as_nanos().try_into().unwrap_or(u64::MAX)
}
pub fn disk_total_bytes(path: &Path) -> Option<u64> {
let existing = path.ancestors().find(|ancestor| ancestor.exists())?;
disk_total_bytes_at(existing)
}
#[cfg(all(unix, target_vendor = "apple"))]
fn disk_total_bytes_at(path: &Path) -> Option<u64> {
use std::os::unix::ffi::OsStrExt as _;
let path = std::ffi::CString::new(path.as_os_str().as_bytes()).ok()?;
let stats = unsafe {
let mut stats = std::mem::zeroed::<libc::statfs>();
if libc::statfs(path.as_ptr(), &mut stats) != 0 {
return None;
}
stats
};
u64::from(stats.f_bsize)
.checked_mul(stats.f_blocks)
.filter(|total| *total > 0)
}
#[cfg(all(unix, not(target_vendor = "apple")))]
fn disk_total_bytes_at(path: &Path) -> Option<u64> {
use std::os::unix::ffi::OsStrExt as _;
let path = std::ffi::CString::new(path.as_os_str().as_bytes()).ok()?;
let stats = unsafe {
let mut stats = std::mem::zeroed::<libc::statvfs>();
if libc::statvfs(path.as_ptr(), &mut stats) != 0 {
return None;
}
stats
};
#[allow(clippy::useless_conversion)]
let block_size = u64::try_from(stats.f_frsize).ok()?;
#[allow(clippy::useless_conversion)]
let blocks = u64::try_from(stats.f_blocks).ok()?;
block_size.checked_mul(blocks).filter(|total| *total > 0)
}
#[cfg(windows)]
fn disk_total_bytes_at(path: &Path) -> Option<u64> {
use std::os::windows::ffi::OsStrExt as _;
use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
wide.push(0);
let mut total = 0_u64;
let ok = unsafe {
GetDiskFreeSpaceExW(
wide.as_ptr(),
std::ptr::null_mut(),
&mut total,
std::ptr::null_mut(),
)
};
if ok == 0 {
None
} else {
Some(total).filter(|total| *total > 0)
}
}
#[cfg(target_vendor = "apple")]
pub fn memory_total_bytes() -> Option<u64> {
let mut mib = [libc::CTL_HW, libc::HW_MEMSIZE];
let mut total = 0_u64;
let mut length = size_of::<u64>();
let ok = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
mib.len() as libc::c_uint,
(&raw mut total).cast(),
&raw mut length,
std::ptr::null_mut(),
0,
)
};
(ok == 0).then_some(total).filter(|total| *total > 0)
}
#[cfg(target_os = "linux")]
pub fn memory_total_bytes() -> Option<u64> {
let info = unsafe {
let mut info = std::mem::zeroed::<libc::sysinfo>();
if libc::sysinfo(&mut info) != 0 {
return None;
}
info
};
#[allow(clippy::useless_conversion)]
let unit = u64::from(info.mem_unit);
#[allow(clippy::useless_conversion)]
let total = u64::try_from(info.totalram).ok()?;
let physical = total.checked_mul(unit).filter(|total| *total > 0)?;
Some(match crate::cgroup::memory().limit {
Some(limit) => physical.min(limit),
None => physical,
})
}
#[cfg(any(target_os = "linux", test))]
pub(crate) fn parse_cgroup_stat(contents: &str, field: &str) -> Option<u64> {
contents.lines().find_map(|line| {
let (name, value) = line.split_once(' ')?;
(name == field).then(|| value.trim().parse().ok())?
})
}
#[cfg(any(target_os = "linux", test))]
pub(crate) fn read_cgroup_text(contents: &str) -> Option<u64> {
let value = contents.trim();
if value == "max" {
return None;
}
const NO_LIMIT: u64 = 1 << 50;
value
.parse::<u64>()
.ok()
.filter(|bytes| *bytes > 0 && *bytes < NO_LIMIT)
}
#[cfg(windows)]
pub fn memory_total_bytes() -> Option<u64> {
use windows_sys::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX};
let status = unsafe {
let mut status = std::mem::zeroed::<MEMORYSTATUSEX>();
status.dwLength = size_of::<MEMORYSTATUSEX>() as u32;
if GlobalMemoryStatusEx(&mut status) == 0 {
return None;
}
status
};
Some(status.ullTotalPhys).filter(|total| *total > 0)
}
#[cfg(not(any(target_vendor = "apple", target_os = "linux", windows)))]
pub fn memory_total_bytes() -> Option<u64> {
None
}
#[cfg(target_os = "linux")]
pub fn memory_available_bytes() -> Option<u64> {
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
let line = meminfo
.lines()
.find_map(|line| line.strip_prefix("MemAvailable:"))?;
let kibibytes = line
.trim()
.trim_end_matches("kB")
.trim()
.parse::<u64>()
.ok()?;
let host = kibibytes.checked_mul(1024)?;
let available = crate::cgroup::memory()
.headroom
.map_or(host, |free| host.min(free));
Some(available)
}
#[allow(deprecated)]
#[cfg(target_vendor = "apple")]
pub fn memory_available_bytes() -> Option<u64> {
let mut count = libc::HOST_VM_INFO64_COUNT;
let stats = unsafe {
let mut stats = std::mem::zeroed::<libc::vm_statistics64>();
if libc::host_statistics64(
libc::mach_host_self(),
libc::HOST_VM_INFO64,
(&raw mut stats).cast(),
&mut count,
) != libc::KERN_SUCCESS
{
return None;
}
stats
};
let page_size = unsafe { libc::vm_page_size } as u64;
let pages = u64::from(stats.free_count)
.checked_add(u64::from(stats.inactive_count))?
.checked_add(u64::from(stats.purgeable_count))?;
pages.checked_mul(page_size).filter(|bytes| *bytes > 0)
}
#[cfg(not(any(target_vendor = "apple", target_os = "linux")))]
pub fn memory_available_bytes() -> Option<u64> {
None
}
pub fn reflinks_work(source_dir: &Path, destination_dir: &Path) -> bool {
fn probe(source_dir: &Path, destination_dir: &Path) -> Option<()> {
std::fs::create_dir_all(source_dir).ok()?;
let anchor = destination_dir
.ancestors()
.find(|ancestor| ancestor.exists())?;
let source_temp = tempfile::tempdir_in(source_dir).ok()?;
let destination_temp = tempfile::tempdir_in(anchor).ok()?;
let source = source_temp.path().join("source");
std::fs::write(&source, b"mbx reflink probe").ok()?;
reflink_copy::reflink(&source, destination_temp.path().join("destination")).ok()
}
probe(source_dir, destination_dir).is_some()
}
pub fn random_string(length: usize) -> String {
use rand::RngExt as _;
use rand::distr::Alphanumeric;
rand::rng()
.sample_iter(&Alphanumeric)
.take(length)
.map(char::from)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn compiler_input_snapshot_fails_closed() {
let directory = tempfile::tempdir().unwrap();
let missing = directory.path().join("missing-input");
let error = snapshot_compiler_inputs([missing.as_path()]).unwrap_err();
assert!(
error
.to_string()
.contains("failed to capture compiler input snapshot")
);
}
#[test]
fn cgroup_usage_discounts_the_page_cache() {
let stat = "anon 1000\nfile 8000\nkernel 12\ninactive_file 7000\nslab 40\n";
assert_eq!(parse_cgroup_stat(stat, "inactive_file"), Some(7000));
assert_eq!(parse_cgroup_stat(stat, "anon"), Some(1000));
assert_eq!(parse_cgroup_stat(stat, "file"), Some(8000));
assert_eq!(parse_cgroup_stat(stat, "absent"), None);
assert_eq!(parse_cgroup_stat("malformed\n", "anon"), None);
}
#[test]
fn cgroup_limits_are_read_and_no_limit_is_recognized() {
assert_eq!(
read_cgroup_text("4294967296\n"),
Some(4 * 1024 * 1024 * 1024)
);
assert_eq!(read_cgroup_text("max\n"), None);
assert_eq!(read_cgroup_text("9223372036854771712"), None);
assert_eq!(read_cgroup_text("0"), None);
assert_eq!(read_cgroup_text("not a number"), None);
assert_eq!(read_cgroup_text(""), None);
}
#[test]
fn writes_files_atomically() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("nested").join("report.json");
write_atomic(&path, b"{}\n").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"{}\n");
write_atomic(&path, b"[]\n").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"[]\n");
}
#[test]
fn random_strings_have_the_requested_length_and_differ() {
let first = random_string(12);
assert_eq!(first.chars().count(), 12);
assert!(first.chars().all(|character| character.is_alphanumeric()));
assert_ne!(first, random_string(12));
assert!(random_string(0).is_empty());
}
#[test]
fn measures_the_disk_holding_a_path() {
let directory = tempfile::tempdir().unwrap();
let total = disk_total_bytes(directory.path()).expect("the test disk has a size");
assert!(total > 0);
let missing = directory.path().join("not").join("created").join("yet");
assert_eq!(disk_total_bytes(&missing), Some(total));
}
#[test]
fn the_reflink_probe_anchors_a_destination_nobody_created_yet() {
let directory = tempfile::tempdir().unwrap();
let store = directory.path().join("cache");
let unmade = directory.path().join("checkout").join("target");
assert_eq!(
reflinks_work(&store, &unmade),
reflinks_work(&store, directory.path()),
"a missing destination answers as its filesystem, not as a failure"
);
assert!(!unmade.exists(), "the probe must not create the directory");
}
#[test]
fn formats_spans_in_the_units_a_policy_is_written_in() {
assert_eq!(format_span(Duration::from_secs(30 * 86_400)), "30 days");
assert_eq!(format_span(Duration::from_secs(86_400)), "1 day");
assert_eq!(format_span(Duration::from_secs(36 * 3_600)), "36 hours");
assert_eq!(format_span(Duration::from_secs(90 * 60)), "90 minutes");
assert_eq!(format_span(Duration::from_secs(45)), "45 seconds");
assert_eq!(format_span(Duration::from_secs(7_200)), "2 hours");
assert_eq!(format_span(Duration::from_secs(60)), "1 minute");
assert_eq!(format_span(Duration::from_secs(90)), "90 seconds");
assert_eq!(format_span(Duration::from_secs(5)), "5 seconds");
}
#[test]
fn parses_durations_with_and_without_units() {
assert_eq!(parse_duration("30").unwrap(), Duration::from_secs(30));
assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
assert_eq!(parse_duration("250ms").unwrap(), Duration::from_millis(250));
assert_eq!(parse_duration("10m").unwrap(), Duration::from_secs(600));
assert_eq!(parse_duration("2h").unwrap(), Duration::from_secs(7_200));
assert!(parse_duration("soon").is_err());
}
}