Skip to main content

lux_lib/build/
external_dependency.rs

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    /// Name of the static library, (without the 'lib' prefix or file extension on unix targets),
38    /// for example, "foo" or "foo.dll"
39    pub(crate) lib_name: Option<String>,
40    /// pkg-config library information if available
41    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                // Search prefixes
140                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        // requires zlib to be in the nativeCheckInputs or dev environment
400        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        // requires zlib to be in the nativeCheckInputs or dev environment
415        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        // requires zlib to be in the nativeCheckInputs or dev environment
430        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        // requires zlib to be in the nativeCheckInputs or dev environment
445        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}