use std::path::{Path, PathBuf};
pub(crate) fn target_dir_permission_blocked(stderr_lines: &[String], target_dir: &str) -> bool {
stderr_lines
.iter()
.any(|line| is_permission_block_for_target(line, target_dir))
}
pub(crate) fn fallback_target_root() -> PathBuf {
#[cfg(test)]
if let Ok(val) = std::env::var("AID_TEST_FALLBACK_TARGET_ROOT") {
return PathBuf::from(val);
}
std::env::temp_dir().join("aid-build-target")
}
pub(crate) fn sandbox_fallback_target_dir() -> Option<String> {
let cwd = std::env::current_dir().ok()?;
sandbox_fallback_target_dir_at(&cwd)
}
pub(crate) fn sandbox_fallback_target_dir_at(cwd: &Path) -> Option<String> {
let key = cwd_key(cwd);
let dir = fallback_target_root().join(key);
Some(dir.to_string_lossy().into_owned())
}
pub(crate) fn cwd_key(cwd: &std::path::Path) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
cwd.hash(&mut hasher);
let name = cwd
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("project");
format!("{name}-{:x}", hasher.finish())
}
pub(crate) fn fallback_digest_note(from: &str, to: &str) -> String {
format!("note: CARGO_TARGET_DIR unwritable; fell back from {from} to {to}")
}
pub(crate) fn should_retry_with_fallback(
success: bool,
stderr_lines: &[String],
target_dir: Option<&str>,
) -> Option<String> {
should_retry_with_fallback_at(success, stderr_lines, target_dir, None)
}
pub(crate) fn should_retry_with_fallback_at(
success: bool,
stderr_lines: &[String],
target_dir: Option<&str>,
cwd: Option<&Path>,
) -> Option<String> {
if success {
return None;
}
let chosen = target_dir?;
if !target_dir_permission_blocked(stderr_lines, chosen) {
return None;
}
let fallback = cwd
.and_then(sandbox_fallback_target_dir_at)
.or_else(sandbox_fallback_target_dir)?;
if Path::new(&fallback) == Path::new(chosen) {
return None;
}
Some(fallback)
}
fn is_permission_block_for_target(line: &str, target_dir: &str) -> bool {
let trimmed = line.trim();
if !is_permission_os_error(trimmed) {
return false;
}
let Some(path) = extract_at_path(trimmed) else {
return false;
};
path_matches_target(path, target_dir)
}
fn is_permission_os_error(line: &str) -> bool {
is_permission_os_error_text(line)
}
pub(crate) fn is_permission_os_error_text(line: &str) -> bool {
line.contains("Operation not permitted (os error 1)")
|| line.contains("Permission denied (os error 13)")
}
fn extract_at_path(line: &str) -> Option<&str> {
if let Some((_, rest)) = line.split_once("at path \"") {
return rest.split_once('"').map(|(path, _)| path);
}
if let Some((_, rest)) = line.split_once(" to `") {
return rest.split_once('`').map(|(path, _)| path);
}
None
}
fn path_matches_target(error_path: &str, target_dir: &str) -> bool {
let target = target_dir.trim_end_matches('/');
error_path == target || error_path.starts_with(target)
}
#[cfg(test)]
#[path = "build_fallback_tests.rs"]
mod tests;