Skip to main content

lust/packages/
dependencies.rs

1use super::{
2    manifest::{ManifestError, ManifestKind, PackageManifest},
3    PackageManager,
4};
5use crate::config::{DependencyKind, LustConfig};
6use object::{File, Object, ObjectSymbol};
7use std::{
8    collections::HashSet,
9    fs, io,
10    path::{Path, PathBuf},
11};
12use thiserror::Error;
13
14#[derive(Debug, Default, Clone)]
15pub struct DependencyResolution {
16    lust: Vec<ResolvedLustDependency>,
17    rust: Vec<ResolvedRustDependency>,
18    lua: Vec<ResolvedLuaDependency>,
19}
20
21impl DependencyResolution {
22    pub fn lust(&self) -> &[ResolvedLustDependency] {
23        &self.lust
24    }
25
26    pub fn rust(&self) -> &[ResolvedRustDependency] {
27        &self.rust
28    }
29
30    pub fn lua(&self) -> &[ResolvedLuaDependency] {
31        &self.lua
32    }
33}
34
35#[derive(Debug, Clone)]
36pub struct ResolvedLustDependency {
37    pub name: String,
38    pub sanitized_name: Option<String>,
39    pub module_root: PathBuf,
40    pub root_module: Option<PathBuf>,
41}
42
43#[derive(Debug, Clone)]
44pub struct ResolvedRustDependency {
45    pub name: String,
46    pub crate_dir: PathBuf,
47    pub features: Vec<String>,
48    pub default_features: bool,
49    pub externs_override: Option<PathBuf>,
50    pub cache_stub_dir: Option<PathBuf>,
51    pub version: Option<String>,
52}
53
54#[derive(Debug, Clone)]
55pub struct ResolvedLuaDependency {
56    pub name: String,
57    pub library_path: PathBuf,
58    pub luaopen_symbols: Vec<String>,
59    pub version: Option<String>,
60    pub lua_files: Vec<PathBuf>,
61}
62
63#[derive(Debug, Clone)]
64enum DetectedKind {
65    Lust,
66    Rust,
67    Lua {
68        luaopen_symbols: Vec<String>,
69        lua_files: Vec<PathBuf>,
70    },
71}
72
73#[derive(Default)]
74struct LibrarySignature {
75    luaopen_symbols: Vec<String>,
76    has_lust_extension: bool,
77}
78
79#[derive(Debug, Error)]
80pub enum DependencyResolutionError {
81    #[error("failed to prepare package cache: {source}")]
82    PackageCache {
83        #[source]
84        source: io::Error,
85    },
86    #[error("dependency '{name}' expected directory at {path}")]
87    MissingPath { name: String, path: PathBuf },
88    #[error("dependency '{name}' package version '{version}' not installed (expected at {path})")]
89    MissingPackage {
90        name: String,
91        version: String,
92        path: PathBuf,
93    },
94    #[error("dependency '{name}' manifest error: {source}")]
95    Manifest {
96        name: String,
97        #[source]
98        source: ManifestError,
99    },
100    #[error("failed to read library '{path}': {source}")]
101    LibraryIo {
102        path: PathBuf,
103        #[source]
104        source: io::Error,
105    },
106    #[error("failed to inspect library '{path}': {source}")]
107    LibraryInspect {
108        path: PathBuf,
109        #[source]
110        source: object::read::Error,
111    },
112    #[error(
113        "dependency '{name}' at {path} is a shared library but its kind could not be detected"
114    )]
115    UnknownLibraryKind { name: String, path: PathBuf },
116}
117
118pub fn resolve_dependencies(
119    config: &LustConfig,
120    project_dir: &Path,
121) -> Result<DependencyResolution, DependencyResolutionError> {
122    let mut resolution = DependencyResolution::default();
123    let manager = PackageManager::new(PackageManager::default_root());
124    manager
125        .ensure_layout()
126        .map_err(|source| DependencyResolutionError::PackageCache { source })?;
127
128    for spec in config.dependencies() {
129        let name = spec.name().to_string();
130        let (root_dir, version) = if let Some(path) = spec.path() {
131            (resolve_dependency_path(project_dir, path), None)
132        } else if let Some(version) = spec.version() {
133            let dir = manager.root().join(spec.name()).join(version);
134            if !dir.exists() {
135                return Err(DependencyResolutionError::MissingPackage {
136                    name: spec.name().to_string(),
137                    version: version.to_string(),
138                    path: dir,
139                });
140            }
141            (dir, Some(version.to_string()))
142        } else {
143            // Parser guarantees either path or version exists.
144            unreachable!("dependency spec missing path and version");
145        };
146
147        if !root_dir.exists() {
148            return Err(DependencyResolutionError::MissingPath {
149                name: spec.name().to_string(),
150                path: root_dir,
151            });
152        }
153
154        let detected = match spec.kind() {
155            Some(DependencyKind::Lust) => DetectedKind::Lust,
156            Some(DependencyKind::Rust) => DetectedKind::Rust,
157            Some(DependencyKind::Lua) => DetectedKind::Lua {
158                luaopen_symbols: detect_luaopen_symbols(&root_dir)?,
159                lua_files: collect_lua_files(&root_dir),
160            },
161            None => detect_kind(spec.name(), &root_dir)?,
162        };
163
164        match detected {
165            DetectedKind::Lust => {
166                let module_root = resolve_module_root(&root_dir);
167                let root_module = detect_root_module(&module_root, spec.name());
168                let sanitized = sanitize_dependency_name(&name);
169                let sanitized_name = if sanitized != name {
170                    Some(sanitized)
171                } else {
172                    None
173                };
174                resolution.lust.push(ResolvedLustDependency {
175                    name,
176                    sanitized_name,
177                    module_root,
178                    root_module,
179                });
180            }
181            DetectedKind::Rust => {
182                let externs_override = spec
183                    .externs()
184                    .map(|value| resolve_optional_path(&root_dir, value));
185                let cache_stub_dir = if spec.path().is_some() {
186                    None
187                } else {
188                    Some(root_dir.join("externs"))
189                };
190                resolution.rust.push(ResolvedRustDependency {
191                    name,
192                    crate_dir: root_dir,
193                    features: spec.features().to_vec(),
194                    default_features: spec.default_features().unwrap_or(true),
195                    externs_override,
196                    cache_stub_dir,
197                    version,
198                });
199            }
200            DetectedKind::Lua {
201                luaopen_symbols,
202                lua_files,
203            } => {
204                resolution.lua.push(ResolvedLuaDependency {
205                    name,
206                    library_path: root_dir,
207                    luaopen_symbols,
208                    version,
209                    lua_files,
210                });
211            }
212        }
213    }
214
215    Ok(resolution)
216}
217
218fn detect_kind(name: &str, root: &Path) -> Result<DetectedKind, DependencyResolutionError> {
219    if root.is_file() {
220        let signature = inspect_library(root)?;
221        let luaopen_symbols = signature.luaopen_symbols;
222        let has_lust_register = signature.has_lust_extension;
223        return if !luaopen_symbols.is_empty() {
224            Ok(DetectedKind::Lua {
225                luaopen_symbols,
226                lua_files: Vec::new(),
227            })
228        } else if has_lust_register {
229            Ok(DetectedKind::Rust)
230        } else {
231            Err(DependencyResolutionError::UnknownLibraryKind {
232                name: name.to_string(),
233                path: root.to_path_buf(),
234            })
235        };
236    }
237
238    match PackageManifest::discover(root) {
239        Ok(manifest) => match manifest.kind() {
240            ManifestKind::Lust => Ok(DetectedKind::Lust),
241            ManifestKind::Cargo => Ok(DetectedKind::Rust),
242        },
243        Err(ManifestError::NotFound(_)) => {
244            if root.join("Cargo.toml").exists() {
245                Ok(DetectedKind::Rust)
246            } else if has_lua_files(root) {
247                Ok(DetectedKind::Lua {
248                    luaopen_symbols: Vec::new(),
249                    lua_files: collect_lua_files(root),
250                })
251            } else {
252                Ok(DetectedKind::Lust)
253            }
254        }
255        Err(err) => Err(DependencyResolutionError::Manifest {
256            name: name.to_string(),
257            source: err,
258        }),
259    }
260}
261
262fn detect_luaopen_symbols(root: &Path) -> Result<Vec<String>, DependencyResolutionError> {
263    if !root.is_file() {
264        return Ok(Vec::new());
265    }
266    let signature = inspect_library(root)?;
267    Ok(signature.luaopen_symbols)
268}
269
270#[allow(dead_code)]
271fn detect_lust_extension_symbol(root: &Path) -> Result<bool, DependencyResolutionError> {
272    if !root.is_file() {
273        return Ok(false);
274    }
275    let signature = inspect_library(root)?;
276    Ok(signature.has_lust_extension)
277}
278
279fn inspect_library(path: &Path) -> Result<LibrarySignature, DependencyResolutionError> {
280    let bytes = fs::read(path).map_err(|source| DependencyResolutionError::LibraryIo {
281        path: path.to_path_buf(),
282        source,
283    })?;
284    let file =
285        File::parse(&*bytes).map_err(|source| DependencyResolutionError::LibraryInspect {
286            path: path.to_path_buf(),
287            source,
288        })?;
289
290    let mut signature = LibrarySignature::default();
291    let mut lua_syms: HashSet<String> = HashSet::new();
292
293    for symbol in file.symbols().chain(file.dynamic_symbols()) {
294        if !symbol.is_definition() {
295            continue;
296        }
297        let Ok(raw_name) = symbol.name() else {
298            continue;
299        };
300        let name = raw_name.trim_start_matches('_');
301        if name == "lust_extension_register" {
302            signature.has_lust_extension = true;
303        }
304        if let Some(stripped) = name.strip_prefix("luaopen_") {
305            lua_syms.insert(format!("luaopen_{stripped}"));
306        }
307    }
308
309    signature.luaopen_symbols = lua_syms.into_iter().collect();
310    signature.luaopen_symbols.sort();
311    Ok(signature)
312}
313
314fn has_lua_files(root: &Path) -> bool {
315    collect_lua_files(root).len() > 0
316}
317
318fn collect_lua_files(root: &Path) -> Vec<PathBuf> {
319    let mut files = Vec::new();
320    collect_lua_files_recursive(root, root, &mut files);
321    files
322}
323
324fn collect_lua_files_recursive(base: &Path, current: &Path, files: &mut Vec<PathBuf>) {
325    if let Ok(read_dir) = fs::read_dir(current) {
326        for entry in read_dir.flatten() {
327            let path = entry.path();
328            if path.is_dir() {
329                collect_lua_files_recursive(base, &path, files);
330            } else if path.extension().and_then(|s| s.to_str()) == Some("lua") {
331                if let Ok(relative) = path.strip_prefix(base) {
332                    files.push(relative.to_path_buf());
333                }
334            }
335        }
336    }
337}
338
339fn resolve_dependency_path(project_dir: &Path, raw: &str) -> PathBuf {
340    if raw == "/" {
341        return project_dir.to_path_buf();
342    }
343
344    let candidate = PathBuf::from(raw);
345    if candidate.is_absolute() {
346        candidate
347    } else {
348        project_dir.join(candidate)
349    }
350}
351
352fn resolve_optional_path(root: &Path, raw: &str) -> PathBuf {
353    if raw == "/" {
354        return root.to_path_buf();
355    }
356    let candidate = PathBuf::from(raw);
357    if candidate.is_absolute() {
358        candidate
359    } else {
360        root.join(candidate)
361    }
362}
363
364fn resolve_module_root(root: &Path) -> PathBuf {
365    let src = root.join("src");
366    if src.is_dir() {
367        src
368    } else {
369        root.to_path_buf()
370    }
371}
372
373fn detect_root_module(module_root: &Path, prefix: &str) -> Option<PathBuf> {
374    let lib = module_root.join("lib.lust");
375    if lib.exists() {
376        return Some(PathBuf::from("lib.lust"));
377    }
378
379    let prefixed = module_root.join(format!("{prefix}.lust"));
380    if prefixed.exists() {
381        return Some(PathBuf::from(format!("{prefix}.lust")));
382    }
383
384    let sanitized = sanitize_dependency_name(prefix);
385    if sanitized != prefix {
386        let sanitized_path = module_root.join(format!("{sanitized}.lust"));
387        if sanitized_path.exists() {
388            return Some(PathBuf::from(format!("{sanitized}.lust")));
389        }
390    }
391
392    let main = module_root.join("main.lust");
393    if main.exists() {
394        return Some(PathBuf::from("main.lust"));
395    }
396
397    None
398}
399
400fn sanitize_dependency_name(name: &str) -> String {
401    name.replace('-', "_")
402}