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