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