lust/packages/
dependencies.rs1use super::{
2 manifest::{ManifestError, ManifestKind, PackageManifest},
3 PackageManager,
4};
5use crate::config::{DependencyKind, LustConfig};
6use std::{
7 io,
8 path::{Path, PathBuf},
9};
10use thiserror::Error;
11
12#[derive(Debug, Default, Clone)]
13pub struct DependencyResolution {
14 lust: Vec<ResolvedLustDependency>,
15 rust: Vec<ResolvedRustDependency>,
16}
17
18impl DependencyResolution {
19 pub fn lust(&self) -> &[ResolvedLustDependency] {
20 &self.lust
21 }
22
23 pub fn rust(&self) -> &[ResolvedRustDependency] {
24 &self.rust
25 }
26}
27
28#[derive(Debug, Clone)]
29pub struct ResolvedLustDependency {
30 pub name: String,
31 pub sanitized_name: Option<String>,
32 pub module_root: PathBuf,
33 pub root_module: Option<PathBuf>,
34}
35
36#[derive(Debug, Clone)]
37pub struct ResolvedRustDependency {
38 pub name: String,
39 pub crate_dir: PathBuf,
40 pub features: Vec<String>,
41 pub default_features: bool,
42 pub externs_override: Option<PathBuf>,
43 pub cache_stub_dir: Option<PathBuf>,
44 pub version: Option<String>,
45}
46
47#[derive(Debug, Error)]
48pub enum DependencyResolutionError {
49 #[error("failed to prepare package cache: {source}")]
50 PackageCache {
51 #[source]
52 source: io::Error,
53 },
54 #[error("dependency '{name}' expected directory at {path}")]
55 MissingPath { name: String, path: PathBuf },
56 #[error("dependency '{name}' package version '{version}' not installed (expected at {path})")]
57 MissingPackage {
58 name: String,
59 version: String,
60 path: PathBuf,
61 },
62 #[error("dependency '{name}' manifest error: {source}")]
63 Manifest {
64 name: String,
65 #[source]
66 source: ManifestError,
67 },
68}
69
70pub fn resolve_dependencies(
71 config: &LustConfig,
72 project_dir: &Path,
73) -> Result<DependencyResolution, DependencyResolutionError> {
74 let mut resolution = DependencyResolution::default();
75 let manager = PackageManager::new(PackageManager::default_root());
76 manager
77 .ensure_layout()
78 .map_err(|source| DependencyResolutionError::PackageCache { source })?;
79
80 for spec in config.dependencies() {
81 let name = spec.name().to_string();
82 let (root_dir, version) = if let Some(path) = spec.path() {
83 (resolve_dependency_path(project_dir, path), None)
84 } else if let Some(version) = spec.version() {
85 let dir = manager.root().join(spec.name()).join(version);
86 if !dir.exists() {
87 return Err(DependencyResolutionError::MissingPackage {
88 name: spec.name().to_string(),
89 version: version.to_string(),
90 path: dir,
91 });
92 }
93 (dir, Some(version.to_string()))
94 } else {
95 unreachable!("dependency spec missing path and version");
97 };
98
99 if !root_dir.exists() {
100 return Err(DependencyResolutionError::MissingPath {
101 name: spec.name().to_string(),
102 path: root_dir,
103 });
104 }
105
106 let kind = match spec.kind() {
107 Some(kind) => kind,
108 None => detect_kind(spec.name(), &root_dir)?,
109 };
110
111 match kind {
112 DependencyKind::Lust => {
113 let module_root = resolve_module_root(&root_dir);
114 let root_module = detect_root_module(&module_root, spec.name());
115 let sanitized = sanitize_dependency_name(&name);
116 let sanitized_name = if sanitized != name {
117 Some(sanitized)
118 } else {
119 None
120 };
121 resolution.lust.push(ResolvedLustDependency {
122 name,
123 sanitized_name,
124 module_root,
125 root_module,
126 });
127 }
128 DependencyKind::Rust => {
129 let externs_override = spec
130 .externs()
131 .map(|value| resolve_optional_path(&root_dir, value));
132 let cache_stub_dir = if spec.path().is_some() {
133 None
134 } else {
135 Some(root_dir.join("externs"))
136 };
137 resolution.rust.push(ResolvedRustDependency {
138 name,
139 crate_dir: root_dir,
140 features: spec.features().to_vec(),
141 default_features: spec.default_features().unwrap_or(true),
142 externs_override,
143 cache_stub_dir,
144 version,
145 });
146 }
147 }
148 }
149
150 Ok(resolution)
151}
152
153fn detect_kind(name: &str, root: &Path) -> Result<DependencyKind, DependencyResolutionError> {
154 match PackageManifest::discover(root) {
155 Ok(manifest) => match manifest.kind() {
156 ManifestKind::Lust => Ok(DependencyKind::Lust),
157 ManifestKind::Cargo => Ok(DependencyKind::Rust),
158 },
159 Err(ManifestError::NotFound(_)) => {
160 if root.join("Cargo.toml").exists() {
161 Ok(DependencyKind::Rust)
162 } else {
163 Ok(DependencyKind::Lust)
164 }
165 }
166 Err(err) => Err(DependencyResolutionError::Manifest {
167 name: name.to_string(),
168 source: err,
169 }),
170 }
171}
172
173fn resolve_dependency_path(project_dir: &Path, raw: &str) -> PathBuf {
174 if raw == "/" {
175 return project_dir.to_path_buf();
176 }
177
178 let candidate = PathBuf::from(raw);
179 if candidate.is_absolute() {
180 candidate
181 } else {
182 project_dir.join(candidate)
183 }
184}
185
186fn resolve_optional_path(root: &Path, raw: &str) -> PathBuf {
187 if raw == "/" {
188 return root.to_path_buf();
189 }
190 let candidate = PathBuf::from(raw);
191 if candidate.is_absolute() {
192 candidate
193 } else {
194 root.join(candidate)
195 }
196}
197
198fn resolve_module_root(root: &Path) -> PathBuf {
199 let src = root.join("src");
200 if src.is_dir() {
201 src
202 } else {
203 root.to_path_buf()
204 }
205}
206
207fn detect_root_module(module_root: &Path, prefix: &str) -> Option<PathBuf> {
208 let lib = module_root.join("lib.lust");
209 if lib.exists() {
210 return Some(PathBuf::from("lib.lust"));
211 }
212
213 let prefixed = module_root.join(format!("{prefix}.lust"));
214 if prefixed.exists() {
215 return Some(PathBuf::from(format!("{prefix}.lust")));
216 }
217
218 let sanitized = sanitize_dependency_name(prefix);
219 if sanitized != prefix {
220 let sanitized_path = module_root.join(format!("{sanitized}.lust"));
221 if sanitized_path.exists() {
222 return Some(PathBuf::from(format!("{sanitized}.lust")));
223 }
224 }
225
226 let main = module_root.join("main.lust");
227 if main.exists() {
228 return Some(PathBuf::from("main.lust"));
229 }
230
231 None
232}
233
234fn sanitize_dependency_name(name: &str) -> String {
235 name.replace('-', "_")
236}