use crate::diagnostics::{Error, Result};
use std::path::{Path, PathBuf};
use std::env;
use std::fs;
#[derive(Debug, Clone)]
pub struct LibraryPathResolver {
primary_lib_dir: Option<PathBuf>,
search_paths: Vec<PathBuf>,
include_dev_paths: bool,
#[allow(dead_code)]
path_cache: std::collections::HashMap<String, Option<PathBuf>>,
}
#[derive(Debug, Clone)]
pub struct LibraryPathConfig {
pub include_dev_paths: bool,
pub additional_paths: Vec<PathBuf>,
pub enable_caching: bool,
pub lib_dir_override: Option<PathBuf>,
}
impl Default for LibraryPathConfig {
fn default() -> Self {
Self {
include_dev_paths: true,
additional_paths: Vec::new(),
enable_caching: true,
lib_dir_override: None,
}
}
}
impl LibraryPathResolver {
pub fn new() -> Result<Self> {
Self::with_config(LibraryPathConfig::default())
}
pub fn with_config(config: LibraryPathConfig) -> Result<Self> {
let mut resolver = Self {
primary_lib_dir: None,
search_paths: Vec::new(),
include_dev_paths: config.include_dev_paths,
path_cache: std::collections::HashMap::new(),
};
resolver.primary_lib_dir = resolver.determine_primary_lib_dir(config.lib_dir_override.as_deref())?;
resolver.build_search_paths(&config.additional_paths);
Ok(resolver)
}
pub fn primary_lib_dir(&self) -> Option<&Path> {
self.primary_lib_dir.as_deref()
}
pub fn search_paths(&self) -> &[PathBuf] {
&self.search_paths
}
pub fn resolve_lib_subdir(&self, subdir: &str) -> Result<PathBuf> {
if let Some(primary) = &self.primary_lib_dir {
let subdir_path = primary.join(subdir);
if subdir_path.exists() && subdir_path.is_dir() {
return Ok(subdir_path);
}
}
for search_path in &self.search_paths {
let subdir_path = search_path.join(subdir);
if subdir_path.exists() && subdir_path.is_dir() {
return Ok(subdir_path);
}
}
Err(Box::new(Error::io_error(format!(
"Library subdirectory '{}' not found in any search path. \
Primary lib dir: {:?}, Search paths: {:?}. \
Consider setting LAMBDUST_LIB_DIR environment variable.",
subdir, self.primary_lib_dir, self.search_paths
))))
}
pub fn resolve_library_file(&self, subdir: &str, filename: &str) -> Result<PathBuf> {
if let Some(primary) = &self.primary_lib_dir {
let file_path = primary.join(subdir).join(filename);
if file_path.exists() && file_path.is_file() {
return Ok(file_path);
}
}
for search_path in self.search_paths.iter() {
let file_path = search_path.join(subdir).join(filename);
if file_path.exists() && file_path.is_file() {
return Ok(file_path);
}
}
Err(Box::new(Error::io_error(format!(
"Library file '{}/{}' not found in any search path. \
Primary lib dir: {:?}, Search paths: {:?}. \
Consider setting LAMBDUST_LIB_DIR environment variable.",
subdir, filename, self.primary_lib_dir, self.search_paths
))))
}
pub fn find_library_files(&self, subdir: &str, extension: &str) -> Vec<PathBuf> {
let mut files = Vec::new();
if let Some(primary) = &self.primary_lib_dir {
let dir_path = primary.join(subdir);
if let Ok(entries) = fs::read_dir(&dir_path) {
for entry in entries.flatten() {
if let Some(filename) = entry.file_name().to_str() {
if filename.ends_with(extension) {
files.push(entry.path());
}
}
}
}
}
for search_path in &self.search_paths {
let dir_path = search_path.join(subdir);
if let Ok(entries) = fs::read_dir(&dir_path) {
for entry in entries.flatten() {
if let Some(filename) = entry.file_name().to_str() {
if filename.ends_with(extension) {
let entry_path = entry.path();
if !files.iter().any(|f| f.file_name() == entry_path.file_name()) {
files.push(entry_path);
}
}
}
}
}
}
files
}
pub fn validate_library_setup(&self) -> Result<LibraryValidationReport> {
let mut report = LibraryValidationReport {
primary_lib_dir_valid: false,
found_search_paths: Vec::new(),
missing_critical_subdirs: Vec::new(),
found_library_files: std::collections::HashMap::new(),
recommendations: Vec::new(),
};
if let Some(primary) = &self.primary_lib_dir {
report.primary_lib_dir_valid = primary.exists() && primary.is_dir();
if report.primary_lib_dir_valid {
report.found_search_paths.push(primary.clone());
}
}
for path in &self.search_paths {
if path.exists() && path.is_dir() {
report.found_search_paths.push(path.clone());
}
}
let critical_subdirs = ["bootstrap", "r7rs", "modules"];
for &subdir in &critical_subdirs {
if self.resolve_lib_subdir(subdir).is_err() {
report.missing_critical_subdirs.push(subdir.to_string());
}
}
for &subdir in &critical_subdirs {
let files = self.find_library_files(subdir, ".scm");
report.found_library_files.insert(subdir.to_string(), files.len());
}
if !report.primary_lib_dir_valid && env::var("LAMBDUST_LIB_DIR").is_err() {
report.recommendations.push(
"Consider setting LAMBDUST_LIB_DIR environment variable to point to your library directory".to_string()
);
}
if report.found_search_paths.is_empty() {
report.recommendations.push(
"No valid library directories found. Ensure stdlib directory exists in your installation".to_string()
);
}
if !report.missing_critical_subdirs.is_empty() {
report.recommendations.push(format!(
"Missing critical library subdirectories: {}. Check your installation",
report.missing_critical_subdirs.join(", ")
));
}
Ok(report)
}
fn determine_primary_lib_dir(&self, override_path: Option<&Path>) -> Result<Option<PathBuf>> {
if let Some(override_path) = override_path {
return Ok(Some(override_path.to_path_buf()));
}
if let Ok(lib_dir_str) = env::var("LAMBDUST_LIB_DIR") {
let lib_dir = PathBuf::from(lib_dir_str);
if lib_dir.exists() && lib_dir.is_dir() {
return Ok(Some(lib_dir));
} else {
return Err(Box::new(Error::io_error(format!(
"LAMBDUST_LIB_DIR points to invalid directory: {}. \
Directory does not exist or is not accessible.",
lib_dir.display()
))));
}
}
Ok(None)
}
fn build_search_paths(&mut self, additional_paths: &[PathBuf]) {
self.search_paths.clear();
if let Some(primary) = &self.primary_lib_dir {
self.search_paths.push(primary.clone());
}
for path in additional_paths {
if path.exists() && path.is_dir() {
self.search_paths.push(path.clone());
}
}
self.add_auto_detected_paths();
if self.include_dev_paths {
self.add_development_paths();
}
self.search_paths.dedup();
}
fn add_auto_detected_paths(&mut self) {
if let Ok(exe_path) = env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
if let Some(install_root) = exe_dir.parent() {
let lib_path = install_root.join("lib").join("lambdust");
if lib_path.exists() && lib_path.is_dir() {
self.search_paths.push(lib_path);
}
}
let exe_stdlib = exe_dir.join("stdlib");
if exe_stdlib.exists() && exe_stdlib.is_dir() {
self.search_paths.push(exe_stdlib);
}
}
}
#[cfg(unix)]
{
let system_paths = [
"/usr/local/share/lambdust",
"/usr/share/lambdust",
"/opt/lambdust/lib",
];
for &path_str in &system_paths {
let path = PathBuf::from(path_str);
if path.exists() && path.is_dir() {
self.search_paths.push(path);
}
}
}
#[cfg(windows)]
{
if let Ok(program_files) = env::var("ProgramFiles") {
let lambdust_path = PathBuf::from(program_files).join("Lambdust").join("lib");
if lambdust_path.exists() && lambdust_path.is_dir() {
self.search_paths.push(lambdust_path);
}
}
}
}
fn add_development_paths(&mut self) {
if let Ok(current_dir) = env::current_dir() {
let dev_stdlib = current_dir.join("stdlib");
if dev_stdlib.exists() && dev_stdlib.is_dir() {
self.search_paths.push(dev_stdlib);
}
}
if let Some(home_dir) = dirs::home_dir() {
let user_lib = home_dir.join(".lambdust").join("lib");
if user_lib.exists() && user_lib.is_dir() {
self.search_paths.push(user_lib);
}
}
}
}
#[derive(Debug, Clone)]
pub struct LibraryValidationReport {
pub primary_lib_dir_valid: bool,
pub found_search_paths: Vec<PathBuf>,
pub missing_critical_subdirs: Vec<String>,
pub found_library_files: std::collections::HashMap<String, usize>,
pub recommendations: Vec<String>,
}
impl LibraryValidationReport {
pub fn is_usable(&self) -> bool {
!self.found_search_paths.is_empty() && self.missing_critical_subdirs.len() < 2
}
pub fn summary(&self) -> String {
let mut summary = String::new();
summary.push_str("Library validation summary:\n");
summary.push_str(&format!("• Primary lib dir valid: {}\n", self.primary_lib_dir_valid));
summary.push_str(&format!("• Valid search paths found: {}\n", self.found_search_paths.len()));
summary.push_str(&format!("• Missing critical subdirs: {}\n", self.missing_critical_subdirs.len()));
if !self.missing_critical_subdirs.is_empty() {
summary.push_str(&format!(" Missing: {}\n", self.missing_critical_subdirs.join(", ")));
}
for (subdir, count) in &self.found_library_files {
summary.push_str(&format!("• {count} library files in {subdir}\n"));
}
if !self.recommendations.is_empty() {
summary.push_str("\nRecommendations:\n");
for rec in &self.recommendations {
summary.push_str(&format!("• {rec}\n"));
}
}
summary
}
}
#[cfg(not(test))]
mod dirs {
use std::path::PathBuf;
pub fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from)
}
}
#[cfg(test)]
mod dirs {
use std::path::PathBuf;
pub fn home_dir() -> Option<PathBuf> {
Some(PathBuf::from("/tmp/test-home"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_library_path_resolver_creation() {
let resolver = LibraryPathResolver::new();
assert!(resolver.is_ok());
}
#[test]
fn test_with_lambdust_lib_dir_env() {
let temp_dir = TempDir::new().unwrap();
let lib_dir = temp_dir.path().to_path_buf();
fs::create_dir_all(lib_dir.join("r7rs")).unwrap();
fs::create_dir_all(lib_dir.join("bootstrap")).unwrap();
let config = LibraryPathConfig {
lib_dir_override: Some(lib_dir.clone()),
..Default::default()
};
let resolver = LibraryPathResolver::with_config(config).unwrap();
assert_eq!(resolver.primary_lib_dir(), Some(lib_dir.as_path()));
let r7rs_path = resolver.resolve_lib_subdir("r7rs");
assert!(r7rs_path.is_ok());
assert_eq!(r7rs_path.unwrap(), lib_dir.join("r7rs"));
}
#[test]
fn test_library_file_resolution() {
let temp_dir = TempDir::new().unwrap();
let lib_dir = temp_dir.path().to_path_buf();
let r7rs_dir = lib_dir.join("r7rs");
fs::create_dir_all(&r7rs_dir).unwrap();
fs::write(r7rs_dir.join("base.scm"), "(define-library (scheme base) ...)").unwrap();
let config = LibraryPathConfig {
lib_dir_override: Some(lib_dir),
..Default::default()
};
let resolver = LibraryPathResolver::with_config(config).unwrap();
let base_file = resolver.resolve_library_file("r7rs", "base.scm");
assert!(base_file.is_ok());
let missing_file = resolver.resolve_library_file("r7rs", "missing.scm");
assert!(missing_file.is_err());
}
#[test]
fn test_find_library_files() {
let temp_dir = TempDir::new().unwrap();
let lib_dir = temp_dir.path().to_path_buf();
let r7rs_dir = lib_dir.join("r7rs");
fs::create_dir_all(&r7rs_dir).unwrap();
fs::write(r7rs_dir.join("base.scm"), "").unwrap();
fs::write(r7rs_dir.join("char.scm"), "").unwrap();
fs::write(r7rs_dir.join("readme.txt"), "").unwrap();
let config = LibraryPathConfig {
lib_dir_override: Some(lib_dir),
..Default::default()
};
let resolver = LibraryPathResolver::with_config(config).unwrap();
let files = resolver.find_library_files("r7rs", ".scm");
assert_eq!(files.len(), 2);
let filenames: Vec<String> = files.iter()
.filter_map(|p| p.file_name()?.to_str())
.map(|s| s.to_string())
.collect();
assert!(filenames.contains(&"base.scm".to_string()));
assert!(filenames.contains(&"char.scm".to_string()));
assert!(!filenames.contains(&"readme.txt".to_string()));
}
#[test]
fn test_validation_report() {
let temp_dir = TempDir::new().unwrap();
let lib_dir = temp_dir.path().to_path_buf();
fs::create_dir_all(lib_dir.join("r7rs")).unwrap();
fs::write(lib_dir.join("r7rs").join("base.scm"), "").unwrap();
let config = LibraryPathConfig {
lib_dir_override: Some(lib_dir),
..Default::default()
};
let resolver = LibraryPathResolver::with_config(config).unwrap();
let report = resolver.validate_library_setup().unwrap();
assert!(report.primary_lib_dir_valid);
assert_eq!(report.found_search_paths.len(), 1);
assert!(report.missing_critical_subdirs.contains(&"bootstrap".to_string()));
assert_eq!(report.found_library_files.get("r7rs"), Some(&1));
let summary = report.summary();
assert!(summary.contains("Primary lib dir valid: true"));
assert!(summary.contains("Missing: bootstrap"));
}
#[test]
fn test_error_handling_invalid_lib_dir() {
let config = LibraryPathConfig {
lib_dir_override: Some(PathBuf::from("/nonexistent/path")),
..Default::default()
};
let result = LibraryPathResolver::with_config(config);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("LAMBDUST_LIB_DIR points to invalid directory"));
}
#[test]
fn test_search_path_priority() {
let temp_dir = TempDir::new().unwrap();
let primary_dir = temp_dir.path().join("primary");
let secondary_dir = temp_dir.path().join("secondary");
fs::create_dir_all(primary_dir.join("r7rs")).unwrap();
fs::create_dir_all(secondary_dir.join("r7rs")).unwrap();
fs::write(primary_dir.join("r7rs").join("base.scm"), "primary version").unwrap();
fs::write(secondary_dir.join("r7rs").join("base.scm"), "secondary version").unwrap();
let config = LibraryPathConfig {
lib_dir_override: Some(primary_dir.clone()),
additional_paths: vec![secondary_dir],
..Default::default()
};
let resolver = LibraryPathResolver::with_config(config).unwrap();
let resolved_file = resolver.resolve_library_file("r7rs", "base.scm").unwrap();
assert_eq!(resolved_file, primary_dir.join("r7rs").join("base.scm"));
let content = fs::read_to_string(&resolved_file).unwrap();
assert_eq!(content, "primary version");
}
}