use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Deserialize)]
pub struct Manifest {
pub package: Option<PackageMeta>,
#[serde(default)]
pub dependencies: HashMap<String, Dependency>,
}
#[derive(Debug, Deserialize)]
pub struct PackageMeta {
pub name: String,
#[serde(default)]
pub version: String,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum Dependency {
Path { path: String },
Git { git: String },
}
impl Manifest {
pub fn load(toml_path: &Path) -> Result<Self, String> {
let src = std::fs::read_to_string(toml_path)
.map_err(|e| format!("reading {}: {e}", toml_path.display()))?;
toml::from_str(&src)
.map_err(|e| format!("parsing {}: {e}", toml_path.display()))
}
}
pub fn find_manifest(start: &Path) -> Option<(PathBuf, PathBuf)> {
let mut dir = if start.is_dir() {
start.to_path_buf()
} else {
start.parent()?.to_path_buf()
};
loop {
let candidate = dir.join("lex.toml");
if candidate.exists() {
return Some((candidate, dir));
}
match dir.parent() {
Some(p) if p != dir => dir = p.to_path_buf(),
_ => return None,
}
}
}
pub fn resolve_package_import(
importer: &Path,
pkg_name: &str,
module_path: &str,
) -> Result<PathBuf, PackageError> {
let (toml_path, toml_dir) = find_manifest(importer).ok_or_else(|| {
PackageError::NoManifest {
reference: format!("{pkg_name}/{module_path}"),
searched_from: importer.display().to_string(),
}
})?;
let manifest = Manifest::load(&toml_path)
.map_err(|e| PackageError::ManifestParse { path: toml_path.display().to_string(), detail: e })?;
let dep = manifest.dependencies.get(pkg_name).ok_or_else(|| {
PackageError::UnknownPackage {
name: pkg_name.to_string(),
manifest: toml_path.display().to_string(),
}
})?;
let pkg_root = match dep {
Dependency::Path { path } => {
let raw = toml_dir.join(path);
raw.canonicalize().map_err(|e| PackageError::Io {
path: raw.display().to_string(),
detail: e.to_string(),
})?
}
Dependency::Git { git } => git_ensure_cached(pkg_name, git)?,
};
find_module_file(&pkg_root, module_path).ok_or_else(|| PackageError::ModuleNotFound {
pkg: pkg_name.to_string(),
module: module_path.to_string(),
pkg_root: pkg_root.display().to_string(),
})
}
fn find_module_file(pkg_root: &Path, module_path: &str) -> Option<PathBuf> {
let rel = PathBuf::from(module_path).with_extension("lex");
let in_src = pkg_root.join("src").join(&rel);
if in_src.exists() {
return Some(in_src);
}
let at_root = pkg_root.join(&rel);
if at_root.exists() {
return Some(at_root);
}
None
}
fn git_ensure_cached(pkg_name: &str, url: &str) -> Result<PathBuf, PackageError> {
let cache_root = packages_cache_dir()?;
let pkg_dir = cache_root.join(pkg_name);
if pkg_dir.exists() {
return Ok(pkg_dir);
}
std::fs::create_dir_all(&cache_root).map_err(|e| PackageError::Io {
path: cache_root.display().to_string(),
detail: e.to_string(),
})?;
let status = std::process::Command::new("git")
.args(["clone", "--depth=1", url, pkg_dir.to_str().unwrap_or(pkg_name)])
.status()
.map_err(|e| PackageError::GitFailed {
url: url.to_string(),
detail: format!("could not run `git`: {e}"),
})?;
if !status.success() {
return Err(PackageError::GitFailed {
url: url.to_string(),
detail: format!("`git clone` exited with {status}"),
});
}
pkg_dir.canonicalize().map_err(|e| PackageError::Io {
path: pkg_dir.display().to_string(),
detail: e.to_string(),
})
}
fn packages_cache_dir() -> Result<PathBuf, PackageError> {
if let Ok(dir) = std::env::var("LEX_PACKAGES_DIR") {
return Ok(PathBuf::from(dir));
}
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map_err(|_| PackageError::Io {
path: "~/.lex/packages".into(),
detail: "could not determine home directory (set LEX_PACKAGES_DIR)".into(),
})?;
Ok(PathBuf::from(home).join(".lex").join("packages"))
}
#[derive(Debug, thiserror::Error)]
pub enum PackageError {
#[error("no lex.toml found searching up from {searched_from} (needed to resolve \"{reference}\")")]
NoManifest { reference: String, searched_from: String },
#[error("failed to parse {path}: {detail}")]
ManifestParse { path: String, detail: String },
#[error("package \"{name}\" not found in {manifest}")]
UnknownPackage { name: String, manifest: String },
#[error("module \"{module}\" not found in package \"{pkg}\" (looked in {pkg_root}/src/ and {pkg_root}/)")]
ModuleNotFound { pkg: String, module: String, pkg_root: String },
#[error("git clone of {url} failed: {detail}")]
GitFailed { url: String, detail: String },
#[error("I/O error at {path}: {detail}")]
Io { path: String, detail: String },
}