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