use std::path::{Path, PathBuf};
pub(crate) fn resolve_project_root(explicit: Option<&str>) -> Option<PathBuf> {
if let Some(s) = explicit {
let p = PathBuf::from(s);
if p.is_dir() {
return Some(p);
}
tracing::warn!("project_root '{}' is not a directory, falling back", s);
}
if let Ok(env) = std::env::var("ALC_PROJECT_ROOT") {
if !env.is_empty() {
let p = PathBuf::from(&env);
if p.is_dir() {
return Some(p);
}
}
}
if let Ok(cwd) = std::env::current_dir() {
return walk_up_for_alc_toml(&cwd);
}
None
}
pub(crate) fn walk_up_for_alc_toml(start: &Path) -> Option<PathBuf> {
let mut current = start.to_path_buf();
loop {
if current.join("alc.toml").is_file() {
return Some(current);
}
if !current.pop() {
return None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn with_env_var<F: FnOnce()>(key: &str, val: &str, f: F) {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = LOCK.lock().unwrap_or_else(|p| p.into_inner());
let prev = std::env::var(key).ok();
unsafe {
std::env::set_var(key, val);
}
f();
unsafe {
match prev {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}
}
#[test]
fn resolve_project_root_via_env() {
let tmp = tempfile::tempdir().unwrap();
let tmp_path = tmp.path().to_path_buf();
with_env_var("ALC_PROJECT_ROOT", tmp_path.to_str().unwrap(), || {
let result = resolve_project_root(None);
assert_eq!(result, Some(tmp_path.clone()));
});
}
#[test]
fn resolve_project_root_explicit_wins_over_env() {
let explicit_dir = tempfile::tempdir().unwrap();
let env_dir = tempfile::tempdir().unwrap();
let explicit_path = explicit_dir.path().to_path_buf();
let env_path = env_dir.path().to_path_buf();
with_env_var("ALC_PROJECT_ROOT", env_path.to_str().unwrap(), || {
let result = resolve_project_root(Some(explicit_path.to_str().unwrap()));
assert_eq!(result, Some(explicit_path.clone()));
});
}
#[test]
fn resolve_project_root_walk_up() {
let tmp = tempfile::tempdir().unwrap();
let project_root = tmp.path().to_path_buf();
std::fs::write(project_root.join("alc.toml"), "[packages]\n").unwrap();
let sub = project_root.join("a").join("b");
std::fs::create_dir_all(&sub).unwrap();
let result = walk_up_for_alc_toml(&sub);
assert_eq!(result, Some(project_root));
}
#[test]
fn resolve_project_root_none_when_no_alc_toml() {
let tmp = tempfile::tempdir().unwrap();
let result = walk_up_for_alc_toml(tmp.path());
assert!(result.is_none());
}
#[test]
fn walk_up_stops_at_root_when_no_alc_toml() {
let root = PathBuf::from("/");
let result = walk_up_for_alc_toml(&root);
drop(result);
}
#[test]
fn resolve_project_root_explicit_non_dir_falls_back() {
let result = resolve_project_root(Some("/this/path/does/not/exist_xyz"));
drop(result);
}
}