Skip to main content

system_deps/
lib.rs

1#![allow(clippy::needless_doctest_main)]
2#![allow(clippy::result_large_err)]
3//!`system-deps` lets you write system dependencies in `Cargo.toml` metadata,
4//! rather than programmatically in `build.rs`. This makes those dependencies
5//! declarative, so other tools can read them as well.
6//!
7//! # Usage
8//!
9//! In your `Cargo.toml`:
10//!
11//! ```toml
12//! [build-dependencies]
13//! system-deps = "7.0"
14//! ```
15//!
16//! Then, to declare a dependency on `testlib >= 1.2`
17//! add the following section:
18//!
19//! ```toml
20//! [package.metadata.system-deps]
21//! testlib = "1.2"
22//! ```
23//!
24//! Finally, in your `build.rs`, add:
25//!
26//! ```should_panic
27//! fn main() {
28//!     system_deps::Config::new().probe().unwrap();
29//! }
30//! ```
31//!
32//! # Version format
33//!
34//! Versions can be expressed in the following formats
35//!
36//!   * "1.2" or ">= 1.2": At least version 1.2
37//!   * ">= 1.2, < 2.0": At least version 1.2 but less than version 2.0
38//!
39//! In the future more complicated version expressions might be supported.
40//!
41//! Note that these versions are not interpreted according to the semver rules, but based on the
42//! rules defined by pkg-config.
43//!
44//! # Feature-specific dependency
45//! You can easily declare an optional system dependency by associating it with a feature:
46//!
47//! ```toml
48//! [package.metadata.system-deps]
49//! testdata = { version = "4.5", feature = "use-testdata" }
50//! ```
51//!
52//! `system-deps` will check for `testdata` only if the `use-testdata` feature has been enabled.
53//!
54//! # Optional dependency
55//!
56//! Another option is to use the `optional` setting, which can also be used using [features versions](#feature-versions):
57//!
58//! ```toml
59//! [package.metadata.system-deps]
60//! test-data = { version = "4.5", optional = true }
61//! testmore = { version = "2", v3 = { version = "3.0", optional = true }}
62//! ```
63//!
64//! `system-deps` will automatically export for each dependency a feature `system_deps_have_$DEP` where `$DEP`
65//! is the `toml` key defining the dependency in [snake_case](https://en.wikipedia.org/wiki/Snake_case).
66//! This can be used to check if an optional dependency has been found or not:
67//!
68//! ```
69//! #[cfg(system_deps_have_testdata)]
70//! println!("found test-data");
71//! ```
72//!
73//! # Overriding library name
74//! `toml` keys cannot contain dot characters so if your library name does, you can define it using the `name` field:
75//!
76//! ```toml
77//! [package.metadata.system-deps]
78//! glib = { name = "glib-2.0", version = "2.64" }
79//! ```
80//!
81//! # Fallback library names
82//!
83//! Some libraries may be available under different names on different platforms or distributions.
84//! To allow for this, you can define fallback names to search for if the main library name does not work.
85//!
86//! ```toml
87//! [package.metadata.system-deps]
88//! aravis = { fallback-names = ["aravis-0.8"] }
89//! ```
90//!
91//! You may also specify different fallback names for different versions:
92//!
93//! ```toml
94//! [package.metadata.system-deps.libfoo]
95//! version = "0.1"
96//! fallback-names = ["libfoo-0.1"]
97//! v1 = { version = "1.0", fallback-names = ["libfoo1"] }
98//! v2 = { version = "2.0", fallback-names = ["libfoo2"] }
99//! ```
100//!
101//! # Feature versions
102//!
103//! `-sys` crates willing to support various versions of their underlying system libraries
104//! can use features to control the version of the dependency required.
105//! `system-deps` will pick the highest version among enabled features.
106//! Such version features must use the pattern `v1_0`, `v1_2`, etc.
107//!
108//! ```toml
109//! [features]
110//! v1_2 = []
111//! v1_4 = ["v1_2"]
112//! v1_6 = ["v1_4"]
113//!
114//! [package.metadata.system-deps.libfoo_1_0]
115//! name = "libfoo-1.0"
116//! version = "1.0"
117//! v1_2 = { version = "1.2" }
118//! v1_4 = { version = "1.4" }
119//! v1_6 = { version = "1.6" }
120//! ```
121//!
122//! The same mechanism can be used to require a different library name depending on the version:
123//!
124//! ```toml
125//! [package.metadata.system-deps.libfoo_gl]
126//! name = "libfoo-gl-1.0"
127//! version = "1.14"
128//! v1_18 = { version = "1.18", name = "libfoo-gl-egl-1.0" }
129//! ```
130//!
131//! # Target specific dependencies
132//!
133//! You can define target specific dependencies:
134//!
135//! ```toml
136//! [package.metadata.system-deps.'cfg(target_os = "linux")']
137//! testdata = "1"
138//! [package.metadata.system-deps.'cfg(not(target_os = "macos"))']
139//! testlib = "1"
140//! [package.metadata.system-deps.'cfg(unix)']
141//! testanotherlib = { version = "1", optional = true }
142//! ```
143//!
144//! See [the Rust documentation](https://doc.rust-lang.org/reference/conditional-compilation.html)
145//! for the exact syntax.
146//! Currently, those keys are supported:
147//! - `target_arch`
148//! - `target_endian`
149//! - `target_env`
150//! - `target_family`
151//! - `target_os`
152//! - `target_pointer_width`
153//! - `target_vendor`
154//! - `unix` and `windows`
155//!
156//! # Overriding build flags
157//!
158//! By default `system-deps` automatically defines the required build flags for each dependency using the information fetched from `pkg-config`.
159//! These flags can be overridden using environment variables if needed:
160//!
161//! - `SYSTEM_DEPS_$NAME_SEARCH_NATIVE` to override the [`cargo:rustc-link-search=native`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorustc-link-searchkindpath) flag;
162//! - `SYSTEM_DEPS_$NAME_SEARCH_FRAMEWORK` to override the [`cargo:rustc-link-search=framework`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorustc-link-searchkindpath) flag;
163//! - `SYSTEM_DEPS_$NAME_LIB` to override the [`cargo:rustc-link-lib`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib) flag;
164//! - `SYSTEM_DEPS_$NAME_LIB_FRAMEWORK` to override the [`cargo:rustc-link-lib=framework`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib) flag;
165//! - `SYSTEM_DEPS_$NAME_INCLUDE` to override the [`cargo:include`](https://kornel.ski/rust-sys-crate#headers) flag.
166//!
167//! With `$NAME` being the upper case name of the key defining the dependency in `Cargo.toml`.
168//! For example `SYSTEM_DEPS_TESTLIB_SEARCH_NATIVE=/opt/lib` could be used to override a dependency named `testlib`.
169//!
170//! One can also define the environment variable `SYSTEM_DEPS_$NAME_NO_PKG_CONFIG` to fully disable `pkg-config` lookup
171//! for the given dependency. In this case at least SYSTEM_DEPS_$NAME_LIB or SYSTEM_DEPS_$NAME_LIB_FRAMEWORK should be defined as well.
172//!
173//! # Internally build system libraries
174//!
175//! `-sys` crates can provide support for building and statically link their underlying system library as part of their build process.
176//! Here is how to do this in your `build.rs`:
177//!
178//! ```should_panic
179//! fn main() {
180//!     system_deps::Config::new()
181//!         .add_build_internal("testlib", |lib, version| {
182//!             // Actually build the library here that fulfills the passed in version requirements
183//!             system_deps::Library::from_internal_pkg_config("build/path-to-pc-file", lib, "1.2.4")
184//!          })
185//!         .probe()
186//!         .unwrap();
187//! }
188//! ```
189//!
190//! This feature can be controlled using the `SYSTEM_DEPS_$NAME_BUILD_INTERNAL` environment variable
191//! which can have the following values:
192//!
193//! - `auto`: build the dependency only if the required version has not been found by `pkg-config`;
194//! - `always`: always build the dependency, ignoring any version which may be installed on the system;
195//! - `never`: (default) never build the dependency, `system-deps` will fail if the required version is not found on the system.
196//!
197//! You can also use the `SYSTEM_DEPS_BUILD_INTERNAL` environment variable with the same values
198//! defining the behavior for all the dependencies which don't have `SYSTEM_DEPS_$NAME_BUILD_INTERNAL` defined.
199//!
200//! # Static linking
201//!
202//! By default all libraries are dynamically linked, except when build internally as [described above](#internally-build-system-libraries).
203//! Libraries can be statically linked by defining the environment variable `SYSTEM_DEPS_$NAME_LINK=static`.
204//! You can also use `SYSTEM_DEPS_LINK=static` to statically link all the libraries.
205//!
206//! The libraries specified with this option can have any form readable by `pkg-config`, and they will inherit the main libraries'
207//! binary paths if you are using them. If `pkg-config` can't find some entry, it will print a warning but the compilation won't fail.
208//!
209//! # Using prebuilt binaries
210//!
211//! Some system libraries may take too long to build or require a specific environment. `system-deps` allows to download and link against
212//! prebuilt library binaries specified in the crate metadata. To do so, you need to enable the `binary` feature and configure the library metadata.
213//!
214//! ```toml
215//! [package.metadata.system-deps.liba]
216//! name = "liba"
217//! version = "1.0"
218//! url = "https://download/liba-1.0.tar.gz"
219//! checksum = "..."
220//! pkg_paths = [ "lib/pkgconfig" ]
221//! ```
222//!
223//! The snippet above will attempt to download the archive specified in the `url` field, extract it and add the relative paths from `pkg_paths` to the
224//! `PKG_CONFIG_PATH` when looking for `liba`. This is done automatically and dependents of the library don't need to make any changes.
225//! It is recommended to have a feature in the crate's `Cargo.toml` that enables the `binary` feature in `system-deps`, instead of hard-coding it.
226//!
227//! ```toml
228//! [features]
229//! binary = [ "system-deps/binary", "system-deps/gz" ]
230//! ```
231//!
232//! As oppossed to the other metadata in `system-deps`, the metadata section can be specified anywhere in the crate tree, with entries from top level crates having priority.
233//! This allows for a crate to provide a default value for its binaries, and a dependent crate to add extra configuration.
234//!
235//! ```toml
236//! # Crate graph: user_project -> libb -> liba
237//!
238//! # libb/Cargo.toml
239//! [package.metadata.system-deps.liba]
240//! url = "https://download/custom-liba-1.0.tar.gz"
241//!
242//! # user_project/Cargo.toml
243//! [package.metadata.system-deps.liba]
244//! url = "file:///tmp/liba"
245//! ```
246//!
247//! In this example, `libb` overwrites the binaries provided by `liba` (for compatibility reasons, to add flags needed by `libb`, to use a single package for both...).
248//! However, the user project overwrites them again to point at a local file for development.
249//!
250//! The binaries can be configured per target like other `system-deps` options:
251//!
252//! ```toml
253//! [package.metadata.system-deps.liba.'cfg(target = "unix")']
254//! url = "https://download/liba-unix-1.0.tar.gz"
255//!
256//! [package.metadata.system-deps.liba.'cfg(target = "windows")']
257//! url = "https://download/liba-windows-1.0.zip"
258//! ```
259//!
260//! By default, a binary archive adds its paths to `PKG_CONFIG_PATH` only for the library it is defined for. However, sometimes you may want to share a single url
261//! for multiple libraries. While it is possible to repeat the url for every entry, a more concise approach is to use `follows` to copy the configuration from another library.
262//!
263//! ```toml
264//! [package.metadata.system-deps.libb]
265//! follows = "liba" # This name corresponds to the key of the metadata table
266//! ```
267//!
268
269#![deny(missing_docs)]
270
271#[cfg(test)]
272mod test;
273
274use heck::{ToShoutySnakeCase, ToSnakeCase};
275use std::{
276    borrow::Borrow,
277    collections::{BTreeMap, HashMap},
278    env,
279    ffi::OsString,
280    fmt, iter,
281    ops::RangeBounds,
282    path::{Path, PathBuf},
283    str::FromStr,
284};
285
286mod metadata;
287use metadata::MetaData;
288
289#[cfg(all(test, feature = "binary"))]
290mod test_binary;
291
292/// system-deps errors
293#[derive(Debug)]
294pub enum Error {
295    /// pkg-config error
296    PkgConfig(pkg_config::Error),
297    /// One of the `Config::add_build_internal` closures failed
298    BuildInternalClosureError(String, BuildInternalClosureError),
299    /// Failed to read `Cargo.toml`
300    FailToRead(String, std::io::Error),
301    /// Raised when an error is detected in the metadata defined in `Cargo.toml`
302    InvalidMetadata(String),
303    /// Raised when dependency defined manually using `SYSTEM_DEPS_$NAME_NO_PKG_CONFIG`
304    /// did not define at least one lib using `SYSTEM_DEPS_$NAME_LIB` or
305    /// `SYSTEM_DEPS_$NAME_LIB_FRAMEWORK`
306    MissingLib(String),
307    /// An environment variable in the form of `SYSTEM_DEPS_$NAME_BUILD_INTERNAL`
308    /// contained an invalid value (allowed: `auto`, `always`, `never`)
309    BuildInternalInvalid(String),
310    /// system-deps has been asked to internally build a lib, through
311    /// `SYSTEM_DEPS_$NAME_BUILD_INTERNAL=always' or `SYSTEM_DEPS_$NAME_BUILD_INTERNAL=auto',
312    /// but not closure has been defined using `Config::add_build_internal` to build
313    /// this lib
314    BuildInternalNoClosure(String, String),
315    /// The library which has been build internally does not match the
316    /// required version defined in `Cargo.toml`
317    BuildInternalWrongVersion(String, String, String),
318    /// The `cfg()` expression used in `Cargo.toml` is currently not supported
319    UnsupportedCfg(String),
320}
321
322impl From<pkg_config::Error> for Error {
323    fn from(err: pkg_config::Error) -> Self {
324        Self::PkgConfig(err)
325    }
326}
327
328impl std::error::Error for Error {
329    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
330        match self {
331            Self::PkgConfig(e) => Some(e),
332            Self::BuildInternalClosureError(_, e) => Some(e),
333            Self::FailToRead(_, e) => Some(e),
334            _ => None,
335        }
336    }
337}
338
339impl fmt::Display for Error {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        match self {
342            Self::PkgConfig(e) => write!(f, "{e}"),
343            Self::BuildInternalClosureError(s, e) => write!(f, "Failed to build {s}: {e}"),
344            Self::FailToRead(s, _) => write!(f, "{s}"),
345            Self::InvalidMetadata(s) => write!(f, "{s}"),
346            Self::MissingLib(s) => write!(
347                f,
348                "You should define at least one lib using {} or {}",
349                EnvVariable::new_lib(s),
350                EnvVariable::new_lib_framework(s),
351            ),
352            Self::BuildInternalInvalid(s) => write!(f, "{s}"),
353            Self::BuildInternalNoClosure(s1, s2) => {
354                write!(f, "Missing build internal closure for {s1} (version {s2})")
355            }
356            Self::BuildInternalWrongVersion(s1, s2, s3) => write!(
357                f,
358                "Internally built {s1} {s2} but minimum required version is {s3}"
359            ),
360            Self::UnsupportedCfg(s) => write!(f, "Unsupported cfg() expression: {s}"),
361        }
362    }
363}
364
365#[derive(Debug, Default)]
366/// All the system dependencies retrieved by [`Config::probe`].
367pub struct Dependencies {
368    libs: BTreeMap<String, Library>,
369}
370
371impl Dependencies {
372    /// Retrieve details about a system dependency.
373    ///
374    /// # Arguments
375    ///
376    /// * `name`: the name of the `toml` key defining the dependency in `Cargo.toml`
377    pub fn get_by_name(&self, name: &str) -> Option<&Library> {
378        self.libs.get(name)
379    }
380
381    /// A vector listing all system dependencies in sorted (for build reproducibility) order.
382    /// The first element of the tuple is the name of the `toml` key defining the
383    /// dependency in `Cargo.toml`.
384    pub fn iter(&self) -> Vec<(&str, &Library)> {
385        let mut v = self
386            .libs
387            .iter()
388            .map(|(k, v)| (k.as_str(), v))
389            .collect::<Vec<_>>();
390        v.sort_by_key(|x| x.0);
391        v
392    }
393
394    fn aggregate_str<F: Fn(&Library) -> &Vec<String>>(&self, getter: F) -> Vec<&str> {
395        let mut v = self
396            .libs
397            .values()
398            .flat_map(getter)
399            .map(|s| s.as_str())
400            .collect::<Vec<_>>();
401        v.sort_unstable();
402        v.dedup();
403        v
404    }
405
406    fn aggregate_path_buf<F: Fn(&Library) -> &Vec<PathBuf>>(&self, getter: F) -> Vec<&PathBuf> {
407        let mut v = self.libs.values().flat_map(getter).collect::<Vec<_>>();
408        v.sort();
409        v.dedup();
410        v
411    }
412
413    /// Returns a vector of [`Library::libs`] of each library, removing duplicates.
414    pub fn all_libs(&self) -> Vec<&str> {
415        let mut v = self
416            .libs
417            .values()
418            .flat_map(|l| l.libs.iter().map(|lib| lib.name.as_str()))
419            .collect::<Vec<_>>();
420        v.sort_unstable();
421        v.dedup();
422        v
423    }
424
425    /// Returns a vector of [`Library::link_paths`] of each library, removing duplicates.
426    pub fn all_link_paths(&self) -> Vec<&PathBuf> {
427        self.aggregate_path_buf(|l| &l.link_paths)
428    }
429
430    /// Returns a vector of [`Library::frameworks`] of each library, removing duplicates.
431    pub fn all_frameworks(&self) -> Vec<&str> {
432        self.aggregate_str(|l| &l.frameworks)
433    }
434
435    /// Returns a vector of [`Library::framework_paths`] of each library, removing duplicates.
436    pub fn all_framework_paths(&self) -> Vec<&PathBuf> {
437        self.aggregate_path_buf(|l| &l.framework_paths)
438    }
439
440    /// Returns a vector of [`Library::include_paths`] of each library, removing duplicates.
441    pub fn all_include_paths(&self) -> Vec<&PathBuf> {
442        self.aggregate_path_buf(|l| &l.include_paths)
443    }
444
445    /// Returns a vector of [`Library::ld_args`] of each library, removing duplicates.
446    pub fn all_linker_args(&self) -> Vec<&Vec<String>> {
447        let mut v = self
448            .libs
449            .values()
450            .flat_map(|l| &l.ld_args)
451            .collect::<Vec<_>>();
452        v.sort_unstable();
453        v.dedup();
454        v
455    }
456
457    /// Returns a vector of [`Library::defines`] of each library, removing duplicates.
458    pub fn all_defines(&self) -> Vec<(&str, &Option<String>)> {
459        let mut v = self
460            .libs
461            .values()
462            .flat_map(|l| l.defines.iter())
463            .map(|(k, v)| (k.as_str(), v))
464            .collect::<Vec<_>>();
465        v.sort();
466        v.dedup();
467        v
468    }
469
470    fn add(&mut self, name: &str, lib: Library) {
471        self.libs.insert(name.to_string(), lib);
472    }
473
474    fn override_from_flags(&mut self, env: &EnvVariables) {
475        for (name, lib) in self.libs.iter_mut() {
476            if let Some(value) = env.get(&EnvVariable::new_search_native(name)) {
477                lib.link_paths = split_paths(&value);
478            }
479            if let Some(value) = env.get(&EnvVariable::new_search_framework(name)) {
480                lib.framework_paths = split_paths(&value);
481            }
482            if let Some(value) = env.get(&EnvVariable::new_lib(name)) {
483                let should_be_linked_statically = env
484                    .has_value(&EnvVariable::new_link(Some(name)), "static")
485                    || env.has_value(&EnvVariable::new_link(None), "static");
486
487                // If somebody manually mandates static linking, that is a
488                // clear intent. Let's just assume that a static lib is
489                // available and let the linking fail if the user is wrong.
490                let is_static_lib_available = should_be_linked_statically;
491
492                lib.libs = split_string(&value)
493                    .into_iter()
494                    .map(|l| InternalLib::new(l, is_static_lib_available))
495                    .collect();
496            }
497            if let Some(value) = env.get(&EnvVariable::new_lib_framework(name)) {
498                lib.frameworks = split_string(&value);
499            }
500            if let Some(value) = env.get(&EnvVariable::new_include(name)) {
501                lib.include_paths = split_paths(&value);
502            }
503            if let Some(value) = env.get(&EnvVariable::new_linker_args(name)) {
504                lib.ld_args = split_string(&value)
505                    .into_iter()
506                    .map(|l| l.split(',').map(|l| l.to_string()).collect())
507                    .collect();
508            }
509        }
510    }
511
512    /// Generate cargo build flags for all probed libraries.
513    ///
514    /// When `target` is `Some`, absolute library paths in `Library::link_files` are converted into
515    /// `rustc-link-search` + `rustc-link-lib`, using the target triple to strip the platform-
516    /// specific prefix and suffix. When `None`, `link_files` is ignored.
517    ///
518    /// Primarily used when cross compiling and linking libraries via absolute paths.
519    fn gen_flags(&self, target: Option<&str>) -> Result<BuildFlags, Error> {
520        let mut flags = BuildFlags::new();
521        let mut include_paths = Vec::new();
522
523        for (name, lib) in self.iter() {
524            include_paths.extend(lib.include_paths.clone());
525
526            if lib.source == Source::EnvVariables
527                && lib.libs.is_empty()
528                && lib.frameworks.is_empty()
529            {
530                return Err(Error::MissingLib(name.to_string()));
531            }
532
533            lib.link_paths
534                .iter()
535                .for_each(|l| flags.add(BuildFlag::SearchNative(l.to_string_lossy().to_string())));
536            lib.framework_paths.iter().for_each(|f| {
537                flags.add(BuildFlag::SearchFramework(f.to_string_lossy().to_string()))
538            });
539            lib.libs.iter().for_each(|l| {
540                flags.add(BuildFlag::Lib(
541                    l.name.clone(),
542                    lib.statik && l.is_static_available,
543                ))
544            });
545
546            // Convert absolute paths in pkg-config's `Libs:` into `rustc-link-search=native` +
547            // `rustc-link-lib` directives. The target triple determines which prefix and suffix to
548            // strip.
549            if let Some(target) = target {
550                let mut seen_dirs = Vec::new();
551
552                for path in &lib.link_files {
553                    if let (Some(dir), Some(file_name)) = (path.parent(), path.file_name()) {
554                        let filename = file_name.to_string_lossy();
555                        let mut new = false;
556
557                        if !seen_dirs.contains(&dir) {
558                            new = true;
559                            seen_dirs.push(dir);
560                        }
561
562                        // Try to extract the library linking name from the filename, e.g.
563                        // libboost_context.a -> boost_context
564                        if let Some(lib_name) =
565                            pkg_config::Library::extract_lib_from_filename(target, &filename)
566                        {
567                            if new {
568                                flags.add(BuildFlag::SearchNative(
569                                    dir.to_string_lossy().to_string(),
570                                ));
571                            }
572
573                            flags.add(BuildFlag::Lib(lib_name.to_string(), lib.statik));
574                        }
575                    }
576                }
577            }
578
579            lib.frameworks
580                .iter()
581                .for_each(|f| flags.add(BuildFlag::LibFramework(f.clone())));
582            lib.ld_args
583                .iter()
584                .for_each(|f| flags.add(BuildFlag::LinkArg(f.clone())))
585        }
586
587        // Export DEP_$CRATE_INCLUDE env variable with the headers paths,
588        // see https://kornel.ski/rust-sys-crate#headers
589        if !include_paths.is_empty() {
590            if let Ok(paths) = std::env::join_paths(include_paths) {
591                flags.add(BuildFlag::Include(paths.to_string_lossy().to_string()));
592            }
593        }
594
595        // Export cargo:rerun-if-env-changed instructions for all env variables affecting system-deps behaviour
596        flags.add(BuildFlag::RerunIfEnvChanged(
597            EnvVariable::new_build_internal(None),
598        ));
599        flags.add(BuildFlag::RerunIfEnvChanged(EnvVariable::new_link(None)));
600
601        for name in self.libs.keys() {
602            EnvVariable::set_rerun_if_changed_for_all_variants(&mut flags, name);
603        }
604
605        Ok(flags)
606    }
607}
608
609#[derive(Debug)]
610/// Error used in return value of `Config::add_build_internal` closures
611pub enum BuildInternalClosureError {
612    /// `pkg-config` error
613    PkgConfig(pkg_config::Error),
614    /// General failure
615    Failed(String),
616}
617
618impl From<pkg_config::Error> for BuildInternalClosureError {
619    fn from(err: pkg_config::Error) -> Self {
620        Self::PkgConfig(err)
621    }
622}
623
624impl BuildInternalClosureError {
625    /// Create a new `BuildInternalClosureError::Failed` representing a general
626    /// failure.
627    ///
628    /// # Arguments
629    ///
630    /// * `details`: human-readable details about the failure
631    pub fn failed(details: &str) -> Self {
632        Self::Failed(details.to_string())
633    }
634}
635
636impl std::error::Error for BuildInternalClosureError {
637    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
638        match self {
639            Self::PkgConfig(e) => Some(e),
640            _ => None,
641        }
642    }
643}
644
645impl fmt::Display for BuildInternalClosureError {
646    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
647        match self {
648            Self::PkgConfig(e) => write!(f, "{e}"),
649            Self::Failed(s) => write!(f, "{s}"),
650        }
651    }
652}
653
654// Enum representing the environment variables user can define to tune system-deps.
655#[derive(Debug, PartialEq)]
656enum EnvVariable {
657    Lib(String),
658    LibFramework(String),
659    SearchNative(String),
660    SearchFramework(String),
661    Include(String),
662    NoPkgConfig(String),
663    BuildInternal(Option<String>),
664    Link(Option<String>),
665    LinkerArgs(String),
666    NoPrebuilt(Option<String>),
667}
668
669impl EnvVariable {
670    fn new_lib(lib: &str) -> Self {
671        Self::Lib(lib.to_string())
672    }
673
674    fn new_lib_framework(lib: &str) -> Self {
675        Self::LibFramework(lib.to_string())
676    }
677
678    fn new_search_native(lib: &str) -> Self {
679        Self::SearchNative(lib.to_string())
680    }
681
682    fn new_search_framework(lib: &str) -> Self {
683        Self::SearchFramework(lib.to_string())
684    }
685
686    fn new_include(lib: &str) -> Self {
687        Self::Include(lib.to_string())
688    }
689
690    fn new_linker_args(lib: &str) -> Self {
691        Self::LinkerArgs(lib.to_string())
692    }
693
694    fn new_no_pkg_config(lib: &str) -> Self {
695        Self::NoPkgConfig(lib.to_string())
696    }
697
698    fn new_build_internal(lib: Option<&str>) -> Self {
699        Self::BuildInternal(lib.map(|l| l.to_string()))
700    }
701
702    fn new_link(lib: Option<&str>) -> Self {
703        Self::Link(lib.map(|l| l.to_string()))
704    }
705
706    fn new_no_prebuilt(lib: Option<&str>) -> Self {
707        Self::NoPrebuilt(lib.map(|l| l.to_string()))
708    }
709
710    const fn suffix(&self) -> &'static str {
711        match self {
712            EnvVariable::Lib(_) => "LIB",
713            EnvVariable::LibFramework(_) => "LIB_FRAMEWORK",
714            EnvVariable::SearchNative(_) => "SEARCH_NATIVE",
715            EnvVariable::SearchFramework(_) => "SEARCH_FRAMEWORK",
716            EnvVariable::Include(_) => "INCLUDE",
717            EnvVariable::NoPkgConfig(_) => "NO_PKG_CONFIG",
718            EnvVariable::BuildInternal(_) => "BUILD_INTERNAL",
719            EnvVariable::Link(_) => "LINK",
720            EnvVariable::LinkerArgs(_) => "LDFLAGS",
721            EnvVariable::NoPrebuilt(_) => "NO_PREBUILT",
722        }
723    }
724
725    fn set_rerun_if_changed_for_all_variants(flags: &mut BuildFlags, name: &str) {
726        #[inline]
727        fn add_to_flags(flags: &mut BuildFlags, var: EnvVariable) {
728            flags.add(BuildFlag::RerunIfEnvChanged(var));
729        }
730        add_to_flags(flags, EnvVariable::new_lib(name));
731        add_to_flags(flags, EnvVariable::new_lib_framework(name));
732        add_to_flags(flags, EnvVariable::new_search_native(name));
733        add_to_flags(flags, EnvVariable::new_search_framework(name));
734        add_to_flags(flags, EnvVariable::new_include(name));
735        add_to_flags(flags, EnvVariable::new_linker_args(name));
736        add_to_flags(flags, EnvVariable::new_no_pkg_config(name));
737        add_to_flags(flags, EnvVariable::new_build_internal(Some(name)));
738        add_to_flags(flags, EnvVariable::new_link(Some(name)));
739        add_to_flags(flags, EnvVariable::new_no_prebuilt(Some(name)));
740    }
741}
742
743impl fmt::Display for EnvVariable {
744    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
745        let suffix = match self {
746            EnvVariable::Lib(lib)
747            | EnvVariable::LibFramework(lib)
748            | EnvVariable::SearchNative(lib)
749            | EnvVariable::SearchFramework(lib)
750            | EnvVariable::Include(lib)
751            | EnvVariable::LinkerArgs(lib)
752            | EnvVariable::NoPkgConfig(lib)
753            | EnvVariable::BuildInternal(Some(lib))
754            | EnvVariable::Link(Some(lib))
755            | EnvVariable::NoPrebuilt(Some(lib)) => {
756                format!("{}_{}", lib.to_shouty_snake_case(), self.suffix())
757            }
758            EnvVariable::BuildInternal(None)
759            | EnvVariable::Link(None)
760            | EnvVariable::NoPrebuilt(None) => self.suffix().to_string(),
761        };
762        write!(f, "SYSTEM_DEPS_{suffix}")
763    }
764}
765
766type FnBuildInternal =
767    dyn FnOnce(&str, &str) -> std::result::Result<Library, BuildInternalClosureError>;
768
769/// Structure used to configure `metadata` before starting to probe for dependencies
770pub struct Config {
771    env: EnvVariables,
772    build_internals: HashMap<String, Box<FnBuildInternal>>,
773    #[cfg(feature = "binary")]
774    paths: &'static system_deps_meta::binary::Paths,
775}
776
777impl Default for Config {
778    fn default() -> Self {
779        Self::new_with_env(EnvVariables::Environment)
780    }
781}
782
783impl Config {
784    /// Create a new set of configuration
785    pub fn new() -> Self {
786        Self::default()
787    }
788
789    fn new_with_env(env: EnvVariables) -> Self {
790        let me = Self {
791            env,
792            build_internals: HashMap::new(),
793            #[cfg(feature = "binary")]
794            paths: {
795                // Constructed by reading the serialized value saved by the build script
796                const CONTENT: &str = include_str!(env!("SYSTEM_DEPS_BINARY_PATHS"));
797                static PATHS: std::sync::OnceLock<system_deps_meta::binary::Paths> =
798                    std::sync::OnceLock::new();
799                PATHS.get_or_init(|| {
800                    toml::from_str(CONTENT)
801                        .expect("The build script should output valid serialization")
802                })
803            },
804        };
805
806        // Prefer the pkg-config shipped by a downloaded bundle. The bundle's
807        // `.pc` files (absolute paths, prefix expectations, MSVC quirks on
808        // Windows) are only guaranteed to be readable by the pkg-config that
809        // ships with it; system pkg-configs in `PATH` are often misconfigured
810        // (Strawberry Perl, MSYS2, chocolatey, distro forks) and silently
811        // produce wrong flags. Only an explicit `PKG_CONFIG` env var
812        // overrides this.
813        #[cfg(feature = "binary")]
814        if env::var_os("PKG_CONFIG").is_none() {
815            if let Some(pc) = me.paths.pkg_config_binary() {
816                env::set_var("PKG_CONFIG", pc);
817            }
818        }
819
820        me
821    }
822
823    /// Probe all libraries configured in the Cargo.toml
824    /// `[package.metadata.system-deps]` section.
825    ///
826    /// The returned hash is using the `toml` key defining the dependency as key.
827    pub fn probe(self) -> Result<Dependencies, Error> {
828        let target = self.env.get("TARGET");
829        let libraries = self.probe_full()?;
830        let flags = libraries.gen_flags(target.as_deref())?;
831
832        // Output cargo flags
833        println!("{flags}");
834
835        for (name, _) in libraries.iter() {
836            println!("cargo:rustc-cfg=system_deps_have_{}", name.to_snake_case());
837        }
838
839        Ok(libraries)
840    }
841
842    /// Add hook so system-deps can internally build library `name` if requested by user.
843    ///
844    /// It will only be triggered if the environment variable
845    /// `SYSTEM_DEPS_$NAME_BUILD_INTERNAL` is defined with either `always` or
846    /// `auto` as value. In the latter case, `func` is called only if the requested
847    /// version of the library was not found on the system.
848    ///
849    /// # Arguments
850    /// * `name`: the name of the library, as defined in `Cargo.toml`
851    /// * `func`: closure called when internally building the library.
852    ///
853    /// It receives as argument the library name, and the minimum version required.
854    pub fn add_build_internal<F>(mut self, name: &str, func: F) -> Self
855    where
856        F: 'static + FnOnce(&str, &str) -> std::result::Result<Library, BuildInternalClosureError>,
857    {
858        let mut build_internals = self.build_internals;
859        build_internals.insert(name.to_string(), Box::new(func));
860
861        self.build_internals = build_internals;
862        self
863    }
864
865    /// Checks the map from packages to their provided prebuilt binaries locations, if available.
866    pub fn query_path(&self, _pkg: &str) -> Option<&'static Vec<PathBuf>> {
867        #[cfg(not(feature = "binary"))]
868        return None;
869
870        #[cfg(feature = "binary")]
871        self.env
872            .get(&EnvVariable::new_no_prebuilt(Some(_pkg)))
873            .or(self.env.get(&EnvVariable::new_no_prebuilt(None)))
874            .map_or_else(|| self.paths.get(_pkg), |_| None)
875    }
876
877    fn probe_full(mut self) -> Result<Dependencies, Error> {
878        let mut libraries = self.probe_pkg_config()?;
879        libraries.override_from_flags(&self.env);
880        Ok(libraries)
881    }
882
883    fn probe_pkg_config(&mut self) -> Result<Dependencies, Error> {
884        let dir = self
885            .env
886            .get("CARGO_MANIFEST_DIR")
887            .ok_or_else(|| Error::InvalidMetadata("$CARGO_MANIFEST_DIR not set".into()))?;
888        let mut path = PathBuf::from(dir);
889        path.push("Cargo.toml");
890
891        println!("cargo:rerun-if-changed={}", path.to_string_lossy());
892
893        let metadata = MetaData::from_file(&path)?;
894        let mut libraries = Dependencies::default();
895
896        // Collect keys that have an unconditional (non-cfg) dep so we can skip
897        // cfg-gated duplicates that only carry binary metadata.
898        let unconditional_keys: std::collections::HashSet<_> = metadata
899            .deps
900            .iter()
901            .filter(|d| d.cfg.is_none())
902            .map(|d| &d.key)
903            .collect();
904
905        for dep in metadata.deps.iter() {
906            if let Some(cfg) = &dep.cfg {
907                // Check if `cfg()` expression matches the target settings
908                if !self.check_cfg(cfg)? {
909                    continue;
910                }
911                // Skip cfg-gated deps whose key also has an unconditional entry.
912                // The cfg-gated entry typically only carries binary-related metadata
913                // (url, checksum, etc.) handled by system-deps-meta, not here.
914                if unconditional_keys.contains(&dep.key) {
915                    continue;
916                }
917            }
918
919            let mut enabled_feature_overrides = Vec::new();
920
921            for o in dep.version_overrides.iter() {
922                if self.has_feature(&o.key) {
923                    enabled_feature_overrides.push(o);
924                }
925            }
926
927            if let Some(feature) = dep.feature.as_ref() {
928                if !self.has_feature(feature) {
929                    continue;
930                }
931            }
932
933            // Pick the highest feature enabled version
934            let version;
935            let lib_name;
936            let fallback_lib_names;
937            let optional;
938            if enabled_feature_overrides.is_empty() {
939                version = dep.version.as_deref();
940                lib_name = dep.lib_name();
941                fallback_lib_names = dep.fallback_names.as_deref().unwrap_or(&[]);
942                optional = dep.optional;
943            } else {
944                enabled_feature_overrides.sort_by(|a, b| {
945                    fn min_version(r: metadata::VersionRange<'_>) -> &str {
946                        match r.start_bound() {
947                            std::ops::Bound::Unbounded => unreachable!(),
948                            std::ops::Bound::Excluded(_) => unreachable!(),
949                            std::ops::Bound::Included(b) => b,
950                        }
951                    }
952
953                    let a = min_version(metadata::parse_version(&a.version));
954                    let b = min_version(metadata::parse_version(&b.version));
955
956                    version_compare::compare(a, b)
957                        .expect("failed to compare versions")
958                        .ord()
959                        .expect("invalid version")
960                });
961                let highest = enabled_feature_overrides.into_iter().next_back().unwrap();
962
963                version = Some(highest.version.as_str());
964                lib_name = highest.name.as_deref().unwrap_or(dep.lib_name());
965                fallback_lib_names = highest
966                    .fallback_names
967                    .as_deref()
968                    .or(dep.fallback_names.as_deref())
969                    .unwrap_or(&[]);
970                optional = highest.optional.unwrap_or(dep.optional);
971            };
972
973            let version = version.ok_or_else(|| {
974                Error::InvalidMetadata(format!("No version defined for {}", dep.key))
975            })?;
976
977            let name = &dep.key;
978            let build_internal = self.get_build_internal_status(name)?;
979
980            // `Some` only for a dep served by a downloaded binary bundle.
981            // TODO: Pass package version here
982            let prebuilt_pkg_config_paths = self.query_path(name);
983
984            // should the lib be statically linked?
985            let statik = cfg!(feature = "binary")
986                || self
987                    .env
988                    .has_value(&EnvVariable::new_link(Some(name)), "static")
989                || self.env.has_value(&EnvVariable::new_link(None), "static");
990
991            let mut library = if self.env.contains(&EnvVariable::new_no_pkg_config(name)) {
992                Library::from_env_variables(name)
993            } else if build_internal == BuildInternal::Always {
994                self.call_build_internal(lib_name, version)?
995            } else {
996                let mut config = pkg_config::Config::new();
997                config
998                    .print_system_libs(false)
999                    .cargo_metadata(false)
1000                    .range_version(metadata::parse_version(version))
1001                    .statik(statik);
1002
1003                // Bundles ship pkg-config 0.29.2, whose `--define-prefix` (on by
1004                // default on Windows) miscomputes `prefix` for nested layouts
1005                // like `lib/gstreamer-1.0/pkgconfig`, inflating `${libdir}` to
1006                // `<bundle>/lib/lib`. The bundled `.pc` carry correct
1007                // `prefix=${pcfiledir}/...`, so disable it, but only when the
1008                // tool accepts the flag (older ones lack the behaviour anyway).
1009                #[cfg(windows)]
1010                if prebuilt_pkg_config_paths.is_some() && pkg_config_accepts_dont_define_prefix() {
1011                    config.arg("--dont-define-prefix");
1012                }
1013
1014                let probe = Library::wrap_pkg_config(prebuilt_pkg_config_paths, || {
1015                    Self::probe_with_fallback(&config, lib_name, fallback_lib_names)
1016                });
1017
1018                match probe {
1019                    Ok((lib_name, lib)) => Library::from_pkg_config(lib_name, lib),
1020                    Err(e) => {
1021                        if build_internal == BuildInternal::Auto {
1022                            // Try building the lib internally as a fallback
1023                            self.call_build_internal(name, version)?
1024                        } else if optional {
1025                            // If the dep is optional just skip it
1026                            continue;
1027                        } else {
1028                            return Err(e.into());
1029                        }
1030                    }
1031                }
1032            };
1033
1034            library.statik = statik;
1035
1036            libraries.add(name, library);
1037        }
1038
1039        Ok(libraries)
1040    }
1041
1042    fn probe_with_fallback<'a>(
1043        config: &'a pkg_config::Config,
1044        name: &'a str,
1045        fallback_names: &'a [String],
1046    ) -> Result<(&'a str, pkg_config::Library), pkg_config::Error> {
1047        let error = match config.probe(name) {
1048            Ok(x) => return Ok((name, x)),
1049            Err(e) => e,
1050        };
1051        for name in fallback_names {
1052            if let Ok(library) = config.probe(name) {
1053                return Ok((name, library));
1054            }
1055        }
1056        Err(error)
1057    }
1058
1059    fn get_build_internal_env_var(&self, var: EnvVariable) -> Result<Option<BuildInternal>, Error> {
1060        match self.env.get(&var).as_deref() {
1061            Some(s) => {
1062                let b = BuildInternal::from_str(s).map_err(|_| {
1063                    Error::BuildInternalInvalid(format!(
1064                        "Invalid value in {var}: {s} (allowed: 'auto', 'always', 'never')"
1065                    ))
1066                })?;
1067                Ok(Some(b))
1068            }
1069            None => Ok(None),
1070        }
1071    }
1072
1073    fn get_build_internal_status(&self, name: &str) -> Result<BuildInternal, Error> {
1074        match self.get_build_internal_env_var(EnvVariable::new_build_internal(Some(name)))? {
1075            Some(b) => Ok(b),
1076            None => Ok(self
1077                .get_build_internal_env_var(EnvVariable::new_build_internal(None))?
1078                .unwrap_or_default()),
1079        }
1080    }
1081
1082    fn call_build_internal(&mut self, name: &str, version_str: &str) -> Result<Library, Error> {
1083        let lib = match self.build_internals.remove(name) {
1084            Some(f) => f(name, version_str)
1085                .map_err(|e| Error::BuildInternalClosureError(name.into(), e))?,
1086            None => {
1087                return Err(Error::BuildInternalNoClosure(
1088                    name.into(),
1089                    version_str.into(),
1090                ))
1091            }
1092        };
1093
1094        // Check that the lib built internally matches the required version
1095        let version = metadata::parse_version(version_str);
1096        fn min_version(r: metadata::VersionRange<'_>) -> &str {
1097            match r.start_bound() {
1098                std::ops::Bound::Unbounded => unreachable!(),
1099                std::ops::Bound::Excluded(_) => unreachable!(),
1100                std::ops::Bound::Included(b) => b,
1101            }
1102        }
1103        fn max_version(r: metadata::VersionRange<'_>) -> Option<&str> {
1104            match r.end_bound() {
1105                std::ops::Bound::Included(_) => unreachable!(),
1106                std::ops::Bound::Unbounded => None,
1107                std::ops::Bound::Excluded(b) => Some(*b),
1108            }
1109        }
1110
1111        let min = min_version(version.clone());
1112        if version_compare::compare(&lib.version, min) == Ok(version_compare::Cmp::Lt) {
1113            return Err(Error::BuildInternalWrongVersion(
1114                name.into(),
1115                lib.version,
1116                version_str.into(),
1117            ));
1118        }
1119
1120        if let Some(max) = max_version(version) {
1121            if version_compare::compare(&lib.version, max) == Ok(version_compare::Cmp::Ge) {
1122                return Err(Error::BuildInternalWrongVersion(
1123                    name.into(),
1124                    lib.version,
1125                    version_str.into(),
1126                ));
1127            }
1128        }
1129
1130        Ok(lib)
1131    }
1132
1133    fn has_feature(&self, feature: &str) -> bool {
1134        let var: &str = &format!("CARGO_FEATURE_{}", feature.to_uppercase().replace('-', "_"));
1135        self.env.contains(var)
1136    }
1137
1138    fn check_cfg(&self, cfg: &cfg_expr::Expression) -> Result<bool, Error> {
1139        use cfg_expr::{targets::get_builtin_target_by_triple, Predicate};
1140
1141        let target = self
1142            .env
1143            .get("TARGET")
1144            .expect("no TARGET env variable defined");
1145
1146        let res = if let Some(target) = get_builtin_target_by_triple(&target) {
1147            cfg.eval(|pred| match pred {
1148                Predicate::Target(tp) => Some(tp.matches(target)),
1149                _ => None,
1150            })
1151        } else {
1152            // Attempt to parse the triple, the target is not an official builtin
1153            let triple: cfg_expr::target_lexicon::Triple = target.parse().unwrap_or_else(|e| panic!("TARGET {} is not a builtin target, and it could not be parsed as a valid triplet: {}", target, e));
1154
1155            cfg.eval(|pred| match pred {
1156                Predicate::Target(tp) => Some(tp.matches(&triple)),
1157                _ => None,
1158            })
1159        };
1160
1161        res.ok_or_else(|| Error::UnsupportedCfg(cfg.original().to_string()))
1162    }
1163}
1164
1165#[derive(Debug, PartialEq, Eq)]
1166/// From where the library settings have been retrieved
1167pub enum Source {
1168    /// Settings have been retrieved from `pkg-config`
1169    PkgConfig,
1170    /// Settings have been defined using user defined environment variables
1171    EnvVariables,
1172}
1173
1174#[derive(Debug, PartialEq, Eq)]
1175/// Internal library name and if a static library is available on the system
1176pub struct InternalLib {
1177    /// Name of the library
1178    pub name: String,
1179    /// Indicates if a static library is available on the system
1180    pub is_static_available: bool,
1181}
1182
1183impl InternalLib {
1184    const fn new(name: String, is_static_available: bool) -> Self {
1185        InternalLib {
1186            name,
1187            is_static_available,
1188        }
1189    }
1190}
1191
1192#[derive(Debug)]
1193/// A system dependency
1194pub struct Library {
1195    /// Name of the library
1196    pub name: String,
1197    /// From where the library settings have been retrieved
1198    pub source: Source,
1199    /// libraries the linker should link on
1200    pub libs: Vec<InternalLib>,
1201    /// directories where the compiler should look for libraries
1202    pub link_paths: Vec<PathBuf>,
1203    /// absolute paths to library files (e.g., from pkg-config output like `/path/to/libfoo.a`)
1204    pub link_files: Vec<PathBuf>,
1205    /// frameworks the linker should link on
1206    pub frameworks: Vec<String>,
1207    /// directories where the compiler should look for frameworks
1208    pub framework_paths: Vec<PathBuf>,
1209    /// directories where the compiler should look for header files
1210    pub include_paths: Vec<PathBuf>,
1211    /// flags that should be passed to the linker
1212    pub ld_args: Vec<Vec<String>>,
1213    /// macros that should be defined by the compiler
1214    pub defines: HashMap<String, Option<String>>,
1215    /// library version
1216    pub version: String,
1217    /// library is statically linked
1218    pub statik: bool,
1219}
1220
1221impl Library {
1222    fn from_pkg_config(name: &str, l: pkg_config::Library) -> Self {
1223        // taken from: https://github.com/rust-lang/pkg-config-rs/blob/54325785816695df031cef3b26b6a9a203bbc01b/src/lib.rs#L502
1224        let system_roots = if cfg!(target_os = "macos") {
1225            vec![PathBuf::from("/Library"), PathBuf::from("/System")]
1226        } else {
1227            let sysroot = env::var_os("PKG_CONFIG_SYSROOT_DIR")
1228                .or_else(|| env::var_os("SYSROOT"))
1229                .map(PathBuf::from);
1230
1231            if cfg!(target_os = "windows") {
1232                if let Some(sysroot) = sysroot {
1233                    vec![sysroot]
1234                } else {
1235                    vec![]
1236                }
1237            } else {
1238                vec![sysroot.unwrap_or_else(|| PathBuf::from("/usr"))]
1239            }
1240        };
1241
1242        let is_static_available = |name: &String| -> bool {
1243            // MSVC: don't claim a static archive is "available" even when
1244            // it is on disk. Emitting `cargo:rustc-link-lib=static=...`
1245            // makes rustc try to bundle the archive into the rlib, which
1246            // overflows `ar_archive_writer` (u32 limits) on archives the
1247            // size of gst-plugins-rs's `lib*.a`. Without the modifier,
1248            // MSVC `link.exe` resolves `name.lib` natively from the bundle.
1249            // Upstream issue:
1250            //   https://github.com/rust-lang/ar_archive_writer/issues/31
1251            if cfg!(all(target_os = "windows", target_env = "msvc")) {
1252                return false;
1253            }
1254            let libnames = {
1255                let mut names = vec![format!("lib{name}.a")];
1256                if cfg!(target_os = "windows") {
1257                    names.push(format!("{name}.lib"));
1258                }
1259                names
1260            };
1261
1262            l.link_paths.iter().any(|dir| {
1263                let library_exists = libnames.iter().any(|libname| dir.join(libname).exists());
1264                library_exists && !system_roots.iter().any(|sys| dir.starts_with(sys))
1265            })
1266        };
1267
1268        Self {
1269            name: name.to_string(),
1270            source: Source::PkgConfig,
1271            libs: l
1272                .libs
1273                .iter()
1274                .map(|lib| InternalLib::new(lib.to_owned(), is_static_available(lib)))
1275                .collect(),
1276            link_paths: l.link_paths,
1277            link_files: l.link_files,
1278            include_paths: l.include_paths,
1279            ld_args: l.ld_args,
1280            frameworks: l.frameworks,
1281            framework_paths: l.framework_paths,
1282            defines: l.defines,
1283            version: l.version,
1284            statik: false,
1285        }
1286    }
1287
1288    fn from_env_variables(name: &str) -> Self {
1289        Self {
1290            name: name.to_string(),
1291            source: Source::EnvVariables,
1292            libs: Vec::new(),
1293            link_paths: Vec::new(),
1294            link_files: Vec::new(),
1295            include_paths: Vec::new(),
1296            ld_args: Vec::new(),
1297            frameworks: Vec::new(),
1298            framework_paths: Vec::new(),
1299            defines: HashMap::new(),
1300            version: String::new(),
1301            statik: false,
1302        }
1303    }
1304
1305    /// Calls a function changing the environment so that `pkg-config` will try to
1306    /// look first in the provided path.
1307    pub fn wrap_pkg_config<T, R>(
1308        pkg_config_paths: impl PathOrList<T>,
1309        f: impl FnOnce() -> Result<R, pkg_config::Error>,
1310    ) -> Result<R, pkg_config::Error> {
1311        // Save current PKG_CONFIG_PATH, so we can restore it
1312        let prev = env::var("PKG_CONFIG_PATH").ok();
1313
1314        let prev_paths = prev.iter().flat_map(env::split_paths).collect::<Vec<_>>();
1315        let joined_paths = pkg_config_paths.join_paths(prev_paths.as_slice());
1316
1317        // pkg-config 0.29.2 eats `\` while expanding `${pcfiledir}`, producing
1318        // corrupt flags (e.g. `C:Userslibpkgconfig`). Forward slashes work on
1319        // Windows and avoid this; there `\` is only ever a path separator.
1320        #[cfg(windows)]
1321        let joined_paths = OsString::from(joined_paths.to_string_lossy().replace('\\', "/"));
1322
1323        env::set_var("PKG_CONFIG_PATH", joined_paths);
1324
1325        let res = f();
1326
1327        if let Some(prev) = prev {
1328            env::set_var("PKG_CONFIG_PATH", prev);
1329        }
1330
1331        res
1332    }
1333
1334    /// Create a `Library` by probing `pkg-config` on an internal directory.
1335    /// This helper is meant to be used by `Config::add_build_internal` closures
1336    /// after having built the lib to return the library information to system-deps.
1337    ///
1338    /// This library will be statically linked.
1339    ///
1340    /// # Arguments
1341    ///
1342    /// * `pkg_config_dir`: the directory where the library `.pc` file is located
1343    /// * `lib`: the name of the library to look for
1344    /// * `version`: the minimum version of `lib` required
1345    ///
1346    /// # Examples
1347    ///
1348    /// ```
1349    /// let mut config = system_deps::Config::new();
1350    /// config.add_build_internal("mylib", |lib, version| {
1351    ///   // Actually build the library here that fulfills the passed in version requirements
1352    ///   system_deps::Library::from_internal_pkg_config("build-dir",
1353    ///       lib, "1.2.4")
1354    /// });
1355    /// ```
1356    pub fn from_internal_pkg_config<T>(
1357        pkg_config_paths: impl PathOrList<T>,
1358        lib: &str,
1359        version: &str,
1360    ) -> Result<Self, BuildInternalClosureError> {
1361        let pkg_lib = Self::wrap_pkg_config(pkg_config_paths, || {
1362            pkg_config::Config::new()
1363                .atleast_version(version)
1364                .print_system_libs(false)
1365                .cargo_metadata(false)
1366                .statik(true)
1367                .probe(lib)
1368        })?;
1369
1370        let mut lib = Self::from_pkg_config(lib, pkg_lib);
1371        lib.statik = true;
1372        Ok(lib)
1373    }
1374}
1375
1376/// Whether the resolved `pkg-config` accepts `--dont-define-prefix`. Cached.
1377#[cfg(windows)]
1378fn pkg_config_accepts_dont_define_prefix() -> bool {
1379    use std::sync::OnceLock;
1380    static SUPPORTED: OnceLock<bool> = OnceLock::new();
1381    *SUPPORTED.get_or_init(|| {
1382        let exe = env::var_os("PKG_CONFIG").unwrap_or_else(|| "pkg-config".into());
1383        std::process::Command::new(exe)
1384            .args(["--dont-define-prefix", "--version"])
1385            .output()
1386            .map(|out| out.status.success())
1387            .unwrap_or(false)
1388    })
1389}
1390
1391/// A trait that can represent both a reference to a Path like object or a list of paths.
1392/// Used in `Library::wrap_pkg_config` and `Library::from_internal_pkg_config` to specify
1393/// the list of `pkg-config` paths that should take priority.
1394pub trait PathOrList<T> {
1395    /// Creates an string of paths appropiately joined for an environment variable.
1396    /// The paths in `self` will go before the paths in `other`.
1397    fn join_paths(&self, other: impl AsRef<[PathBuf]>) -> OsString;
1398}
1399
1400impl<T: AsRef<Path>> PathOrList<T> for T {
1401    fn join_paths(&self, other: impl AsRef<[PathBuf]>) -> OsString {
1402        let other = other.as_ref().iter().map(|p| p.as_path());
1403        env::join_paths(iter::once(self.as_ref()).chain(other))
1404            .expect("Path contains invalid character")
1405    }
1406}
1407
1408impl<T: AsRef<Path>, S: Borrow<[T]>> PathOrList<(T, S)> for &S {
1409    fn join_paths(&self, other: impl AsRef<[PathBuf]>) -> OsString {
1410        let slice: &[T] = (*self).borrow();
1411        let other = other.as_ref().iter().map(|p| p.as_path());
1412        env::join_paths(slice.iter().map(|p| p.as_ref()).chain(other))
1413            .expect("Path contains invalid character")
1414    }
1415}
1416
1417impl<T: PathOrList<S>, S> PathOrList<(T, S)> for Option<T> {
1418    fn join_paths(&self, other: impl AsRef<[PathBuf]>) -> OsString {
1419        match self {
1420            Some(s) => s.join_paths(other),
1421            None => env::join_paths(other.as_ref().iter().map(|p| p.as_os_str()))
1422                .expect("Path contains invalid character"),
1423        }
1424    }
1425}
1426
1427#[derive(Debug)]
1428enum EnvVariables {
1429    Environment,
1430    #[cfg(test)]
1431    Mock(HashMap<&'static str, String>),
1432}
1433
1434trait EnvVariablesExt<T> {
1435    fn contains(&self, var: T) -> bool {
1436        self.get(var).is_some()
1437    }
1438
1439    fn get(&self, var: T) -> Option<String>;
1440
1441    fn has_value(&self, var: T, val: &str) -> bool {
1442        match self.get(var) {
1443            Some(v) => v == val,
1444            None => false,
1445        }
1446    }
1447}
1448
1449impl EnvVariablesExt<&str> for EnvVariables {
1450    fn get(&self, var: &str) -> Option<String> {
1451        match self {
1452            EnvVariables::Environment => env::var(var).ok(),
1453            #[cfg(test)]
1454            EnvVariables::Mock(vars) => vars.get(var).cloned(),
1455        }
1456    }
1457}
1458
1459impl EnvVariablesExt<&EnvVariable> for EnvVariables {
1460    fn get(&self, var: &EnvVariable) -> Option<String> {
1461        let s = var.to_string();
1462        let var: &str = s.as_ref();
1463        self.get(var)
1464    }
1465}
1466
1467#[derive(Debug, PartialEq)]
1468enum BuildFlag {
1469    Include(String),
1470    SearchNative(String),
1471    SearchFramework(String),
1472    Lib(String, bool), // true if static
1473    LibFramework(String),
1474    RerunIfEnvChanged(EnvVariable),
1475    LinkArg(Vec<String>),
1476}
1477
1478impl fmt::Display for BuildFlag {
1479    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1480        match self {
1481            BuildFlag::Include(paths) => write!(f, "include={paths}"),
1482            BuildFlag::SearchNative(lib) => write!(f, "rustc-link-search=native={lib}"),
1483            BuildFlag::SearchFramework(lib) => write!(f, "rustc-link-search=framework={lib}"),
1484            BuildFlag::Lib(lib, statik) => {
1485                if *statik {
1486                    write!(f, "rustc-link-lib=static={lib}")
1487                } else {
1488                    write!(f, "rustc-link-lib={lib}")
1489                }
1490            }
1491            BuildFlag::LibFramework(lib) => write!(f, "rustc-link-lib=framework={lib}"),
1492            BuildFlag::RerunIfEnvChanged(env) => write!(f, "rerun-if-env-changed={env}"),
1493            BuildFlag::LinkArg(ld_option) => {
1494                write!(f, "rustc-link-arg=-Wl,{}", ld_option.join(","))
1495            }
1496        }
1497    }
1498}
1499
1500#[derive(Debug, PartialEq)]
1501struct BuildFlags(Vec<BuildFlag>);
1502
1503impl BuildFlags {
1504    const fn new() -> Self {
1505        Self(Vec::new())
1506    }
1507
1508    fn add(&mut self, flag: BuildFlag) {
1509        self.0.push(flag);
1510    }
1511}
1512
1513impl fmt::Display for BuildFlags {
1514    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1515        for flag in self.0.iter() {
1516            writeln!(f, "cargo:{flag}")?;
1517        }
1518        Ok(())
1519    }
1520}
1521
1522fn split_paths(value: &str) -> Vec<PathBuf> {
1523    if !value.is_empty() {
1524        let paths = env::split_paths(&value);
1525        paths.map(|p| Path::new(&p).into()).collect()
1526    } else {
1527        Vec::new()
1528    }
1529}
1530
1531fn split_string(value: &str) -> Vec<String> {
1532    if !value.is_empty() {
1533        value.split(' ').map(|s| s.to_string()).collect()
1534    } else {
1535        Vec::new()
1536    }
1537}
1538
1539#[derive(Debug, PartialEq, Default)]
1540enum BuildInternal {
1541    Auto,
1542    Always,
1543    #[default]
1544    Never,
1545}
1546
1547impl FromStr for BuildInternal {
1548    type Err = ParseError;
1549
1550    fn from_str(s: &str) -> Result<Self, Self::Err> {
1551        match s {
1552            "auto" => Ok(Self::Auto),
1553            "always" => Ok(Self::Always),
1554            "never" => Ok(Self::Never),
1555            v => Err(ParseError::VariantNotFound(v.to_owned())),
1556        }
1557    }
1558}
1559
1560#[derive(Debug, PartialEq)]
1561enum ParseError {
1562    VariantNotFound(String),
1563}
1564
1565impl std::error::Error for ParseError {}
1566
1567impl fmt::Display for ParseError {
1568    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1569        match self {
1570            Self::VariantNotFound(v) => write!(f, "Unknown variant: `{v}`"),
1571        }
1572    }
1573}