1use serde::Deserialize;
33use std::collections::HashMap;
34use std::path::{Path, PathBuf};
35
36#[derive(Debug, Deserialize)]
39pub struct Manifest {
40 pub package: Option<PackageMeta>,
41 #[serde(default)]
42 pub dependencies: HashMap<String, Dependency>,
43}
44
45#[derive(Debug, Deserialize)]
46pub struct PackageMeta {
47 pub name: String,
48 #[serde(default)]
49 pub version: String,
50}
51
52#[derive(Debug, Deserialize)]
53#[serde(untagged)]
54pub enum Dependency {
55 Path { path: String },
56 Git { git: String },
57}
58
59impl Manifest {
60 pub fn load(toml_path: &Path) -> Result<Self, String> {
61 let src = std::fs::read_to_string(toml_path)
62 .map_err(|e| format!("reading {}: {e}", toml_path.display()))?;
63 toml::from_str(&src)
64 .map_err(|e| format!("parsing {}: {e}", toml_path.display()))
65 }
66}
67
68pub fn find_manifest(start: &Path) -> Option<(PathBuf, PathBuf)> {
73 let mut dir = if start.is_dir() {
74 start.to_path_buf()
75 } else {
76 start.parent()?.to_path_buf()
77 };
78 loop {
79 let candidate = dir.join("lex.toml");
80 if candidate.exists() {
81 return Some((candidate, dir));
82 }
83 match dir.parent() {
84 Some(p) if p != dir => dir = p.to_path_buf(),
85 _ => return None,
86 }
87 }
88}
89
90pub fn resolve_package_import(
97 importer: &Path,
98 pkg_name: &str,
99 module_path: &str,
100) -> Result<PathBuf, PackageError> {
101 let (toml_path, toml_dir) = find_manifest(importer).ok_or_else(|| {
102 PackageError::NoManifest {
103 reference: format!("{pkg_name}/{module_path}"),
104 searched_from: importer.display().to_string(),
105 }
106 })?;
107
108 let manifest = Manifest::load(&toml_path)
109 .map_err(|e| PackageError::ManifestParse { path: toml_path.display().to_string(), detail: e })?;
110
111 let dep = manifest.dependencies.get(pkg_name).ok_or_else(|| {
112 PackageError::UnknownPackage {
113 name: pkg_name.to_string(),
114 manifest: toml_path.display().to_string(),
115 }
116 })?;
117
118 let pkg_root = match dep {
119 Dependency::Path { path } => {
120 let raw = toml_dir.join(path);
121 raw.canonicalize().map_err(|e| PackageError::Io {
122 path: raw.display().to_string(),
123 detail: e.to_string(),
124 })?
125 }
126 Dependency::Git { git } => git_ensure_cached(pkg_name, git)?,
127 };
128
129 find_module_file(&pkg_root, module_path).ok_or_else(|| PackageError::ModuleNotFound {
130 pkg: pkg_name.to_string(),
131 module: module_path.to_string(),
132 pkg_root: pkg_root.display().to_string(),
133 })
134}
135
136fn find_module_file(pkg_root: &Path, module_path: &str) -> Option<PathBuf> {
139 let rel = PathBuf::from(module_path).with_extension("lex");
140 let in_src = pkg_root.join("src").join(&rel);
141 if in_src.exists() {
142 return Some(in_src);
143 }
144 let at_root = pkg_root.join(&rel);
145 if at_root.exists() {
146 return Some(at_root);
147 }
148 None
149}
150
151fn git_ensure_cached(pkg_name: &str, url: &str) -> Result<PathBuf, PackageError> {
158 let cache_root = packages_cache_dir()?;
159 let pkg_dir = cache_root.join(pkg_name);
160 if pkg_dir.exists() {
161 return Ok(pkg_dir);
162 }
163 std::fs::create_dir_all(&cache_root).map_err(|e| PackageError::Io {
164 path: cache_root.display().to_string(),
165 detail: e.to_string(),
166 })?;
167 let status = std::process::Command::new("git")
168 .args(["clone", "--depth=1", url, pkg_dir.to_str().unwrap_or(pkg_name)])
169 .status()
170 .map_err(|e| PackageError::GitFailed {
171 url: url.to_string(),
172 detail: format!("could not run `git`: {e}"),
173 })?;
174 if !status.success() {
175 return Err(PackageError::GitFailed {
176 url: url.to_string(),
177 detail: format!("`git clone` exited with {status}"),
178 });
179 }
180 pkg_dir.canonicalize().map_err(|e| PackageError::Io {
181 path: pkg_dir.display().to_string(),
182 detail: e.to_string(),
183 })
184}
185
186fn packages_cache_dir() -> Result<PathBuf, PackageError> {
187 if let Ok(dir) = std::env::var("LEX_PACKAGES_DIR") {
188 return Ok(PathBuf::from(dir));
189 }
190 let home = std::env::var("HOME")
191 .or_else(|_| std::env::var("USERPROFILE"))
192 .map_err(|_| PackageError::Io {
193 path: "~/.lex/packages".into(),
194 detail: "could not determine home directory (set LEX_PACKAGES_DIR)".into(),
195 })?;
196 Ok(PathBuf::from(home).join(".lex").join("packages"))
197}
198
199#[derive(Debug, thiserror::Error)]
202pub enum PackageError {
203 #[error("no lex.toml found searching up from {searched_from} (needed to resolve \"{reference}\")")]
204 NoManifest { reference: String, searched_from: String },
205
206 #[error("failed to parse {path}: {detail}")]
207 ManifestParse { path: String, detail: String },
208
209 #[error("package \"{name}\" not found in {manifest}")]
210 UnknownPackage { name: String, manifest: String },
211
212 #[error("module \"{module}\" not found in package \"{pkg}\" (looked in {pkg_root}/src/ and {pkg_root}/)")]
213 ModuleNotFound { pkg: String, module: String, pkg_root: String },
214
215 #[error("git clone of {url} failed: {detail}")]
216 GitFailed { url: String, detail: String },
217
218 #[error("I/O error at {path}: {detail}")]
219 Io { path: String, detail: String },
220}