lust/packages/
dependencies.rs

1use 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            // Parser guarantees either path or version exists.
96            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}