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 crate::{path, target};
17
18use crate::fs;
19
20#[derive(Error, Debug, Diagnostic)]
21#[non_exhaustive]
22pub enum Error {
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::Error),
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, Error> {
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(Error::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.to_lowercase(), "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, Error> {
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(Error::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(Error::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(Error::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(path::quoted)
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| {
405 file.extension()
406 .is_some_and(|ext| ext == target::c_lib_extension())
407 })
408 .filter(|file| {
409 file.file_name()
410 .is_some_and(|name| is_lib_name(&name.to_string_lossy(), prefix))
411 })
412 .collect_vec()
413 .first()
414 .cloned()
415 })
416 .map(|file| to_lib_name(&file))
417}
418
419fn is_lib_name(file_name: &str, prefix: &str) -> bool {
420 #[cfg(target_family = "unix")]
421 let file_name = file_name.trim_start_matches("lib");
422 file_name == format!("{}.{}", prefix, target::c_lib_extension())
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428 use assert_fs::{prelude::*, TempDir};
429
430 #[tokio::test]
431 async fn test_detect_zlib_pkg_config_header() {
432 let config = ExternalDependencySearchConfig::default();
434 ExternalDependencyInfo::probe(
435 "zlib",
436 &ExternalDependencySpec {
437 header: Some("zlib.h".into()),
438 library: None,
439 },
440 &config,
441 )
442 .unwrap();
443 }
444
445 #[tokio::test]
446 async fn test_detect_zlib_pkg_config_library_libz() {
447 let config = ExternalDependencySearchConfig::default();
449 ExternalDependencyInfo::probe(
450 "zlib",
451 &ExternalDependencySpec {
452 library: Some("libz".into()),
453 header: None,
454 },
455 &config,
456 )
457 .unwrap();
458 }
459
460 #[tokio::test]
461 async fn test_detect_zlib_pkg_config_library_z() {
462 let config = ExternalDependencySearchConfig::default();
464 ExternalDependencyInfo::probe(
465 "zlib",
466 &ExternalDependencySpec {
467 library: Some("z".into()),
468 header: None,
469 },
470 &config,
471 )
472 .unwrap();
473 }
474
475 #[tokio::test]
476 async fn test_detect_zlib_pkg_config_library_zlib() {
477 let config = ExternalDependencySearchConfig::default();
479 ExternalDependencyInfo::probe(
480 "zlib",
481 &ExternalDependencySpec {
482 library: Some("zlib".into()),
483 header: None,
484 },
485 &config,
486 )
487 .unwrap();
488 }
489
490 #[tokio::test]
491 async fn test_fallback_detect_header_prefix() {
492 let temp = TempDir::new().unwrap();
493 let prefix_dir = temp.child("usr");
494 let include_dir = prefix_dir.child("include");
495 include_dir.create_dir_all().unwrap();
496
497 let header = include_dir.child("foo.h");
498 header.touch().unwrap();
499
500 let mut config = ExternalDependencySearchConfig::default();
501 config
502 .prefixes
503 .insert("FOO_DIR".into(), prefix_dir.path().to_path_buf());
504
505 ExternalDependencyInfo::fallback_probe(
506 "foo",
507 &ExternalDependencySpec {
508 header: Some("foo.h".into()),
509 library: None,
510 },
511 &config,
512 )
513 .unwrap();
514 }
515
516 #[tokio::test]
517 async fn test_fallback_detect_header_prefix_incdir() {
518 let temp = TempDir::new().unwrap();
519 let include_dir = temp.child("include");
520 include_dir.create_dir_all().unwrap();
521
522 let header = include_dir.child("foo.h");
523 header.touch().unwrap();
524
525 let mut config = ExternalDependencySearchConfig::default();
526 config
527 .prefixes
528 .insert("FOO_INCDIR".into(), include_dir.path().to_path_buf());
529
530 ExternalDependencyInfo::fallback_probe(
531 "foo",
532 &ExternalDependencySpec {
533 header: Some("foo.h".into()),
534 library: None,
535 },
536 &config,
537 )
538 .unwrap();
539 }
540
541 #[tokio::test]
542 async fn test_fallback_detect_library_prefix() {
543 let temp = TempDir::new().unwrap();
544 let prefix_dir = temp.child("usr");
545 let include_dir = prefix_dir.child("include");
546 let lib_dir = prefix_dir.child("lib");
547 include_dir.create_dir_all().unwrap();
548 lib_dir.create_dir_all().unwrap();
549
550 #[cfg(any(target_os = "linux", target_os = "android"))]
551 let lib = lib_dir.child("libfoo.so");
552 #[cfg(target_os = "macos")]
553 let lib = lib_dir.child("libfoo.dylib");
554 #[cfg(target_family = "windows")]
555 let lib = lib_dir.child("foo.dll");
556
557 lib.touch().unwrap();
558
559 let mut config = ExternalDependencySearchConfig::default();
560 config
561 .prefixes
562 .insert("FOO_DIR".to_string(), prefix_dir.path().to_path_buf());
563
564 ExternalDependencyInfo::fallback_probe(
565 "foo",
566 &ExternalDependencySpec {
567 library: Some("foo".into()),
568 header: None,
569 },
570 &config,
571 )
572 .unwrap();
573 }
574
575 #[tokio::test]
576 async fn test_fallback_detect_library_dirs() {
577 let temp = TempDir::new().unwrap();
578
579 let include_dir = temp.child("include");
580 include_dir.create_dir_all().unwrap();
581
582 let lib_dir = temp.child("lib");
583 lib_dir.create_dir_all().unwrap();
584
585 #[cfg(any(target_os = "linux", target_os = "android"))]
586 let lib = lib_dir.child("libfoo.so");
587 #[cfg(target_os = "macos")]
588 let lib = lib_dir.child("libfoo.dylib");
589 #[cfg(target_family = "windows")]
590 let lib = lib_dir.child("foo.dll");
591
592 lib.touch().unwrap();
593
594 let mut config = ExternalDependencySearchConfig::default();
595 config
596 .prefixes
597 .insert("FOO_INCDIR".into(), include_dir.path().to_path_buf());
598 config
599 .prefixes
600 .insert("FOO_LIBDIR".into(), lib_dir.path().to_path_buf());
601
602 ExternalDependencyInfo::fallback_probe(
603 "foo",
604 &ExternalDependencySpec {
605 library: Some("foo".into()),
606 header: None,
607 },
608 &config,
609 )
610 .unwrap();
611 }
612
613 #[tokio::test]
614 async fn test_fallback_detect_search_prefixes() {
615 let temp = TempDir::new().unwrap();
616 let prefix_dir = temp.child("usr");
617 let include_dir = prefix_dir.child("include");
618 let lib_dir = prefix_dir.child("lib");
619 include_dir.create_dir_all().unwrap();
620 lib_dir.create_dir_all().unwrap();
621
622 #[cfg(any(target_os = "linux", target_os = "android"))]
623 let lib = lib_dir.child("libfoo.so");
624 #[cfg(target_os = "macos")]
625 let lib = lib_dir.child("libfoo.dylib");
626 #[cfg(target_family = "windows")]
627 let lib = lib_dir.child("foo.dll");
628
629 lib.touch().unwrap();
630
631 let mut config = ExternalDependencySearchConfig::default();
632 config.search_prefixes.push(prefix_dir.path().to_path_buf());
633
634 ExternalDependencyInfo::fallback_probe(
635 "foo",
636 &ExternalDependencySpec {
637 library: Some("foo".into()),
638 header: None,
639 },
640 &config,
641 )
642 .unwrap();
643 }
644
645 #[tokio::test]
646 async fn test_fallback_detect_not_found() {
647 let config = ExternalDependencySearchConfig::default();
648
649 let result = ExternalDependencyInfo::fallback_probe(
650 "foo",
651 &ExternalDependencySpec {
652 header: Some("foo.h".into()),
653 library: None,
654 },
655 &config,
656 );
657
658 assert!(matches!(result, Err(Error::HeaderNotFound { .. })));
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}