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