diff --git a/src/package.rs b/src/package.rs index 1b01469..8972142 100644 --- a/src/package.rs +++ b/src/package.rs @@ -55,25 +55,38 @@ pub struct PackageVariant { } impl Package { - /// Load a package from a directory + /// Load a package from a directory containing package.yaml pub fn load(path: &Path) -> Result { let package_file = path.join("package.yaml"); - + if !package_file.exists() { anyhow::bail!("Package file not found: {:?}", package_file); } - - let content = std::fs::read_to_string(&package_file) - .with_context(|| format!("Failed to read package: {:?}", package_file))?; - + + Self::load_from_file(&package_file, Some(path)) + } + + /// Load a package from a YAML file directly. + /// If `root` is None, the parent directory of the file is used as the package root. + pub fn load_from_file(file_path: &Path, root: Option<&Path>) -> Result { + if !file_path.exists() { + anyhow::bail!("Package file not found: {:?}", file_path); + } + + let content = std::fs::read_to_string(file_path) + .with_context(|| format!("Failed to read package: {:?}", file_path))?; + let mut package: Package = serde_yaml::from_str(&content) - .with_context(|| format!("Failed to parse package: {:?}", package_file))?; - - package.root = path.to_path_buf(); - + .with_context(|| format!("Failed to parse package: {:?}", file_path))?; + + package.root = root + .map(|p| p.to_path_buf()) + .or_else(|| file_path.parent().map(|p| p.to_path_buf())) + .unwrap_or_default(); + // Apply variant for current platform package.apply_current_variant(); - + Ok(package) } @@ -107,31 +120,38 @@ impl Package { } } - /// Expand environment variables in a value + /// Expand environment variables and tilde in a value pub fn expand_env_value(&self, value: &str, env: &HashMap) -> String { let mut result = value.to_string(); - + // Replace ${PACKAGE_ROOT} with actual path result = result.replace("${PACKAGE_ROOT}", &self.root.to_string_lossy()); - + // Replace ${VERSION} with package version result = result.replace("${VERSION}", &self.version); - + // Replace ${NAME} with package name result = result.replace("${NAME}", &self.name); - + // Replace other ${VAR} references for (key, val) in env { result = result.replace(&format!("${{{}}}", key), val); } - + // Replace remaining ${VAR} with current environment let re = regex::Regex::new(r"\$\{([^}]+)\}").unwrap(); result = re.replace_all(&result, |caps: ®ex::Captures| { let var = &caps[1]; std::env::var(var).unwrap_or_default() }).to_string(); - + + // Expand ~ to home directory + if result.starts_with("~/") { + if let Ok(home) = std::env::var("HOME") { + result = format!("{}{}", home, &result[1..]); + } + } + result } diff --git a/src/resolver.rs b/src/resolver.rs index fecec36..7334a72 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -54,64 +54,77 @@ impl Resolver { Ok(resolver) } - /// Scan package paths and load all packages + /// Scan package paths and load all packages. + /// + /// Supports two layouts: + /// + /// 1. **Flat files** — YAML files directly in the package path: + /// `{package_path}/maya-2026.yaml` + /// The file must contain `name` and `version` fields. + /// + /// 2. **Nested directories** — the original `{name}/{version}/package.yaml` layout: + /// `{package_path}/maya/2026/package.yaml` fn scan_packages(&mut self) -> Result<()> { for base_path in self.config.all_package_paths() { debug!("Scanning packages in {:?}", base_path); - + if !base_path.exists() { continue; } - - // Iterate over package directories + for entry in std::fs::read_dir(&base_path)? { let entry = entry?; - let pkg_dir = entry.path(); - - if !pkg_dir.is_dir() { - continue; - } - - let pkg_name = pkg_dir.file_name() - .and_then(|n| n.to_str()) - .map(|s| s.to_string()); - - let pkg_name = match pkg_name { - Some(n) => n, - None => continue, - }; - - // Iterate over versions - for version_entry in std::fs::read_dir(&pkg_dir)? { - let version_entry = version_entry?; - let version_dir = version_entry.path(); - - if !version_dir.is_dir() { - continue; - } - - // Check for package.yaml - let package_file = version_dir.join("package.yaml"); - if !package_file.exists() { - continue; + let path = entry.path(); + + if path.is_file() { + // Flat file: load any .yaml / .yml file directly + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + if ext == "yaml" || ext == "yml" { + match Package::load_from_file(&path, None) { + Ok(pkg) => { + debug!("Loaded package (flat): {}-{}", pkg.name, pkg.version); + self.package_cache + .entry(pkg.name.clone()) + .or_default() + .insert(pkg.version.clone(), pkg); + } + Err(e) => { + warn!("Failed to load package {:?}: {}", path, e); + } + } } - - match Package::load(&version_dir) { - Ok(pkg) => { - debug!("Loaded package: {}-{}", pkg.name, pkg.version); - self.package_cache - .entry(pkg.name.clone()) - .or_default() - .insert(pkg.version.clone(), pkg); + } else if path.is_dir() { + // Nested directory: {name}/{version}/package.yaml + for version_entry in std::fs::read_dir(&path)? { + let version_entry = version_entry?; + let version_dir = version_entry.path(); + + if !version_dir.is_dir() { + continue; } - Err(e) => { - warn!("Failed to load package {:?}: {}", version_dir, e); + + let package_file = version_dir.join("package.yaml"); + if !package_file.exists() { + continue; + } + + match Package::load(&version_dir) { + Ok(pkg) => { + debug!("Loaded package (nested): {}-{}", pkg.name, pkg.version); + self.package_cache + .entry(pkg.name.clone()) + .or_default() + .insert(pkg.version.clone(), pkg); + } + Err(e) => { + warn!("Failed to load package {:?}: {}", version_dir, e); + } } } } } } - + info!("Loaded {} packages", self.package_cache.len()); Ok(()) }