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