1use crate::{
2 config::external_deps::ExternalDependencySearchConfig,
3 lua_rockspec::ExternalDependencySpec,
4 variables::{GetVariableError, HasVariables},
5};
6use itertools::Itertools;
7use miette::Diagnostic;
8use path_slash::{PathBufExt, PathExt};
9use pkg_config::{Config as PkgConfig, Library};
10use std::{
11 collections::HashMap,
12 path::{Path, PathBuf},
13};
14use thiserror::Error;
15
16use super::utils::{c_lib_extension, format_path};
17
18use crate::fs;
19
20#[derive(Error, Debug, Diagnostic)]
21#[non_exhaustive]
22pub enum ExternalDependencyError {
23 #[error("external dependency '{0}' not found")]
24 #[diagnostic(help("{}", not_found_help(.0)))]
25 NotFound(String),
26 #[error(transparent)]
27 #[diagnostic(transparent)]
28 Fs(#[from] fs::FsError),
29 #[error("{0} was probed successfully, but the header {1} could not be found")]
30 #[diagnostic(help(
31 r#"ensure the header is installed.
32As a fallback, set {0}_INCDIR to the include directory."#
33 ))]
34 SuccessfulProbeHeaderNotFound(String, String),
35 #[error("error probing external dependency {0}: the header {1} could not be found")]
36 #[diagnostic(help(
37 r#"ensure the package is installed with pkg-config support,
38or set {0}_INCDIR to the directory containing the header."#
39 ))]
40 HeaderNotFound(String, String),
41 #[error("error probing external dependency {0}: the library {1} could not be found")]
42 #[diagnostic(help(
43 r#"ensure the package is installed with pkg-config support,
44or set {0}_LIBDIR to the directory containing the library."#
45 ))]
46 LibraryNotFound(String, String),
47}
48
49#[derive(Debug)]
50pub struct ExternalDependencyInfo {
51 pub(crate) include_dir: Option<PathBuf>,
52 pub(crate) lib_dir: Option<PathBuf>,
53 pub(crate) bin_dir: Option<PathBuf>,
54 pub(crate) lib_name: Option<String>,
57 pub(crate) lib_info: Option<Library>,
59}
60
61#[tracing::instrument(level = "trace")]
62fn pkg_config_probe(name: &str) -> Option<Library> {
63 PkgConfig::new()
64 .print_system_libs(false)
65 .cargo_metadata(false)
66 .env_metadata(false)
67 .probe(&name.to_lowercase())
68 .ok()
69}
70
71impl ExternalDependencyInfo {
72 #[tracing::instrument(level = "trace", skip(config))]
73 pub fn probe(
74 name: &str,
75 dependency: &ExternalDependencySpec,
76 config: &ExternalDependencySearchConfig,
77 ) -> Result<Self, ExternalDependencyError> {
78 let lib_info = pkg_config_probe(name)
79 .or(pkg_config_probe(&format!("lib{}", name.to_lowercase())))
80 .or(dependency.library.as_ref().and_then(|lib_name| {
81 let lib_name = lib_name.to_string_lossy().to_string();
82 let lib_name_without_ext = lib_name.split('.').next().unwrap_or(&lib_name);
83 pkg_config_probe(lib_name_without_ext)
84 .or(pkg_config_probe(&format!("lib{lib_name_without_ext}")))
85 }));
86 if let Some(info) = lib_info {
87 let include_dir = if let Some(header) = &dependency.header {
88 Some(
89 info.include_paths
90 .iter()
91 .find(|path| path.join(header).exists())
92 .ok_or(ExternalDependencyError::SuccessfulProbeHeaderNotFound(
93 name.to_string(),
94 header.to_slash_lossy().to_string(),
95 ))?
96 .clone(),
97 )
98 } else {
99 info.include_paths.first().cloned()
100 };
101 let lib_dir = if let Some(lib) = &dependency.library {
102 info.link_paths
103 .iter()
104 .find(|path| library_exists(path, lib, &config.lib_patterns))
105 .cloned()
106 .or(info.link_paths.first().cloned())
107 } else {
108 info.link_paths.first().cloned()
109 };
110 let bin_dir = lib_dir.as_ref().and_then(|lib_dir| {
111 lib_dir
112 .parent()
113 .map(|parent| parent.join("bin"))
114 .filter(|dir| dir.is_dir())
115 });
116 let lib_name = lib_dir.as_ref().and_then(|lib_dir| {
117 let prefix = dependency
118 .library
119 .as_ref()
120 .map(|lib_name| lib_name.to_string_lossy().to_string())
121 .unwrap_or(name.to_lowercase());
122 get_lib_name(lib_dir, &prefix)
123 });
124 return Ok(ExternalDependencyInfo {
125 include_dir,
126 lib_dir,
127 bin_dir,
128 lib_name,
129 lib_info: Some(info),
130 });
131 }
132 Self::fallback_probe(name, dependency, config)
133 }
134
135 #[tracing::instrument(level = "trace", skip(config))]
136 fn fallback_probe(
137 name: &str,
138 dependency: &ExternalDependencySpec,
139 config: &ExternalDependencySearchConfig,
140 ) -> Result<Self, ExternalDependencyError> {
141 let env_prefix = std::env::var(format!("{}_DIR", name.to_uppercase())).ok();
142
143 let mut search_prefixes = Vec::new();
144 if let Some(dir) = env_prefix {
145 search_prefixes.push(PathBuf::from(dir));
146 }
147 if let Some(prefix) = config.prefixes.get(&format!("{}_DIR", name.to_uppercase())) {
148 search_prefixes.push(prefix.clone());
149 }
150 search_prefixes.extend(config.search_prefixes.iter().cloned());
151
152 let mut include_dir = get_incdir(name, config);
153
154 if let Some(header) = &dependency.header {
155 if !&include_dir
156 .as_ref()
157 .is_some_and(|inc_dir| inc_dir.join(header).exists())
158 {
159 let inc_dir = search_prefixes
161 .iter()
162 .find_map(|prefix| {
163 let inc_dir = prefix.join(&config.include_subdir);
164 if inc_dir.join(header).exists() {
165 Some(inc_dir)
166 } else {
167 None
168 }
169 })
170 .ok_or(ExternalDependencyError::HeaderNotFound(
171 name.to_string(),
172 header.to_slash_lossy().to_string(),
173 ))?;
174 include_dir = Some(inc_dir);
175 }
176 }
177
178 let mut lib_dir = get_libdir(name, config);
179
180 if let Some(lib) = &dependency.library {
181 if !lib_dir
182 .as_ref()
183 .is_some_and(|lib_dir| library_exists(lib_dir, lib, &config.lib_patterns))
184 {
185 let probed_lib_dir = search_prefixes
186 .iter()
187 .find_map(|prefix| {
188 for lib_subdir in &config.lib_subdirs {
189 let lib_dir_candidate = prefix.join(lib_subdir);
190 if library_exists(&lib_dir_candidate, lib, &config.lib_patterns) {
191 return Some(lib_dir_candidate);
192 }
193 }
194 None
195 })
196 .ok_or(ExternalDependencyError::LibraryNotFound(
197 name.to_string(),
198 lib.to_slash_lossy().to_string(),
199 ))?;
200 lib_dir = Some(probed_lib_dir);
201 }
202 }
203
204 if let (None, None) = (&include_dir, &lib_dir) {
205 return Err(ExternalDependencyError::NotFound(name.into()));
206 }
207 let bin_dir = lib_dir.as_ref().and_then(|lib_dir| {
208 lib_dir
209 .parent()
210 .map(|parent| parent.join("bin"))
211 .filter(|dir| dir.is_dir())
212 });
213 let lib_name = lib_dir.as_ref().and_then(|lib_dir| {
214 let prefix = dependency
215 .library
216 .as_ref()
217 .map(|lib_name| lib_name.to_string_lossy().to_string())
218 .unwrap_or(name.to_lowercase());
219 get_lib_name(lib_dir, &prefix)
220 });
221 Ok(ExternalDependencyInfo {
222 include_dir,
223 lib_dir,
224 bin_dir,
225 lib_name,
226 lib_info: None,
227 })
228 }
229
230 pub(crate) fn define_flags(&self) -> Vec<String> {
231 if let Some(info) = &self.lib_info {
232 info.defines
233 .iter()
234 .map(|(k, v)| match v {
235 Some(val) => {
236 format!("-D{k}={val}")
237 }
238 None => format!("-D{k}"),
239 })
240 .collect_vec()
241 } else {
242 Vec::new()
243 }
244 }
245
246 pub(crate) fn lib_link_args(&self, compiler: &cc::Tool) -> Vec<String> {
247 if let Some(info) = &self.lib_info {
248 info.link_paths
249 .iter()
250 .map(|p| lib_dir_compile_arg(p, compiler))
251 .chain(
252 info.libs
253 .iter()
254 .map(|lib| format_lib_link_arg(lib, compiler)),
255 )
256 .chain(info.ld_args.iter().map(|ld_arg_group| {
257 ld_arg_group
258 .iter()
259 .map(|arg| format_linker_arg(arg, compiler))
260 .collect::<Vec<_>>()
261 .join(" ")
262 }))
263 .collect_vec()
264 } else {
265 self.lib_dir
266 .iter()
267 .map(|lib_dir| lib_dir_compile_arg(lib_dir, compiler))
268 .chain(
269 self.lib_name
270 .as_ref()
271 .and_then(|lib_name| {
272 if compiler.is_like_msvc() {
273 self.lib_dir.as_ref().map(|lib_dir| {
274 lib_dir.join(lib_name).to_slash_lossy().to_string()
275 })
276 } else {
277 Some(format!("-l{lib_name}"))
278 }
279 })
280 .iter()
281 .cloned(),
282 )
283 .collect_vec()
284 }
285 }
286}
287
288impl HasVariables for HashMap<String, ExternalDependencyInfo> {
289 #[tracing::instrument(level = "trace")]
290 fn get_variable(&self, input: &str) -> Result<Option<String>, GetVariableError> {
291 Ok(input.split_once('_').and_then(|(dep_key, dep_dir_type)| {
292 self.get(dep_key)
293 .and_then(|dep| match dep_dir_type {
294 "DIR" => dep
295 .include_dir
296 .as_ref()
297 .and_then(|dir| dir.parent().map(|parent| parent.to_path_buf())),
298 "INCDIR" => dep.include_dir.clone(),
299 "LIBDIR" => dep.lib_dir.clone(),
300 "BINDIR" => dep.bin_dir.clone(),
301 _ => None,
302 })
303 .as_deref()
304 .map(format_path)
305 }))
306 }
307}
308
309fn library_exists(lib_dir: &Path, lib: &Path, patterns: &[String]) -> bool {
310 patterns.iter().any(|pattern| {
311 let file_name = pattern.replace('?', &format!("{}", lib.display()));
312 lib_dir.join(&file_name).exists()
313 })
314}
315
316fn get_incdir(name: &str, config: &ExternalDependencySearchConfig) -> Option<PathBuf> {
317 let var_name = format!("{}_INCDIR", name.to_uppercase());
318 if let Ok(env_incdir) = std::env::var(&var_name) {
319 Some(env_incdir.into())
320 } else {
321 config.prefixes.get(&var_name).cloned()
322 }
323 .filter(|dir| dir.is_dir())
324}
325
326fn get_libdir(name: &str, config: &ExternalDependencySearchConfig) -> Option<PathBuf> {
327 let var_name = format!("{}_LIBDIR", name.to_uppercase());
328 if let Ok(env_incdir) = std::env::var(&var_name) {
329 Some(env_incdir.into())
330 } else {
331 config.prefixes.get(&var_name).cloned()
332 }
333 .filter(|dir| dir.is_dir())
334}
335
336fn not_found_help(name: &String) -> String {
337 let env_dir = format!("{}_DIR", &name.to_uppercase());
338 let env_inc = format!("{}_INCDIR", &name.to_uppercase());
339 let env_lib = format!("{}_LIBDIR", &name.to_uppercase());
340
341 format!(
342 r#"run `lx debug toolchains` to check pkg-config availability, or set:
343- {env_dir} for the installation prefix, or
344- {env_inc} and {env_lib} for specific directories
345
346alternatively, add to the [external_dependencies] section in lux.toml:
347[external_dependencies.{name}]
348prefix = "/path/to/installation""#
349 )
350}
351
352fn lib_dir_compile_arg(dir: &Path, compiler: &cc::Tool) -> String {
353 if compiler.is_like_msvc() {
354 format!("/LIBPATH:{}", dir.to_slash_lossy())
355 } else {
356 format!("-L{}", dir.to_slash_lossy())
357 }
358}
359
360fn format_lib_link_arg(lib: &str, compiler: &cc::Tool) -> String {
361 if compiler.is_like_msvc() {
362 format!("{lib}.lib")
363 } else {
364 format!("-l{lib}")
365 }
366}
367
368fn format_linker_arg(arg: &str, compiler: &cc::Tool) -> String {
369 if compiler.is_like_msvc() {
370 format!("-Wl,{arg}")
371 } else {
372 format!("/link {arg}")
373 }
374}
375
376pub(crate) fn to_lib_name(file: &Path) -> String {
377 let file_name = file.file_name().unwrap_or_default();
378 if cfg!(target_family = "unix") {
379 file_name
380 .to_string_lossy()
381 .trim_start_matches("lib")
382 .trim_end_matches(".a")
383 .to_string()
384 } else {
385 file_name.to_string_lossy().to_string()
386 }
387}
388
389fn get_lib_name(lib_dir: &Path, prefix: &str) -> Option<String> {
390 fs::sync::read_dir(lib_dir)
391 .ok()
392 .and_then(|entries| {
393 entries
394 .filter_map(Result::ok)
395 .map(|entry| entry.path().to_path_buf())
396 .filter(|file| file.extension().is_some_and(|ext| ext == c_lib_extension()))
397 .filter(|file| {
398 file.file_name()
399 .is_some_and(|name| is_lib_name(&name.to_string_lossy(), prefix))
400 })
401 .collect_vec()
402 .first()
403 .cloned()
404 })
405 .map(|file| to_lib_name(&file))
406}
407
408fn is_lib_name(file_name: &str, prefix: &str) -> bool {
409 #[cfg(target_family = "unix")]
410 let file_name = file_name.trim_start_matches("lib");
411 file_name == format!("{}.{}", prefix, c_lib_extension())
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use assert_fs::{prelude::*, TempDir};
418
419 #[tokio::test]
420 async fn test_detect_zlib_pkg_config_header() {
421 let config = ExternalDependencySearchConfig::default();
423 ExternalDependencyInfo::probe(
424 "zlib",
425 &ExternalDependencySpec {
426 header: Some("zlib.h".into()),
427 library: None,
428 },
429 &config,
430 )
431 .unwrap();
432 }
433
434 #[tokio::test]
435 async fn test_detect_zlib_pkg_config_library_libz() {
436 let config = ExternalDependencySearchConfig::default();
438 ExternalDependencyInfo::probe(
439 "zlib",
440 &ExternalDependencySpec {
441 library: Some("libz".into()),
442 header: None,
443 },
444 &config,
445 )
446 .unwrap();
447 }
448
449 #[tokio::test]
450 async fn test_detect_zlib_pkg_config_library_z() {
451 let config = ExternalDependencySearchConfig::default();
453 ExternalDependencyInfo::probe(
454 "zlib",
455 &ExternalDependencySpec {
456 library: Some("z".into()),
457 header: None,
458 },
459 &config,
460 )
461 .unwrap();
462 }
463
464 #[tokio::test]
465 async fn test_detect_zlib_pkg_config_library_zlib() {
466 let config = ExternalDependencySearchConfig::default();
468 ExternalDependencyInfo::probe(
469 "zlib",
470 &ExternalDependencySpec {
471 library: Some("zlib".into()),
472 header: None,
473 },
474 &config,
475 )
476 .unwrap();
477 }
478
479 #[tokio::test]
480 async fn test_fallback_detect_header_prefix() {
481 let temp = TempDir::new().unwrap();
482 let prefix_dir = temp.child("usr");
483 let include_dir = prefix_dir.child("include");
484 include_dir.create_dir_all().unwrap();
485
486 let header = include_dir.child("foo.h");
487 header.touch().unwrap();
488
489 let mut config = ExternalDependencySearchConfig::default();
490 config
491 .prefixes
492 .insert("FOO_DIR".into(), prefix_dir.path().to_path_buf());
493
494 ExternalDependencyInfo::fallback_probe(
495 "foo",
496 &ExternalDependencySpec {
497 header: Some("foo.h".into()),
498 library: None,
499 },
500 &config,
501 )
502 .unwrap();
503 }
504
505 #[tokio::test]
506 async fn test_fallback_detect_header_prefix_incdir() {
507 let temp = TempDir::new().unwrap();
508 let include_dir = temp.child("include");
509 include_dir.create_dir_all().unwrap();
510
511 let header = include_dir.child("foo.h");
512 header.touch().unwrap();
513
514 let mut config = ExternalDependencySearchConfig::default();
515 config
516 .prefixes
517 .insert("FOO_INCDIR".into(), include_dir.path().to_path_buf());
518
519 ExternalDependencyInfo::fallback_probe(
520 "foo",
521 &ExternalDependencySpec {
522 header: Some("foo.h".into()),
523 library: None,
524 },
525 &config,
526 )
527 .unwrap();
528 }
529
530 #[tokio::test]
531 async fn test_fallback_detect_library_prefix() {
532 let temp = TempDir::new().unwrap();
533 let prefix_dir = temp.child("usr");
534 let include_dir = prefix_dir.child("include");
535 let lib_dir = prefix_dir.child("lib");
536 include_dir.create_dir_all().unwrap();
537 lib_dir.create_dir_all().unwrap();
538
539 #[cfg(any(target_os = "linux", target_os = "android"))]
540 let lib = lib_dir.child("libfoo.so");
541 #[cfg(target_os = "macos")]
542 let lib = lib_dir.child("libfoo.dylib");
543 #[cfg(target_family = "windows")]
544 let lib = lib_dir.child("foo.dll");
545
546 lib.touch().unwrap();
547
548 let mut config = ExternalDependencySearchConfig::default();
549 config
550 .prefixes
551 .insert("FOO_DIR".to_string(), prefix_dir.path().to_path_buf());
552
553 ExternalDependencyInfo::fallback_probe(
554 "foo",
555 &ExternalDependencySpec {
556 library: Some("foo".into()),
557 header: None,
558 },
559 &config,
560 )
561 .unwrap();
562 }
563
564 #[tokio::test]
565 async fn test_fallback_detect_library_dirs() {
566 let temp = TempDir::new().unwrap();
567
568 let include_dir = temp.child("include");
569 include_dir.create_dir_all().unwrap();
570
571 let lib_dir = temp.child("lib");
572 lib_dir.create_dir_all().unwrap();
573
574 #[cfg(any(target_os = "linux", target_os = "android"))]
575 let lib = lib_dir.child("libfoo.so");
576 #[cfg(target_os = "macos")]
577 let lib = lib_dir.child("libfoo.dylib");
578 #[cfg(target_family = "windows")]
579 let lib = lib_dir.child("foo.dll");
580
581 lib.touch().unwrap();
582
583 let mut config = ExternalDependencySearchConfig::default();
584 config
585 .prefixes
586 .insert("FOO_INCDIR".into(), include_dir.path().to_path_buf());
587 config
588 .prefixes
589 .insert("FOO_LIBDIR".into(), lib_dir.path().to_path_buf());
590
591 ExternalDependencyInfo::fallback_probe(
592 "foo",
593 &ExternalDependencySpec {
594 library: Some("foo".into()),
595 header: None,
596 },
597 &config,
598 )
599 .unwrap();
600 }
601
602 #[tokio::test]
603 async fn test_fallback_detect_search_prefixes() {
604 let temp = TempDir::new().unwrap();
605 let prefix_dir = temp.child("usr");
606 let include_dir = prefix_dir.child("include");
607 let lib_dir = prefix_dir.child("lib");
608 include_dir.create_dir_all().unwrap();
609 lib_dir.create_dir_all().unwrap();
610
611 #[cfg(any(target_os = "linux", target_os = "android"))]
612 let lib = lib_dir.child("libfoo.so");
613 #[cfg(target_os = "macos")]
614 let lib = lib_dir.child("libfoo.dylib");
615 #[cfg(target_family = "windows")]
616 let lib = lib_dir.child("foo.dll");
617
618 lib.touch().unwrap();
619
620 let mut config = ExternalDependencySearchConfig::default();
621 config.search_prefixes.push(prefix_dir.path().to_path_buf());
622
623 ExternalDependencyInfo::fallback_probe(
624 "foo",
625 &ExternalDependencySpec {
626 library: Some("foo".into()),
627 header: None,
628 },
629 &config,
630 )
631 .unwrap();
632 }
633
634 #[tokio::test]
635 async fn test_fallback_detect_not_found() {
636 let config = ExternalDependencySearchConfig::default();
637
638 let result = ExternalDependencyInfo::fallback_probe(
639 "foo",
640 &ExternalDependencySpec {
641 header: Some("foo.h".into()),
642 library: None,
643 },
644 &config,
645 );
646
647 assert!(matches!(
648 result,
649 Err(ExternalDependencyError::HeaderNotFound { .. })
650 ));
651 }
652
653 #[cfg(not(target_env = "msvc"))]
654 #[tokio::test]
655 async fn test_to_lib_name() {
656 assert_eq!(to_lib_name(&PathBuf::from("lua.a")), "lua".to_string());
657 assert_eq!(
658 to_lib_name(&PathBuf::from("lua-5.1.a")),
659 "lua-5.1".to_string()
660 );
661 assert_eq!(
662 to_lib_name(&PathBuf::from("lua5.1.a")),
663 "lua5.1".to_string()
664 );
665 assert_eq!(to_lib_name(&PathBuf::from("lua51.a")), "lua51".to_string());
666 assert_eq!(
667 to_lib_name(&PathBuf::from("luajit-5.2.a")),
668 "luajit-5.2".to_string()
669 );
670 assert_eq!(
671 to_lib_name(&PathBuf::from("lua-5.2.a")),
672 "lua-5.2".to_string()
673 );
674 assert_eq!(to_lib_name(&PathBuf::from("liblua.a")), "lua".to_string());
675 assert_eq!(
676 to_lib_name(&PathBuf::from("liblua-5.1.a")),
677 "lua-5.1".to_string()
678 );
679 assert_eq!(
680 to_lib_name(&PathBuf::from("liblua53.a")),
681 "lua53".to_string()
682 );
683 assert_eq!(
684 to_lib_name(&PathBuf::from("liblua-54.a")),
685 "lua-54".to_string()
686 );
687 }
688}