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 = "2.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.gstreamer_1_0]
115//! name = "gstreamer-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.gst_gl]
126//! name = "gstreamer-gl-1.0"
127//! version = "1.14"
128//! v1_18 = { version = "1.18", name = "gstreamer-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#![deny(missing_docs)]
207
208#[cfg(test)]
209#[macro_use]
210extern crate lazy_static;
211
212#[cfg(test)]
213mod test;
214
215use heck::{ToShoutySnakeCase, ToSnakeCase};
216use std::collections::HashMap;
217use std::env;
218use std::fmt;
219use std::ops::RangeBounds;
220use std::path::{Path, PathBuf};
221use std::str::FromStr;
222
223mod metadata;
224use metadata::MetaData;
225
226/// system-deps errors
227#[derive(Debug)]
228pub enum Error {
229    /// pkg-config error
230    PkgConfig(pkg_config::Error),
231    /// One of the `Config::add_build_internal` closures failed
232    BuildInternalClosureError(String, BuildInternalClosureError),
233    /// Failed to read `Cargo.toml`
234    FailToRead(String, std::io::Error),
235    /// Raised when an error is detected in the metadata defined in `Cargo.toml`
236    InvalidMetadata(String),
237    /// Raised when dependency defined manually using `SYSTEM_DEPS_$NAME_NO_PKG_CONFIG`
238    /// did not define at least one lib using `SYSTEM_DEPS_$NAME_LIB` or
239    /// `SYSTEM_DEPS_$NAME_LIB_FRAMEWORK`
240    MissingLib(String),
241    /// An environment variable in the form of `SYSTEM_DEPS_$NAME_BUILD_INTERNAL`
242    /// contained an invalid value (allowed: `auto`, `always`, `never`)
243    BuildInternalInvalid(String),
244    /// system-deps has been asked to internally build a lib, through
245    /// `SYSTEM_DEPS_$NAME_BUILD_INTERNAL=always' or `SYSTEM_DEPS_$NAME_BUILD_INTERNAL=auto',
246    /// but not closure has been defined using `Config::add_build_internal` to build
247    /// this lib
248    BuildInternalNoClosure(String, String),
249    /// The library which has been build internally does not match the
250    /// required version defined in `Cargo.toml`
251    BuildInternalWrongVersion(String, String, String),
252    /// The `cfg()` expression used in `Cargo.toml` is currently not supported
253    UnsupportedCfg(String),
254}
255
256impl From<pkg_config::Error> for Error {
257    fn from(err: pkg_config::Error) -> Self {
258        Self::PkgConfig(err)
259    }
260}
261
262impl std::error::Error for Error {
263    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
264        match self {
265            Self::PkgConfig(e) => Some(e),
266            Self::BuildInternalClosureError(_, e) => Some(e),
267            Self::FailToRead(_, e) => Some(e),
268            _ => None,
269        }
270    }
271}
272
273impl fmt::Display for Error {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        match self {
276            Self::PkgConfig(e) => write!(f, "{e}"),
277            Self::BuildInternalClosureError(s, e) => write!(f, "Failed to build {s}: {e}"),
278            Self::FailToRead(s, _) => write!(f, "{s}"),
279            Self::InvalidMetadata(s) => write!(f, "{s}"),
280            Self::MissingLib(s) => write!(
281                f,
282                "You should define at least one lib using {} or {}",
283                EnvVariable::new_lib(s),
284                EnvVariable::new_lib_framework(s),
285            ),
286            Self::BuildInternalInvalid(s) => write!(f, "{s}"),
287            Self::BuildInternalNoClosure(s1, s2) => {
288                write!(f, "Missing build internal closure for {s1} (version {s2})")
289            }
290            Self::BuildInternalWrongVersion(s1, s2, s3) => write!(
291                f,
292                "Internally built {s1} {s2} but minimum required version is {s3}"
293            ),
294            Self::UnsupportedCfg(s) => write!(f, "Unsupported cfg() expression: {s}"),
295        }
296    }
297}
298
299#[derive(Debug, Default)]
300/// All the system dependencies retrieved by [`Config::probe`].
301pub struct Dependencies {
302    libs: HashMap<String, Library>,
303}
304
305impl Dependencies {
306    /// Retrieve details about a system dependency.
307    ///
308    /// # Arguments
309    ///
310    /// * `name`: the name of the `toml` key defining the dependency in `Cargo.toml`
311    pub fn get_by_name(&self, name: &str) -> Option<&Library> {
312        self.libs.get(name)
313    }
314
315    /// A vector listing all system dependencies in sorted (for build reproducibility) order.
316    /// The first element of the tuple is the name of the `toml` key defining the
317    /// dependency in `Cargo.toml`.
318    pub fn iter(&self) -> Vec<(&str, &Library)> {
319        let mut v = self
320            .libs
321            .iter()
322            .map(|(k, v)| (k.as_str(), v))
323            .collect::<Vec<_>>();
324        v.sort_by_key(|x| x.0);
325        v
326    }
327
328    fn aggregate_str<F: Fn(&Library) -> &Vec<String>>(&self, getter: F) -> Vec<&str> {
329        let mut v = self
330            .libs
331            .values()
332            .flat_map(getter)
333            .map(|s| s.as_str())
334            .collect::<Vec<_>>();
335        v.sort_unstable();
336        v.dedup();
337        v
338    }
339
340    fn aggregate_path_buf<F: Fn(&Library) -> &Vec<PathBuf>>(&self, getter: F) -> Vec<&PathBuf> {
341        let mut v = self.libs.values().flat_map(getter).collect::<Vec<_>>();
342        v.sort();
343        v.dedup();
344        v
345    }
346
347    /// Returns a vector of [`Library::libs`] of each library, removing duplicates.
348    pub fn all_libs(&self) -> Vec<&str> {
349        let mut v = self
350            .libs
351            .values()
352            .flat_map(|l| l.libs.iter().map(|lib| lib.name.as_str()))
353            .collect::<Vec<_>>();
354        v.sort_unstable();
355        v.dedup();
356        v
357    }
358
359    /// Returns a vector of [`Library::link_paths`] of each library, removing duplicates.
360    pub fn all_link_paths(&self) -> Vec<&PathBuf> {
361        self.aggregate_path_buf(|l| &l.link_paths)
362    }
363
364    /// Returns a vector of [`Library::frameworks`] of each library, removing duplicates.
365    pub fn all_frameworks(&self) -> Vec<&str> {
366        self.aggregate_str(|l| &l.frameworks)
367    }
368
369    /// Returns a vector of [`Library::framework_paths`] of each library, removing duplicates.
370    pub fn all_framework_paths(&self) -> Vec<&PathBuf> {
371        self.aggregate_path_buf(|l| &l.framework_paths)
372    }
373
374    /// Returns a vector of [`Library::include_paths`] of each library, removing duplicates.
375    pub fn all_include_paths(&self) -> Vec<&PathBuf> {
376        self.aggregate_path_buf(|l| &l.include_paths)
377    }
378
379    /// Returns a vector of [`Library::ld_args`] of each library, removing duplicates.
380    pub fn all_linker_args(&self) -> Vec<&Vec<String>> {
381        let mut v = self
382            .libs
383            .values()
384            .flat_map(|l| &l.ld_args)
385            .collect::<Vec<_>>();
386        v.sort_unstable();
387        v.dedup();
388        v
389    }
390
391    /// Returns a vector of [`Library::defines`] of each library, removing duplicates.
392    pub fn all_defines(&self) -> Vec<(&str, &Option<String>)> {
393        let mut v = self
394            .libs
395            .values()
396            .flat_map(|l| l.defines.iter())
397            .map(|(k, v)| (k.as_str(), v))
398            .collect::<Vec<_>>();
399        v.sort();
400        v.dedup();
401        v
402    }
403
404    fn add(&mut self, name: &str, lib: Library) {
405        self.libs.insert(name.to_string(), lib);
406    }
407
408    fn override_from_flags(&mut self, env: &EnvVariables) {
409        for (name, lib) in self.libs.iter_mut() {
410            if let Some(value) = env.get(&EnvVariable::new_search_native(name)) {
411                lib.link_paths = split_paths(&value);
412            }
413            if let Some(value) = env.get(&EnvVariable::new_search_framework(name)) {
414                lib.framework_paths = split_paths(&value);
415            }
416            if let Some(value) = env.get(&EnvVariable::new_lib(name)) {
417                let should_be_linked_statically = env
418                    .has_value(&EnvVariable::new_link(Some(name)), "static")
419                    || env.has_value(&EnvVariable::new_link(None), "static");
420
421                // If somebody manually mandates static linking, that is a
422                // clear intent. Let's just assume that a static lib is
423                // available and let the linking fail if the user is wrong.
424                let is_static_lib_available = should_be_linked_statically;
425
426                lib.libs = split_string(&value)
427                    .into_iter()
428                    .map(|l| InternalLib::new(l, is_static_lib_available))
429                    .collect();
430            }
431            if let Some(value) = env.get(&EnvVariable::new_lib_framework(name)) {
432                lib.frameworks = split_string(&value);
433            }
434            if let Some(value) = env.get(&EnvVariable::new_include(name)) {
435                lib.include_paths = split_paths(&value);
436            }
437            if let Some(value) = env.get(&EnvVariable::new_linker_args(name)) {
438                lib.ld_args = split_string(&value)
439                    .into_iter()
440                    .map(|l| l.split(',').map(|l| l.to_string()).collect())
441                    .collect();
442            }
443        }
444    }
445
446    fn gen_flags(&self) -> Result<BuildFlags, Error> {
447        let mut flags = BuildFlags::new();
448        let mut include_paths = Vec::new();
449
450        for (name, lib) in self.iter() {
451            include_paths.extend(lib.include_paths.clone());
452
453            if lib.source == Source::EnvVariables
454                && lib.libs.is_empty()
455                && lib.frameworks.is_empty()
456            {
457                return Err(Error::MissingLib(name.to_string()));
458            }
459
460            lib.link_paths
461                .iter()
462                .for_each(|l| flags.add(BuildFlag::SearchNative(l.to_string_lossy().to_string())));
463            lib.framework_paths.iter().for_each(|f| {
464                flags.add(BuildFlag::SearchFramework(f.to_string_lossy().to_string()))
465            });
466            lib.libs.iter().for_each(|l| {
467                flags.add(BuildFlag::Lib(
468                    l.name.clone(),
469                    lib.statik && l.is_static_available,
470                ))
471            });
472            lib.frameworks
473                .iter()
474                .for_each(|f| flags.add(BuildFlag::LibFramework(f.clone())));
475            lib.ld_args
476                .iter()
477                .for_each(|f| flags.add(BuildFlag::LinkArg(f.clone())))
478        }
479
480        // Export DEP_$CRATE_INCLUDE env variable with the headers paths,
481        // see https://kornel.ski/rust-sys-crate#headers
482        if !include_paths.is_empty() {
483            if let Ok(paths) = std::env::join_paths(include_paths) {
484                flags.add(BuildFlag::Include(paths.to_string_lossy().to_string()));
485            }
486        }
487
488        // Export cargo:rerun-if-env-changed instructions for all env variables affecting system-deps behaviour
489        flags.add(BuildFlag::RerunIfEnvChanged(
490            EnvVariable::new_build_internal(None),
491        ));
492        flags.add(BuildFlag::RerunIfEnvChanged(EnvVariable::new_link(None)));
493
494        for (name, _lib) in self.libs.iter() {
495            EnvVariable::set_rerun_if_changed_for_all_variants(&mut flags, name);
496        }
497
498        Ok(flags)
499    }
500}
501
502#[derive(Debug)]
503/// Error used in return value of `Config::add_build_internal` closures
504pub enum BuildInternalClosureError {
505    /// `pkg-config` error
506    PkgConfig(pkg_config::Error),
507    /// General failure
508    Failed(String),
509}
510
511impl From<pkg_config::Error> for BuildInternalClosureError {
512    fn from(err: pkg_config::Error) -> Self {
513        Self::PkgConfig(err)
514    }
515}
516
517impl BuildInternalClosureError {
518    /// Create a new `BuildInternalClosureError::Failed` representing a general
519    /// failure.
520    ///
521    /// # Arguments
522    ///
523    /// * `details`: human-readable details about the failure
524    pub fn failed(details: &str) -> Self {
525        Self::Failed(details.to_string())
526    }
527}
528
529impl std::error::Error for BuildInternalClosureError {
530    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
531        match self {
532            Self::PkgConfig(e) => Some(e),
533            _ => None,
534        }
535    }
536}
537
538impl fmt::Display for BuildInternalClosureError {
539    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540        match self {
541            Self::PkgConfig(e) => write!(f, "{e}"),
542            Self::Failed(s) => write!(f, "{s}"),
543        }
544    }
545}
546
547// Enum representing the environment variables user can define to tune system-deps.
548#[derive(Debug, PartialEq)]
549enum EnvVariable {
550    Lib(String),
551    LibFramework(String),
552    SearchNative(String),
553    SearchFramework(String),
554    Include(String),
555    NoPkgConfig(String),
556    BuildInternal(Option<String>),
557    Link(Option<String>),
558    LinkerArgs(String),
559}
560
561impl EnvVariable {
562    fn new_lib(lib: &str) -> Self {
563        Self::Lib(lib.to_string())
564    }
565
566    fn new_lib_framework(lib: &str) -> Self {
567        Self::LibFramework(lib.to_string())
568    }
569
570    fn new_search_native(lib: &str) -> Self {
571        Self::SearchNative(lib.to_string())
572    }
573
574    fn new_search_framework(lib: &str) -> Self {
575        Self::SearchFramework(lib.to_string())
576    }
577
578    fn new_include(lib: &str) -> Self {
579        Self::Include(lib.to_string())
580    }
581
582    fn new_linker_args(lib: &str) -> Self {
583        Self::LinkerArgs(lib.to_string())
584    }
585
586    fn new_no_pkg_config(lib: &str) -> Self {
587        Self::NoPkgConfig(lib.to_string())
588    }
589
590    fn new_build_internal(lib: Option<&str>) -> Self {
591        Self::BuildInternal(lib.map(|l| l.to_string()))
592    }
593
594    fn new_link(lib: Option<&str>) -> Self {
595        Self::Link(lib.map(|l| l.to_string()))
596    }
597
598    const fn suffix(&self) -> &'static str {
599        match self {
600            EnvVariable::Lib(_) => "LIB",
601            EnvVariable::LibFramework(_) => "LIB_FRAMEWORK",
602            EnvVariable::SearchNative(_) => "SEARCH_NATIVE",
603            EnvVariable::SearchFramework(_) => "SEARCH_FRAMEWORK",
604            EnvVariable::Include(_) => "INCLUDE",
605            EnvVariable::NoPkgConfig(_) => "NO_PKG_CONFIG",
606            EnvVariable::BuildInternal(_) => "BUILD_INTERNAL",
607            EnvVariable::Link(_) => "LINK",
608            EnvVariable::LinkerArgs(_) => "LDFLAGS",
609        }
610    }
611
612    fn set_rerun_if_changed_for_all_variants(flags: &mut BuildFlags, name: &str) {
613        #[inline]
614        fn add_to_flags(flags: &mut BuildFlags, var: EnvVariable) {
615            flags.add(BuildFlag::RerunIfEnvChanged(var));
616        }
617        add_to_flags(flags, EnvVariable::new_lib(name));
618        add_to_flags(flags, EnvVariable::new_lib_framework(name));
619        add_to_flags(flags, EnvVariable::new_search_native(name));
620        add_to_flags(flags, EnvVariable::new_search_framework(name));
621        add_to_flags(flags, EnvVariable::new_include(name));
622        add_to_flags(flags, EnvVariable::new_linker_args(name));
623        add_to_flags(flags, EnvVariable::new_no_pkg_config(name));
624        add_to_flags(flags, EnvVariable::new_build_internal(Some(name)));
625        add_to_flags(flags, EnvVariable::new_link(Some(name)));
626    }
627}
628
629impl fmt::Display for EnvVariable {
630    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
631        let suffix = match self {
632            EnvVariable::Lib(lib)
633            | EnvVariable::LibFramework(lib)
634            | EnvVariable::SearchNative(lib)
635            | EnvVariable::SearchFramework(lib)
636            | EnvVariable::Include(lib)
637            | EnvVariable::LinkerArgs(lib)
638            | EnvVariable::NoPkgConfig(lib)
639            | EnvVariable::BuildInternal(Some(lib))
640            | EnvVariable::Link(Some(lib)) => {
641                format!("{}_{}", lib.to_shouty_snake_case(), self.suffix())
642            }
643            EnvVariable::BuildInternal(None) | EnvVariable::Link(None) => self.suffix().to_string(),
644        };
645        write!(f, "SYSTEM_DEPS_{suffix}")
646    }
647}
648
649type FnBuildInternal =
650    dyn FnOnce(&str, &str) -> std::result::Result<Library, BuildInternalClosureError>;
651
652/// Structure used to configure `metadata` before starting to probe for dependencies
653pub struct Config {
654    env: EnvVariables,
655    build_internals: HashMap<String, Box<FnBuildInternal>>,
656}
657
658impl Default for Config {
659    fn default() -> Self {
660        Self::new_with_env(EnvVariables::Environment)
661    }
662}
663
664impl Config {
665    /// Create a new set of configuration
666    pub fn new() -> Self {
667        Self::default()
668    }
669
670    fn new_with_env(env: EnvVariables) -> Self {
671        Self {
672            env,
673            build_internals: HashMap::new(),
674        }
675    }
676
677    /// Probe all libraries configured in the Cargo.toml
678    /// `[package.metadata.system-deps]` section.
679    ///
680    /// The returned hash is using the `toml` key defining the dependency as key.
681    pub fn probe(self) -> Result<Dependencies, Error> {
682        let libraries = self.probe_full()?;
683        let flags = libraries.gen_flags()?;
684
685        // Output cargo flags
686        println!("{flags}");
687
688        for (name, _) in libraries.iter() {
689            println!("cargo:rustc-cfg=system_deps_have_{}", name.to_snake_case());
690        }
691
692        Ok(libraries)
693    }
694
695    /// Add hook so system-deps can internally build library `name` if requested by user.
696    ///
697    /// It will only be triggered if the environment variable
698    /// `SYSTEM_DEPS_$NAME_BUILD_INTERNAL` is defined with either `always` or
699    /// `auto` as value. In the latter case, `func` is called only if the requested
700    /// version of the library was not found on the system.
701    ///
702    /// # Arguments
703    /// * `name`: the name of the library, as defined in `Cargo.toml`
704    /// * `func`: closure called when internally building the library.
705    ///
706    /// It receives as argument the library name, and the minimum version required.
707    pub fn add_build_internal<F>(self, name: &str, func: F) -> Self
708    where
709        F: 'static + FnOnce(&str, &str) -> std::result::Result<Library, BuildInternalClosureError>,
710    {
711        let mut build_internals = self.build_internals;
712        build_internals.insert(name.to_string(), Box::new(func));
713
714        Self {
715            env: self.env,
716            build_internals,
717        }
718    }
719
720    fn probe_full(mut self) -> Result<Dependencies, Error> {
721        let mut libraries = self.probe_pkg_config()?;
722        libraries.override_from_flags(&self.env);
723
724        Ok(libraries)
725    }
726
727    fn probe_pkg_config(&mut self) -> Result<Dependencies, Error> {
728        let dir = self
729            .env
730            .get("CARGO_MANIFEST_DIR")
731            .ok_or_else(|| Error::InvalidMetadata("$CARGO_MANIFEST_DIR not set".into()))?;
732        let mut path = PathBuf::from(dir);
733        path.push("Cargo.toml");
734
735        println!("cargo:rerun-if-changed={}", &path.to_string_lossy());
736
737        let metadata = MetaData::from_file(&path)?;
738
739        let mut libraries = Dependencies::default();
740
741        for dep in metadata.deps.iter() {
742            if let Some(cfg) = &dep.cfg {
743                // Check if `cfg()` expression matches the target settings
744                if !self.check_cfg(cfg)? {
745                    continue;
746                }
747            }
748
749            let mut enabled_feature_overrides = Vec::new();
750
751            for o in dep.version_overrides.iter() {
752                if self.has_feature(&o.key) {
753                    enabled_feature_overrides.push(o);
754                }
755            }
756
757            if let Some(feature) = dep.feature.as_ref() {
758                if !self.has_feature(feature) {
759                    continue;
760                }
761            }
762
763            // Pick the highest feature enabled version
764            let version;
765            let lib_name;
766            let fallback_lib_names;
767            let optional;
768            if enabled_feature_overrides.is_empty() {
769                version = dep.version.as_deref();
770                lib_name = dep.lib_name();
771                fallback_lib_names = dep.fallback_names.as_deref().unwrap_or(&[]);
772                optional = dep.optional;
773            } else {
774                enabled_feature_overrides.sort_by(|a, b| {
775                    fn min_version(r: metadata::VersionRange<'_>) -> &str {
776                        match r.start_bound() {
777                            std::ops::Bound::Unbounded => unreachable!(),
778                            std::ops::Bound::Excluded(_) => unreachable!(),
779                            std::ops::Bound::Included(b) => b,
780                        }
781                    }
782
783                    let a = min_version(metadata::parse_version(&a.version));
784                    let b = min_version(metadata::parse_version(&b.version));
785
786                    version_compare::compare(a, b)
787                        .expect("failed to compare versions")
788                        .ord()
789                        .expect("invalid version")
790                });
791                let highest = enabled_feature_overrides.into_iter().next_back().unwrap();
792
793                version = Some(highest.version.as_str());
794                lib_name = highest.name.as_deref().unwrap_or(dep.lib_name());
795                fallback_lib_names = highest
796                    .fallback_names
797                    .as_deref()
798                    .or(dep.fallback_names.as_deref())
799                    .unwrap_or(&[]);
800                optional = highest.optional.unwrap_or(dep.optional);
801            };
802
803            let version = version.ok_or_else(|| {
804                Error::InvalidMetadata(format!("No version defined for {}", dep.key))
805            })?;
806
807            let name = &dep.key;
808            let build_internal = self.get_build_internal_status(name)?;
809
810            // should the lib be statically linked?
811            let statik = self
812                .env
813                .has_value(&EnvVariable::new_link(Some(name)), "static")
814                || self.env.has_value(&EnvVariable::new_link(None), "static");
815
816            let mut library = if self.env.contains(&EnvVariable::new_no_pkg_config(name)) {
817                Library::from_env_variables(name)
818            } else if build_internal == BuildInternal::Always {
819                self.call_build_internal(lib_name, version)?
820            } else {
821                let mut config = pkg_config::Config::new();
822                config
823                    .print_system_libs(false)
824                    .cargo_metadata(false)
825                    .range_version(metadata::parse_version(version))
826                    .statik(statik);
827
828                match Self::probe_with_fallback(config, lib_name, fallback_lib_names) {
829                    Ok((lib_name, lib)) => Library::from_pkg_config(lib_name, lib),
830                    Err(e) => {
831                        if build_internal == BuildInternal::Auto {
832                            // Try building the lib internally as a fallback
833                            self.call_build_internal(name, version)?
834                        } else if optional {
835                            // If the dep is optional just skip it
836                            continue;
837                        } else {
838                            return Err(e.into());
839                        }
840                    }
841                }
842            };
843
844            library.statik = statik;
845
846            libraries.add(name, library);
847        }
848        Ok(libraries)
849    }
850
851    fn probe_with_fallback<'a>(
852        config: pkg_config::Config,
853        name: &'a str,
854        fallback_names: &'a [String],
855    ) -> Result<(&'a str, pkg_config::Library), pkg_config::Error> {
856        let error = match config.probe(name) {
857            Ok(x) => return Ok((name, x)),
858            Err(e) => e,
859        };
860        for name in fallback_names {
861            if let Ok(library) = config.probe(name) {
862                return Ok((name, library));
863            }
864        }
865        Err(error)
866    }
867
868    fn get_build_internal_env_var(&self, var: EnvVariable) -> Result<Option<BuildInternal>, Error> {
869        match self.env.get(&var).as_deref() {
870            Some(s) => {
871                let b = BuildInternal::from_str(s).map_err(|_| {
872                    Error::BuildInternalInvalid(format!(
873                        "Invalid value in {var}: {s} (allowed: 'auto', 'always', 'never')"
874                    ))
875                })?;
876                Ok(Some(b))
877            }
878            None => Ok(None),
879        }
880    }
881
882    fn get_build_internal_status(&self, name: &str) -> Result<BuildInternal, Error> {
883        match self.get_build_internal_env_var(EnvVariable::new_build_internal(Some(name)))? {
884            Some(b) => Ok(b),
885            None => Ok(self
886                .get_build_internal_env_var(EnvVariable::new_build_internal(None))?
887                .unwrap_or_default()),
888        }
889    }
890
891    fn call_build_internal(&mut self, name: &str, version_str: &str) -> Result<Library, Error> {
892        let lib = match self.build_internals.remove(name) {
893            Some(f) => f(name, version_str)
894                .map_err(|e| Error::BuildInternalClosureError(name.into(), e))?,
895            None => {
896                return Err(Error::BuildInternalNoClosure(
897                    name.into(),
898                    version_str.into(),
899                ))
900            }
901        };
902
903        // Check that the lib built internally matches the required version
904        let version = metadata::parse_version(version_str);
905        fn min_version(r: metadata::VersionRange<'_>) -> &str {
906            match r.start_bound() {
907                std::ops::Bound::Unbounded => unreachable!(),
908                std::ops::Bound::Excluded(_) => unreachable!(),
909                std::ops::Bound::Included(b) => b,
910            }
911        }
912        fn max_version(r: metadata::VersionRange<'_>) -> Option<&str> {
913            match r.end_bound() {
914                std::ops::Bound::Included(_) => unreachable!(),
915                std::ops::Bound::Unbounded => None,
916                std::ops::Bound::Excluded(b) => Some(*b),
917            }
918        }
919
920        let min = min_version(version.clone());
921        if version_compare::compare(&lib.version, min) == Ok(version_compare::Cmp::Lt) {
922            return Err(Error::BuildInternalWrongVersion(
923                name.into(),
924                lib.version,
925                version_str.into(),
926            ));
927        }
928
929        if let Some(max) = max_version(version) {
930            if version_compare::compare(&lib.version, max) == Ok(version_compare::Cmp::Ge) {
931                return Err(Error::BuildInternalWrongVersion(
932                    name.into(),
933                    lib.version,
934                    version_str.into(),
935                ));
936            }
937        }
938
939        Ok(lib)
940    }
941
942    fn has_feature(&self, feature: &str) -> bool {
943        let var: &str = &format!("CARGO_FEATURE_{}", feature.to_uppercase().replace('-', "_"));
944        self.env.contains(var)
945    }
946
947    fn check_cfg(&self, cfg: &cfg_expr::Expression) -> Result<bool, Error> {
948        use cfg_expr::{targets::get_builtin_target_by_triple, Predicate};
949
950        let target = self
951            .env
952            .get("TARGET")
953            .expect("no TARGET env variable defined");
954
955        let res = if let Some(target) = get_builtin_target_by_triple(&target) {
956            cfg.eval(|pred| match pred {
957                Predicate::Target(tp) => Some(tp.matches(target)),
958                _ => None,
959            })
960        } else {
961            // Attempt to parse the triple, the target is not an official builtin
962            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));
963
964            cfg.eval(|pred| match pred {
965                Predicate::Target(tp) => Some(tp.matches(&triple)),
966                _ => None,
967            })
968        };
969
970        res.ok_or_else(|| Error::UnsupportedCfg(cfg.original().to_string()))
971    }
972}
973
974#[derive(Debug, PartialEq, Eq)]
975/// From where the library settings have been retrieved
976pub enum Source {
977    /// Settings have been retrieved from `pkg-config`
978    PkgConfig,
979    /// Settings have been defined using user defined environment variables
980    EnvVariables,
981}
982
983#[derive(Debug, PartialEq, Eq)]
984/// Internal library name and if a static library is available on the system
985pub struct InternalLib {
986    /// Name of the library
987    pub name: String,
988    /// Indicates if a static library is available on the system
989    pub is_static_available: bool,
990}
991
992impl InternalLib {
993    const fn new(name: String, is_static_available: bool) -> Self {
994        InternalLib {
995            name,
996            is_static_available,
997        }
998    }
999}
1000
1001#[derive(Debug)]
1002/// A system dependency
1003pub struct Library {
1004    /// Name of the library
1005    pub name: String,
1006    /// From where the library settings have been retrieved
1007    pub source: Source,
1008    /// libraries the linker should link on
1009    pub libs: Vec<InternalLib>,
1010    /// directories where the compiler should look for libraries
1011    pub link_paths: Vec<PathBuf>,
1012    /// frameworks the linker should link on
1013    pub frameworks: Vec<String>,
1014    /// directories where the compiler should look for frameworks
1015    pub framework_paths: Vec<PathBuf>,
1016    /// directories where the compiler should look for header files
1017    pub include_paths: Vec<PathBuf>,
1018    /// flags that should be passed to the linker
1019    pub ld_args: Vec<Vec<String>>,
1020    /// macros that should be defined by the compiler
1021    pub defines: HashMap<String, Option<String>>,
1022    /// library version
1023    pub version: String,
1024    /// library is statically linked
1025    pub statik: bool,
1026}
1027
1028impl Library {
1029    fn from_pkg_config(name: &str, l: pkg_config::Library) -> Self {
1030        // taken from: https://github.com/rust-lang/pkg-config-rs/blob/54325785816695df031cef3b26b6a9a203bbc01b/src/lib.rs#L502
1031        let system_roots = if cfg!(target_os = "macos") {
1032            vec![PathBuf::from("/Library"), PathBuf::from("/System")]
1033        } else {
1034            let sysroot = env::var_os("PKG_CONFIG_SYSROOT_DIR")
1035                .or_else(|| env::var_os("SYSROOT"))
1036                .map(PathBuf::from);
1037
1038            if cfg!(target_os = "windows") {
1039                if let Some(sysroot) = sysroot {
1040                    vec![sysroot]
1041                } else {
1042                    vec![]
1043                }
1044            } else {
1045                vec![sysroot.unwrap_or_else(|| PathBuf::from("/usr"))]
1046            }
1047        };
1048
1049        let is_static_available = |name: &String| -> bool {
1050            let libnames = {
1051                let mut names = vec![format!("lib{}.a", name)];
1052
1053                if cfg!(target_os = "windows") {
1054                    names.push(format!("{name}.lib"));
1055                }
1056
1057                names
1058            };
1059
1060            l.link_paths.iter().any(|dir| {
1061                let library_exists = libnames.iter().any(|libname| dir.join(libname).exists());
1062                library_exists && !system_roots.iter().any(|sys| dir.starts_with(sys))
1063            })
1064        };
1065
1066        Self {
1067            name: name.to_string(),
1068            source: Source::PkgConfig,
1069            libs: l
1070                .libs
1071                .iter()
1072                .map(|lib| InternalLib::new(lib.to_owned(), is_static_available(lib)))
1073                .collect(),
1074            link_paths: l.link_paths,
1075            include_paths: l.include_paths,
1076            ld_args: l.ld_args,
1077            frameworks: l.frameworks,
1078            framework_paths: l.framework_paths,
1079            defines: l.defines,
1080            version: l.version,
1081            statik: false,
1082        }
1083    }
1084
1085    fn from_env_variables(name: &str) -> Self {
1086        Self {
1087            name: name.to_string(),
1088            source: Source::EnvVariables,
1089            libs: Vec::new(),
1090            link_paths: Vec::new(),
1091            include_paths: Vec::new(),
1092            ld_args: Vec::new(),
1093            frameworks: Vec::new(),
1094            framework_paths: Vec::new(),
1095            defines: HashMap::new(),
1096            version: String::new(),
1097            statik: false,
1098        }
1099    }
1100
1101    /// Create a `Library` by probing `pkg-config` on an internal directory.
1102    /// This helper is meant to be used by `Config::add_build_internal` closures
1103    /// after having built the lib to return the library information to system-deps.
1104    ///
1105    /// This library will be statically linked.
1106    ///
1107    /// # Arguments
1108    ///
1109    /// * `pkg_config_dir`: the directory where the library `.pc` file is located
1110    /// * `lib`: the name of the library to look for
1111    /// * `version`: the minimum version of `lib` required
1112    ///
1113    /// # Examples
1114    ///
1115    /// ```
1116    /// let mut config = system_deps::Config::new();
1117    /// config.add_build_internal("mylib", |lib, version| {
1118    ///   // Actually build the library here that fulfills the passed in version requirements
1119    ///   system_deps::Library::from_internal_pkg_config("build-dir",
1120    ///       lib, "1.2.4")
1121    /// });
1122    /// ```
1123    pub fn from_internal_pkg_config<P>(
1124        pkg_config_dir: P,
1125        lib: &str,
1126        version: &str,
1127    ) -> Result<Self, BuildInternalClosureError>
1128    where
1129        P: AsRef<Path>,
1130    {
1131        // save current PKG_CONFIG_PATH, so we can restore it
1132        let old = env::var("PKG_CONFIG_PATH");
1133
1134        match old {
1135            Ok(ref s) => {
1136                let mut paths = env::split_paths(s).collect::<Vec<_>>();
1137                paths.push(PathBuf::from(pkg_config_dir.as_ref()));
1138                let paths = env::join_paths(paths).unwrap();
1139                env::set_var("PKG_CONFIG_PATH", paths)
1140            }
1141            Err(_) => env::set_var("PKG_CONFIG_PATH", pkg_config_dir.as_ref()),
1142        }
1143
1144        let pkg_lib = pkg_config::Config::new()
1145            .atleast_version(version)
1146            .print_system_libs(false)
1147            .cargo_metadata(false)
1148            .statik(true)
1149            .probe(lib);
1150
1151        env::set_var("PKG_CONFIG_PATH", old.unwrap_or_else(|_| "".into()));
1152
1153        match pkg_lib {
1154            Ok(pkg_lib) => {
1155                let mut lib = Self::from_pkg_config(lib, pkg_lib);
1156                lib.statik = true;
1157                Ok(lib)
1158            }
1159            Err(e) => Err(e.into()),
1160        }
1161    }
1162}
1163
1164#[derive(Debug)]
1165enum EnvVariables {
1166    Environment,
1167    #[cfg(test)]
1168    Mock(HashMap<&'static str, String>),
1169}
1170
1171trait EnvVariablesExt<T> {
1172    fn contains(&self, var: T) -> bool {
1173        self.get(var).is_some()
1174    }
1175
1176    fn get(&self, var: T) -> Option<String>;
1177
1178    fn has_value(&self, var: T, val: &str) -> bool {
1179        match self.get(var) {
1180            Some(v) => v == val,
1181            None => false,
1182        }
1183    }
1184}
1185
1186impl EnvVariablesExt<&str> for EnvVariables {
1187    fn get(&self, var: &str) -> Option<String> {
1188        match self {
1189            EnvVariables::Environment => env::var(var).ok(),
1190            #[cfg(test)]
1191            EnvVariables::Mock(vars) => vars.get(var).cloned(),
1192        }
1193    }
1194}
1195
1196impl EnvVariablesExt<&EnvVariable> for EnvVariables {
1197    fn get(&self, var: &EnvVariable) -> Option<String> {
1198        let s = var.to_string();
1199        let var: &str = s.as_ref();
1200        self.get(var)
1201    }
1202}
1203
1204#[derive(Debug, PartialEq)]
1205enum BuildFlag {
1206    Include(String),
1207    SearchNative(String),
1208    SearchFramework(String),
1209    Lib(String, bool), // true if static
1210    LibFramework(String),
1211    RerunIfEnvChanged(EnvVariable),
1212    LinkArg(Vec<String>),
1213}
1214
1215impl fmt::Display for BuildFlag {
1216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1217        match self {
1218            BuildFlag::Include(paths) => write!(f, "include={paths}"),
1219            BuildFlag::SearchNative(lib) => write!(f, "rustc-link-search=native={lib}"),
1220            BuildFlag::SearchFramework(lib) => write!(f, "rustc-link-search=framework={lib}"),
1221            BuildFlag::Lib(lib, statik) => {
1222                if *statik {
1223                    write!(f, "rustc-link-lib=static={lib}")
1224                } else {
1225                    write!(f, "rustc-link-lib={lib}")
1226                }
1227            }
1228            BuildFlag::LibFramework(lib) => write!(f, "rustc-link-lib=framework={lib}"),
1229            BuildFlag::RerunIfEnvChanged(env) => write!(f, "rerun-if-env-changed={env}"),
1230            BuildFlag::LinkArg(ld_option) => {
1231                write!(f, "rustc-link-arg=-Wl,{}", ld_option.join(","))
1232            }
1233        }
1234    }
1235}
1236
1237#[derive(Debug, PartialEq)]
1238struct BuildFlags(Vec<BuildFlag>);
1239
1240impl BuildFlags {
1241    const fn new() -> Self {
1242        Self(Vec::new())
1243    }
1244
1245    fn add(&mut self, flag: BuildFlag) {
1246        self.0.push(flag);
1247    }
1248}
1249
1250impl fmt::Display for BuildFlags {
1251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1252        for flag in self.0.iter() {
1253            writeln!(f, "cargo:{flag}")?;
1254        }
1255        Ok(())
1256    }
1257}
1258
1259fn split_paths(value: &str) -> Vec<PathBuf> {
1260    if !value.is_empty() {
1261        let paths = env::split_paths(&value);
1262        paths.map(|p| Path::new(&p).into()).collect()
1263    } else {
1264        Vec::new()
1265    }
1266}
1267
1268fn split_string(value: &str) -> Vec<String> {
1269    if !value.is_empty() {
1270        value.split(' ').map(|s| s.to_string()).collect()
1271    } else {
1272        Vec::new()
1273    }
1274}
1275
1276#[derive(Debug, PartialEq)]
1277enum BuildInternal {
1278    Auto,
1279    Always,
1280    Never,
1281}
1282
1283impl Default for BuildInternal {
1284    fn default() -> Self {
1285        Self::Never
1286    }
1287}
1288
1289impl FromStr for BuildInternal {
1290    type Err = ParseError;
1291
1292    fn from_str(s: &str) -> Result<Self, Self::Err> {
1293        match s {
1294            "auto" => Ok(Self::Auto),
1295            "always" => Ok(Self::Always),
1296            "never" => Ok(Self::Never),
1297            v => Err(ParseError::VariantNotFound(v.to_owned())),
1298        }
1299    }
1300}
1301
1302#[derive(Debug, PartialEq)]
1303enum ParseError {
1304    VariantNotFound(String),
1305}
1306
1307impl std::error::Error for ParseError {}
1308
1309impl fmt::Display for ParseError {
1310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1311        match self {
1312            Self::VariantNotFound(v) => write!(f, "Unknown variant: `{v}`"),
1313        }
1314    }
1315}