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 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}