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 = "9.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//! The metadata is read from the whole crate graph, starting at the top level `Cargo.toml`, which is found by looking up
269//! from Cargo's `build-dir` (the target directory by default). If it is outside the project, or inside a different one,
270//! set `SYSTEM_DEPS_BUILD_MANIFEST` to the absolute path of the top level `Cargo.toml`.
271//!
272//! `SYSTEM_DEPS_NO_PREBUILT` disables all the prebuilt binaries, and `SYSTEM_DEPS_$NAME_NO_PREBUILT` those of a single
273//! library and of the libraries that follow it. Disabled binaries are not downloaded, and `pkg-config` looks for the
274//! libraries in its usual paths. The prebuilt paths take precedence over `PKG_CONFIG_PATH` and its target specific
275//! variants, but the binaries are selected for the host, so set `SYSTEM_DEPS_NO_PREBUILT` when cross-compiling to a
276//! target they don't support.
277//!
278//! Builds on [docs.rs](https://docs.rs) have no network access, so when `DOCS_RS` is set no binaries are downloaded and
279//! [`Config::query_path`] returns `None` for every library. Build scripts that rely on the prebuilt binaries should return
280//! early in that case, before probing or querying paths:
281//!
282//! ```no_run
283//! fn main() {
284//! println!("cargo:rerun-if-env-changed=DOCS_RS");
285//! if std::env::var("DOCS_RS").is_ok() {
286//! return;
287//! }
288//! system_deps::Config::new().probe().unwrap();
289//! }
290//! ```
291//!
292
293#![deny(missing_docs)]
294
295#[cfg(test)]
296mod test;
297
298use heck::{ToShoutySnakeCase, ToSnakeCase};
299use std::{
300 borrow::Borrow,
301 collections::{BTreeMap, HashMap},
302 env,
303 ffi::OsString,
304 fmt, iter,
305 ops::RangeBounds,
306 path::{Path, PathBuf},
307 str::FromStr,
308};
309
310mod metadata;
311use metadata::MetaData;
312
313#[cfg(all(test, feature = "binary"))]
314mod test_binary;
315
316/// system-deps errors
317#[derive(Debug)]
318pub enum Error {
319 /// pkg-config error
320 PkgConfig(pkg_config::Error),
321 /// One of the `Config::add_build_internal` closures failed
322 BuildInternalClosureError(String, BuildInternalClosureError),
323 /// Failed to read `Cargo.toml`
324 FailToRead(String, std::io::Error),
325 /// Raised when an error is detected in the metadata defined in `Cargo.toml`
326 InvalidMetadata(String),
327 /// Raised when dependency defined manually using `SYSTEM_DEPS_$NAME_NO_PKG_CONFIG`
328 /// did not define at least one lib using `SYSTEM_DEPS_$NAME_LIB` or
329 /// `SYSTEM_DEPS_$NAME_LIB_FRAMEWORK`
330 MissingLib(String),
331 /// An environment variable in the form of `SYSTEM_DEPS_$NAME_BUILD_INTERNAL`
332 /// contained an invalid value (allowed: `auto`, `always`, `never`)
333 BuildInternalInvalid(String),
334 /// system-deps has been asked to internally build a lib, through
335 /// `SYSTEM_DEPS_$NAME_BUILD_INTERNAL=always' or `SYSTEM_DEPS_$NAME_BUILD_INTERNAL=auto',
336 /// but not closure has been defined using `Config::add_build_internal` to build
337 /// this lib
338 BuildInternalNoClosure(String, String),
339 /// The library which has been build internally does not match the
340 /// required version defined in `Cargo.toml`
341 BuildInternalWrongVersion(String, String, String),
342 /// The `cfg()` expression used in `Cargo.toml` is currently not supported
343 UnsupportedCfg(String),
344}
345
346impl From<pkg_config::Error> for Error {
347 fn from(err: pkg_config::Error) -> Self {
348 Self::PkgConfig(err)
349 }
350}
351
352impl std::error::Error for Error {
353 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
354 match self {
355 Self::PkgConfig(e) => Some(e),
356 Self::BuildInternalClosureError(_, e) => Some(e),
357 Self::FailToRead(_, e) => Some(e),
358 _ => None,
359 }
360 }
361}
362
363impl fmt::Display for Error {
364 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365 match self {
366 Self::PkgConfig(e) => write!(f, "{e}"),
367 Self::BuildInternalClosureError(s, e) => write!(f, "Failed to build {s}: {e}"),
368 Self::FailToRead(s, _) => write!(f, "{s}"),
369 Self::InvalidMetadata(s) => write!(f, "{s}"),
370 Self::MissingLib(s) => write!(
371 f,
372 "You should define at least one lib using {} or {}",
373 EnvVariable::new_lib(s),
374 EnvVariable::new_lib_framework(s),
375 ),
376 Self::BuildInternalInvalid(s) => write!(f, "{s}"),
377 Self::BuildInternalNoClosure(s1, s2) => {
378 write!(f, "Missing build internal closure for {s1} (version {s2})")
379 }
380 Self::BuildInternalWrongVersion(s1, s2, s3) => write!(
381 f,
382 "Internally built {s1} {s2} but minimum required version is {s3}"
383 ),
384 Self::UnsupportedCfg(s) => write!(f, "Unsupported cfg() expression: {s}"),
385 }
386 }
387}
388
389#[derive(Debug, Default)]
390/// All the system dependencies retrieved by [`Config::probe`].
391pub struct Dependencies {
392 libs: BTreeMap<String, Library>,
393}
394
395impl Dependencies {
396 /// Retrieve details about a system dependency.
397 ///
398 /// # Arguments
399 ///
400 /// * `name`: the name of the `toml` key defining the dependency in `Cargo.toml`
401 pub fn get_by_name(&self, name: &str) -> Option<&Library> {
402 self.libs.get(name)
403 }
404
405 /// A vector listing all system dependencies in sorted (for build reproducibility) order.
406 /// The first element of the tuple is the name of the `toml` key defining the
407 /// dependency in `Cargo.toml`.
408 pub fn iter(&self) -> Vec<(&str, &Library)> {
409 let mut v = self
410 .libs
411 .iter()
412 .map(|(k, v)| (k.as_str(), v))
413 .collect::<Vec<_>>();
414 v.sort_by_key(|x| x.0);
415 v
416 }
417
418 fn aggregate_str<F: Fn(&Library) -> &Vec<String>>(&self, getter: F) -> Vec<&str> {
419 let mut v = self
420 .libs
421 .values()
422 .flat_map(getter)
423 .map(|s| s.as_str())
424 .collect::<Vec<_>>();
425 v.sort_unstable();
426 v.dedup();
427 v
428 }
429
430 fn aggregate_path_buf<F: Fn(&Library) -> &Vec<PathBuf>>(&self, getter: F) -> Vec<&PathBuf> {
431 let mut v = self.libs.values().flat_map(getter).collect::<Vec<_>>();
432 v.sort();
433 v.dedup();
434 v
435 }
436
437 /// Returns a vector of [`Library::libs`] of each library, removing duplicates.
438 pub fn all_libs(&self) -> Vec<&str> {
439 let mut v = self
440 .libs
441 .values()
442 .flat_map(|l| l.libs.iter().map(|lib| lib.name.as_str()))
443 .collect::<Vec<_>>();
444 v.sort_unstable();
445 v.dedup();
446 v
447 }
448
449 /// Returns a vector of [`Library::link_paths`] of each library, removing duplicates.
450 pub fn all_link_paths(&self) -> Vec<&PathBuf> {
451 self.aggregate_path_buf(|l| &l.link_paths)
452 }
453
454 /// Returns a vector of [`Library::frameworks`] of each library, removing duplicates.
455 pub fn all_frameworks(&self) -> Vec<&str> {
456 self.aggregate_str(|l| &l.frameworks)
457 }
458
459 /// Returns a vector of [`Library::framework_paths`] of each library, removing duplicates.
460 pub fn all_framework_paths(&self) -> Vec<&PathBuf> {
461 self.aggregate_path_buf(|l| &l.framework_paths)
462 }
463
464 /// Returns a vector of [`Library::include_paths`] of each library, removing duplicates.
465 pub fn all_include_paths(&self) -> Vec<&PathBuf> {
466 self.aggregate_path_buf(|l| &l.include_paths)
467 }
468
469 /// Returns a vector of [`Library::ld_args`] of each library, removing duplicates.
470 pub fn all_linker_args(&self) -> Vec<&Vec<String>> {
471 let mut v = self
472 .libs
473 .values()
474 .flat_map(|l| &l.ld_args)
475 .collect::<Vec<_>>();
476 v.sort_unstable();
477 v.dedup();
478 v
479 }
480
481 /// Returns a vector of [`Library::defines`] of each library, removing duplicates.
482 pub fn all_defines(&self) -> Vec<(&str, &Option<String>)> {
483 let mut v = self
484 .libs
485 .values()
486 .flat_map(|l| l.defines.iter())
487 .map(|(k, v)| (k.as_str(), v))
488 .collect::<Vec<_>>();
489 v.sort();
490 v.dedup();
491 v
492 }
493
494 fn add(&mut self, name: &str, lib: Library) {
495 self.libs.insert(name.to_string(), lib);
496 }
497
498 fn override_from_flags(&mut self, env: &EnvVariables) {
499 for (name, lib) in self.libs.iter_mut() {
500 if let Some(value) = env.get(&EnvVariable::new_search_native(name)) {
501 lib.link_paths = split_paths(&value);
502 }
503 if let Some(value) = env.get(&EnvVariable::new_search_framework(name)) {
504 lib.framework_paths = split_paths(&value);
505 }
506 if let Some(value) = env.get(&EnvVariable::new_lib(name)) {
507 let should_be_linked_statically = env
508 .has_value(&EnvVariable::new_link(Some(name)), "static")
509 || env.has_value(&EnvVariable::new_link(None), "static");
510
511 // If somebody manually mandates static linking, that is a
512 // clear intent. Let's just assume that a static lib is
513 // available and let the linking fail if the user is wrong.
514 let is_static_lib_available = should_be_linked_statically;
515
516 lib.libs = split_string(&value)
517 .into_iter()
518 .map(|l| InternalLib::new(l, is_static_lib_available))
519 .collect();
520 }
521 if let Some(value) = env.get(&EnvVariable::new_lib_framework(name)) {
522 lib.frameworks = split_string(&value);
523 }
524 if let Some(value) = env.get(&EnvVariable::new_include(name)) {
525 lib.include_paths = split_paths(&value);
526 }
527 if let Some(value) = env.get(&EnvVariable::new_linker_args(name)) {
528 lib.ld_args = split_string(&value)
529 .into_iter()
530 .map(|l| l.split(',').map(|l| l.to_string()).collect())
531 .collect();
532 }
533 }
534 }
535
536 /// Generate cargo build flags for all probed libraries.
537 ///
538 /// When `target` is `Some`, absolute library paths in `Library::link_files` are converted into
539 /// `rustc-link-search` + `rustc-link-lib`, using the target triple to strip the platform-
540 /// specific prefix and suffix. When `None`, `link_files` is ignored.
541 ///
542 /// Primarily used when cross compiling and linking libraries via absolute paths.
543 fn gen_flags(&self, target: Option<&str>) -> Result<BuildFlags, Error> {
544 let mut flags = BuildFlags::new();
545 let mut include_paths = Vec::new();
546
547 for (name, lib) in self.iter() {
548 include_paths.extend(lib.include_paths.clone());
549
550 if lib.source == Source::EnvVariables
551 && lib.libs.is_empty()
552 && lib.frameworks.is_empty()
553 {
554 return Err(Error::MissingLib(name.to_string()));
555 }
556
557 lib.link_paths
558 .iter()
559 .for_each(|l| flags.add(BuildFlag::SearchNative(l.to_string_lossy().to_string())));
560 lib.framework_paths.iter().for_each(|f| {
561 flags.add(BuildFlag::SearchFramework(f.to_string_lossy().to_string()))
562 });
563 lib.libs.iter().for_each(|l| {
564 flags.add(BuildFlag::Lib(
565 l.name.clone(),
566 lib.statik && l.is_static_available,
567 ))
568 });
569
570 // Convert absolute paths in pkg-config's `Libs:` into `rustc-link-search=native` +
571 // `rustc-link-lib` directives. The target triple determines which prefix and suffix to
572 // strip.
573 if let Some(target) = target {
574 let mut seen_dirs = Vec::new();
575
576 for path in &lib.link_files {
577 if let (Some(dir), Some(file_name)) = (path.parent(), path.file_name()) {
578 let filename = file_name.to_string_lossy();
579 let mut new = false;
580
581 if !seen_dirs.contains(&dir) {
582 new = true;
583 seen_dirs.push(dir);
584 }
585
586 // Try to extract the library linking name from the filename, e.g.
587 // libboost_context.a -> boost_context
588 if let Some(lib_name) =
589 pkg_config::Library::extract_lib_from_filename(target, &filename)
590 {
591 if new {
592 flags.add(BuildFlag::SearchNative(
593 dir.to_string_lossy().to_string(),
594 ));
595 }
596
597 flags.add(BuildFlag::Lib(lib_name.to_string(), lib.statik));
598 }
599 }
600 }
601 }
602
603 lib.frameworks
604 .iter()
605 .for_each(|f| flags.add(BuildFlag::LibFramework(f.clone())));
606 lib.ld_args
607 .iter()
608 .for_each(|f| flags.add(BuildFlag::LinkArg(f.clone())))
609 }
610
611 // Export DEP_$CRATE_INCLUDE env variable with the headers paths,
612 // see https://kornel.ski/rust-sys-crate#headers
613 if !include_paths.is_empty() {
614 if let Ok(paths) = std::env::join_paths(include_paths) {
615 flags.add(BuildFlag::Include(paths.to_string_lossy().to_string()));
616 }
617 }
618
619 // Export cargo:rerun-if-env-changed instructions for all env variables affecting system-deps behaviour
620 flags.add(BuildFlag::RerunIfEnvChanged(
621 EnvVariable::new_build_internal(None),
622 ));
623 flags.add(BuildFlag::RerunIfEnvChanged(EnvVariable::new_link(None)));
624
625 for name in self.libs.keys() {
626 EnvVariable::set_rerun_if_changed_for_all_variants(&mut flags, name);
627 }
628
629 Ok(flags)
630 }
631}
632
633#[derive(Debug)]
634/// Error used in return value of `Config::add_build_internal` closures
635pub enum BuildInternalClosureError {
636 /// `pkg-config` error
637 PkgConfig(pkg_config::Error),
638 /// General failure
639 Failed(String),
640}
641
642impl From<pkg_config::Error> for BuildInternalClosureError {
643 fn from(err: pkg_config::Error) -> Self {
644 Self::PkgConfig(err)
645 }
646}
647
648impl BuildInternalClosureError {
649 /// Create a new `BuildInternalClosureError::Failed` representing a general
650 /// failure.
651 ///
652 /// # Arguments
653 ///
654 /// * `details`: human-readable details about the failure
655 pub fn failed(details: &str) -> Self {
656 Self::Failed(details.to_string())
657 }
658}
659
660impl std::error::Error for BuildInternalClosureError {
661 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
662 match self {
663 Self::PkgConfig(e) => Some(e),
664 _ => None,
665 }
666 }
667}
668
669impl fmt::Display for BuildInternalClosureError {
670 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
671 match self {
672 Self::PkgConfig(e) => write!(f, "{e}"),
673 Self::Failed(s) => write!(f, "{s}"),
674 }
675 }
676}
677
678// Enum representing the environment variables user can define to tune system-deps.
679#[derive(Debug, PartialEq)]
680enum EnvVariable {
681 Lib(String),
682 LibFramework(String),
683 SearchNative(String),
684 SearchFramework(String),
685 Include(String),
686 NoPkgConfig(String),
687 BuildInternal(Option<String>),
688 Link(Option<String>),
689 LinkerArgs(String),
690 NoPrebuilt(Option<String>),
691}
692
693impl EnvVariable {
694 fn new_lib(lib: &str) -> Self {
695 Self::Lib(lib.to_string())
696 }
697
698 fn new_lib_framework(lib: &str) -> Self {
699 Self::LibFramework(lib.to_string())
700 }
701
702 fn new_search_native(lib: &str) -> Self {
703 Self::SearchNative(lib.to_string())
704 }
705
706 fn new_search_framework(lib: &str) -> Self {
707 Self::SearchFramework(lib.to_string())
708 }
709
710 fn new_include(lib: &str) -> Self {
711 Self::Include(lib.to_string())
712 }
713
714 fn new_linker_args(lib: &str) -> Self {
715 Self::LinkerArgs(lib.to_string())
716 }
717
718 fn new_no_pkg_config(lib: &str) -> Self {
719 Self::NoPkgConfig(lib.to_string())
720 }
721
722 fn new_build_internal(lib: Option<&str>) -> Self {
723 Self::BuildInternal(lib.map(|l| l.to_string()))
724 }
725
726 fn new_link(lib: Option<&str>) -> Self {
727 Self::Link(lib.map(|l| l.to_string()))
728 }
729
730 fn new_no_prebuilt(lib: Option<&str>) -> Self {
731 Self::NoPrebuilt(lib.map(|l| l.to_string()))
732 }
733
734 const fn suffix(&self) -> &'static str {
735 match self {
736 EnvVariable::Lib(_) => "LIB",
737 EnvVariable::LibFramework(_) => "LIB_FRAMEWORK",
738 EnvVariable::SearchNative(_) => "SEARCH_NATIVE",
739 EnvVariable::SearchFramework(_) => "SEARCH_FRAMEWORK",
740 EnvVariable::Include(_) => "INCLUDE",
741 EnvVariable::NoPkgConfig(_) => "NO_PKG_CONFIG",
742 EnvVariable::BuildInternal(_) => "BUILD_INTERNAL",
743 EnvVariable::Link(_) => "LINK",
744 EnvVariable::LinkerArgs(_) => "LDFLAGS",
745 EnvVariable::NoPrebuilt(_) => "NO_PREBUILT",
746 }
747 }
748
749 fn set_rerun_if_changed_for_all_variants(flags: &mut BuildFlags, name: &str) {
750 #[inline]
751 fn add_to_flags(flags: &mut BuildFlags, var: EnvVariable) {
752 flags.add(BuildFlag::RerunIfEnvChanged(var));
753 }
754 add_to_flags(flags, EnvVariable::new_lib(name));
755 add_to_flags(flags, EnvVariable::new_lib_framework(name));
756 add_to_flags(flags, EnvVariable::new_search_native(name));
757 add_to_flags(flags, EnvVariable::new_search_framework(name));
758 add_to_flags(flags, EnvVariable::new_include(name));
759 add_to_flags(flags, EnvVariable::new_linker_args(name));
760 add_to_flags(flags, EnvVariable::new_no_pkg_config(name));
761 add_to_flags(flags, EnvVariable::new_build_internal(Some(name)));
762 add_to_flags(flags, EnvVariable::new_link(Some(name)));
763 add_to_flags(flags, EnvVariable::new_no_prebuilt(Some(name)));
764 }
765}
766
767impl fmt::Display for EnvVariable {
768 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
769 let suffix = match self {
770 EnvVariable::Lib(lib)
771 | EnvVariable::LibFramework(lib)
772 | EnvVariable::SearchNative(lib)
773 | EnvVariable::SearchFramework(lib)
774 | EnvVariable::Include(lib)
775 | EnvVariable::LinkerArgs(lib)
776 | EnvVariable::NoPkgConfig(lib)
777 | EnvVariable::BuildInternal(Some(lib))
778 | EnvVariable::Link(Some(lib))
779 | EnvVariable::NoPrebuilt(Some(lib)) => {
780 format!("{}_{}", lib.to_shouty_snake_case(), self.suffix())
781 }
782 EnvVariable::BuildInternal(None)
783 | EnvVariable::Link(None)
784 | EnvVariable::NoPrebuilt(None) => self.suffix().to_string(),
785 };
786 write!(f, "SYSTEM_DEPS_{suffix}")
787 }
788}
789
790type FnBuildInternal =
791 dyn FnOnce(&str, &str) -> std::result::Result<Library, BuildInternalClosureError>;
792
793/// Restores environment variables to their saved values when dropped.
794struct EnvRestore(Vec<(String, Option<OsString>)>);
795
796impl Drop for EnvRestore {
797 fn drop(&mut self) {
798 for (var, value) in &self.0 {
799 match value {
800 Some(val) => env::set_var(var, val),
801 None => env::remove_var(var),
802 }
803 }
804 }
805}
806
807/// Structure used to configure `metadata` before starting to probe for dependencies
808pub struct Config {
809 env: EnvVariables,
810 build_internals: HashMap<String, Box<FnBuildInternal>>,
811 #[cfg(feature = "binary")]
812 paths: &'static system_deps_meta::binary::Paths,
813}
814
815impl Default for Config {
816 fn default() -> Self {
817 Self::new_with_env(EnvVariables::Environment)
818 }
819}
820
821impl Config {
822 /// Create a new set of configuration
823 pub fn new() -> Self {
824 Self::default()
825 }
826
827 fn new_with_env(env: EnvVariables) -> Self {
828 let me = Self {
829 env,
830 build_internals: HashMap::new(),
831 #[cfg(feature = "binary")]
832 paths: {
833 // Constructed by reading the serialized value saved by the build script
834 const CONTENT: &str = include_str!(env!("SYSTEM_DEPS_BINARY_PATHS"));
835 static PATHS: std::sync::OnceLock<system_deps_meta::binary::Paths> =
836 std::sync::OnceLock::new();
837 PATHS.get_or_init(|| {
838 toml::from_str(CONTENT)
839 .expect("The build script should output valid serialization")
840 })
841 },
842 };
843
844 // Prefer the pkg-config shipped by a downloaded bundle. The bundle's
845 // `.pc` files (absolute paths, prefix expectations, MSVC quirks on
846 // Windows) are only guaranteed to be readable by the pkg-config that
847 // ships with it; system pkg-configs in `PATH` are often misconfigured
848 // (Strawberry Perl, MSYS2, chocolatey, distro forks) and silently
849 // produce wrong flags. Only an explicit `PKG_CONFIG` env var
850 // overrides this.
851 #[cfg(feature = "binary")]
852 if env::var_os("PKG_CONFIG").is_none() {
853 if let Some(pc) = me.paths.pkg_config_binary() {
854 env::set_var("PKG_CONFIG", pc);
855 }
856 }
857
858 me
859 }
860
861 /// Probe all libraries configured in the Cargo.toml
862 /// `[package.metadata.system-deps]` section.
863 ///
864 /// The returned hash is using the `toml` key defining the dependency as key.
865 pub fn probe(self) -> Result<Dependencies, Error> {
866 let target = self.env.get("TARGET");
867 let libraries = self.probe_full()?;
868 let flags = libraries.gen_flags(target.as_deref())?;
869
870 // Output cargo flags
871 println!("{flags}");
872
873 for (name, _) in libraries.iter() {
874 println!("cargo:rustc-cfg=system_deps_have_{}", name.to_snake_case());
875 }
876
877 Ok(libraries)
878 }
879
880 /// Add hook so system-deps can internally build library `name` if requested by user.
881 ///
882 /// It will only be triggered if the environment variable
883 /// `SYSTEM_DEPS_$NAME_BUILD_INTERNAL` is defined with either `always` or
884 /// `auto` as value. In the latter case, `func` is called only if the requested
885 /// version of the library was not found on the system.
886 ///
887 /// # Arguments
888 /// * `name`: the name of the library, as defined in `Cargo.toml`
889 /// * `func`: closure called when internally building the library.
890 ///
891 /// It receives as argument the library name, and the minimum version required.
892 pub fn add_build_internal<F>(mut self, name: &str, func: F) -> Self
893 where
894 F: 'static + FnOnce(&str, &str) -> std::result::Result<Library, BuildInternalClosureError>,
895 {
896 let mut build_internals = self.build_internals;
897 build_internals.insert(name.to_string(), Box::new(func));
898
899 self.build_internals = build_internals;
900 self
901 }
902
903 /// Checks the map from packages to their provided prebuilt binaries locations, if available.
904 pub fn query_path(&self, _pkg: &str) -> Option<&'static Vec<PathBuf>> {
905 #[cfg(not(feature = "binary"))]
906 return None;
907
908 #[cfg(feature = "binary")]
909 self.env
910 .get(&EnvVariable::new_no_prebuilt(Some(_pkg)))
911 .or(self.env.get(&EnvVariable::new_no_prebuilt(None)))
912 .map_or_else(|| self.paths.get(_pkg), |_| None)
913 }
914
915 fn probe_full(mut self) -> Result<Dependencies, Error> {
916 let mut libraries = self.probe_pkg_config()?;
917 libraries.override_from_flags(&self.env);
918 Ok(libraries)
919 }
920
921 fn probe_pkg_config(&mut self) -> Result<Dependencies, Error> {
922 let dir = self
923 .env
924 .get("CARGO_MANIFEST_DIR")
925 .ok_or_else(|| Error::InvalidMetadata("$CARGO_MANIFEST_DIR not set".into()))?;
926 let mut path = PathBuf::from(dir);
927 path.push("Cargo.toml");
928
929 println!("cargo:rerun-if-changed={}", path.to_string_lossy());
930
931 let metadata = MetaData::from_file(&path)?;
932 let mut libraries = Dependencies::default();
933
934 // Collect keys that have an unconditional (non-cfg) dep so we can skip
935 // cfg-gated duplicates that only carry binary metadata.
936 let unconditional_keys: std::collections::HashSet<_> = metadata
937 .deps
938 .iter()
939 .filter(|d| d.cfg.is_none())
940 .map(|d| &d.key)
941 .collect();
942
943 for dep in metadata.deps.iter() {
944 if let Some(cfg) = &dep.cfg {
945 // Check if `cfg()` expression matches the target settings
946 if !self.check_cfg(cfg)? {
947 continue;
948 }
949 // Skip cfg-gated deps whose key also has an unconditional entry.
950 // The cfg-gated entry typically only carries binary-related metadata
951 // (url, checksum, etc.) handled by system-deps-meta, not here.
952 if unconditional_keys.contains(&dep.key) {
953 continue;
954 }
955 }
956
957 let mut enabled_feature_overrides = Vec::new();
958
959 for o in dep.version_overrides.iter() {
960 if self.has_feature(&o.key) {
961 enabled_feature_overrides.push(o);
962 }
963 }
964
965 if let Some(feature) = dep.feature.as_ref() {
966 if !self.has_feature(feature) {
967 continue;
968 }
969 }
970
971 // Pick the highest feature enabled version
972
973 let (version, lib_name, fallback_lib_names, optional) =
974 if enabled_feature_overrides.is_empty() {
975 (
976 dep.version.as_deref(),
977 dep.lib_name(),
978 dep.fallback_names.as_deref().unwrap_or(&[]),
979 dep.optional,
980 )
981 } else {
982 enabled_feature_overrides.sort_by(|a, b| {
983 fn min_version(r: metadata::VersionRange<'_>) -> &str {
984 match r.start_bound() {
985 std::ops::Bound::Unbounded => unreachable!(),
986 std::ops::Bound::Excluded(_) => unreachable!(),
987 std::ops::Bound::Included(b) => b,
988 }
989 }
990
991 let a = min_version(metadata::parse_version(&a.version));
992 let b = min_version(metadata::parse_version(&b.version));
993
994 version_compare::compare(a, b)
995 .expect("failed to compare versions")
996 .ord()
997 .expect("invalid version")
998 });
999 let highest = enabled_feature_overrides.into_iter().next_back().unwrap();
1000
1001 (
1002 Some(highest.version.as_str()),
1003 highest.name.as_deref().unwrap_or(dep.lib_name()),
1004 highest
1005 .fallback_names
1006 .as_deref()
1007 .or(dep.fallback_names.as_deref())
1008 .unwrap_or(&[]),
1009 highest.optional.unwrap_or(dep.optional),
1010 )
1011 };
1012
1013 let version = version.ok_or_else(|| {
1014 Error::InvalidMetadata(format!("No version defined for {}", dep.key))
1015 })?;
1016
1017 let name = &dep.key;
1018 let build_internal = self.get_build_internal_status(name)?;
1019
1020 // `Some` only for a dep served by a downloaded binary bundle.
1021 // TODO: Pass package version here
1022 let prebuilt_pkg_config_paths = self.query_path(name);
1023
1024 // should the lib be statically linked?
1025 let statik = cfg!(feature = "binary")
1026 || self
1027 .env
1028 .has_value(&EnvVariable::new_link(Some(name)), "static")
1029 || self.env.has_value(&EnvVariable::new_link(None), "static");
1030
1031 let mut library = if self.env.contains(&EnvVariable::new_no_pkg_config(name)) {
1032 Library::from_env_variables(name)
1033 } else if build_internal == BuildInternal::Always {
1034 self.call_build_internal(lib_name, version)?
1035 } else {
1036 let mut config = pkg_config::Config::new();
1037 config
1038 .print_system_libs(false)
1039 .cargo_metadata(false)
1040 .range_version(metadata::parse_version(version))
1041 .statik(statik);
1042
1043 // Bundles ship pkg-config 0.29.2, whose `--define-prefix` (on by
1044 // default on Windows) miscomputes `prefix` for nested layouts
1045 // like `lib/gstreamer-1.0/pkgconfig`, inflating `${libdir}` to
1046 // `<bundle>/lib/lib`. The bundled `.pc` carry correct
1047 // `prefix=${pcfiledir}/...`, so disable it, but only when the
1048 // tool accepts the flag (older ones lack the behaviour anyway).
1049 #[cfg(windows)]
1050 if prebuilt_pkg_config_paths.is_some() && pkg_config_accepts_dont_define_prefix() {
1051 config.arg("--dont-define-prefix");
1052 }
1053
1054 let probe = || Self::probe_with_fallback(&config, lib_name, fallback_lib_names);
1055 let probe = match prebuilt_pkg_config_paths {
1056 Some(paths) => Library::wrap_pkg_config(paths, probe),
1057 None => probe(),
1058 };
1059
1060 match probe {
1061 Ok((lib_name, lib)) => Library::from_pkg_config(lib_name, lib),
1062 Err(e) => {
1063 if build_internal == BuildInternal::Auto {
1064 // Try building the lib internally as a fallback
1065 self.call_build_internal(name, version)?
1066 } else if optional {
1067 // If the dep is optional just skip it
1068 continue;
1069 } else {
1070 return Err(e.into());
1071 }
1072 }
1073 }
1074 };
1075
1076 library.statik = statik;
1077
1078 libraries.add(name, library);
1079 }
1080
1081 Ok(libraries)
1082 }
1083
1084 fn probe_with_fallback<'a>(
1085 config: &'a pkg_config::Config,
1086 name: &'a str,
1087 fallback_names: &'a [String],
1088 ) -> Result<(&'a str, pkg_config::Library), pkg_config::Error> {
1089 let error = match config.probe(name) {
1090 Ok(x) => return Ok((name, x)),
1091 Err(e) => e,
1092 };
1093 for name in fallback_names {
1094 if let Ok(library) = config.probe(name) {
1095 return Ok((name, library));
1096 }
1097 }
1098 Err(error)
1099 }
1100
1101 fn get_build_internal_env_var(&self, var: EnvVariable) -> Result<Option<BuildInternal>, Error> {
1102 match self.env.get(&var).as_deref() {
1103 Some(s) => {
1104 let b = BuildInternal::from_str(s).map_err(|_| {
1105 Error::BuildInternalInvalid(format!(
1106 "Invalid value in {var}: {s} (allowed: 'auto', 'always', 'never')"
1107 ))
1108 })?;
1109 Ok(Some(b))
1110 }
1111 None => Ok(None),
1112 }
1113 }
1114
1115 fn get_build_internal_status(&self, name: &str) -> Result<BuildInternal, Error> {
1116 match self.get_build_internal_env_var(EnvVariable::new_build_internal(Some(name)))? {
1117 Some(b) => Ok(b),
1118 None => Ok(self
1119 .get_build_internal_env_var(EnvVariable::new_build_internal(None))?
1120 .unwrap_or_default()),
1121 }
1122 }
1123
1124 fn call_build_internal(&mut self, name: &str, version_str: &str) -> Result<Library, Error> {
1125 let lib = match self.build_internals.remove(name) {
1126 Some(f) => f(name, version_str)
1127 .map_err(|e| Error::BuildInternalClosureError(name.into(), e))?,
1128 None => {
1129 return Err(Error::BuildInternalNoClosure(
1130 name.into(),
1131 version_str.into(),
1132 ))
1133 }
1134 };
1135
1136 // Check that the lib built internally matches the required version
1137 let version = metadata::parse_version(version_str);
1138 fn min_version(r: metadata::VersionRange<'_>) -> &str {
1139 match r.start_bound() {
1140 std::ops::Bound::Unbounded => unreachable!(),
1141 std::ops::Bound::Excluded(_) => unreachable!(),
1142 std::ops::Bound::Included(b) => b,
1143 }
1144 }
1145 fn max_version(r: metadata::VersionRange<'_>) -> Option<&str> {
1146 match r.end_bound() {
1147 std::ops::Bound::Included(_) => unreachable!(),
1148 std::ops::Bound::Unbounded => None,
1149 std::ops::Bound::Excluded(b) => Some(*b),
1150 }
1151 }
1152
1153 let min = min_version(version.clone());
1154 if version_compare::compare(&lib.version, min) == Ok(version_compare::Cmp::Lt) {
1155 return Err(Error::BuildInternalWrongVersion(
1156 name.into(),
1157 lib.version,
1158 version_str.into(),
1159 ));
1160 }
1161
1162 if let Some(max) = max_version(version) {
1163 if version_compare::compare(&lib.version, max) == Ok(version_compare::Cmp::Ge) {
1164 return Err(Error::BuildInternalWrongVersion(
1165 name.into(),
1166 lib.version,
1167 version_str.into(),
1168 ));
1169 }
1170 }
1171
1172 Ok(lib)
1173 }
1174
1175 fn has_feature(&self, feature: &str) -> bool {
1176 let var: &str = &format!("CARGO_FEATURE_{}", feature.to_uppercase().replace('-', "_"));
1177 self.env.contains(var)
1178 }
1179
1180 fn check_cfg(&self, cfg: &cfg_expr::Expression) -> Result<bool, Error> {
1181 use cfg_expr::{targets::get_builtin_target_by_triple, Predicate};
1182
1183 let target = self
1184 .env
1185 .get("TARGET")
1186 .expect("no TARGET env variable defined");
1187
1188 let res = if let Some(target) = get_builtin_target_by_triple(&target) {
1189 cfg.eval(|pred| match pred {
1190 Predicate::Target(tp) => Some(tp.matches(target)),
1191 _ => None,
1192 })
1193 } else {
1194 // Attempt to parse the triple, the target is not an official builtin
1195 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));
1196
1197 cfg.eval(|pred| match pred {
1198 Predicate::Target(tp) => Some(tp.matches(&triple)),
1199 _ => None,
1200 })
1201 };
1202
1203 res.ok_or_else(|| Error::UnsupportedCfg(cfg.original().to_string()))
1204 }
1205}
1206
1207#[derive(Debug, PartialEq, Eq)]
1208/// From where the library settings have been retrieved
1209pub enum Source {
1210 /// Settings have been retrieved from `pkg-config`
1211 PkgConfig,
1212 /// Settings have been defined using user defined environment variables
1213 EnvVariables,
1214}
1215
1216#[derive(Debug, PartialEq, Eq)]
1217/// Internal library name and if a static library is available on the system
1218pub struct InternalLib {
1219 /// Name of the library
1220 pub name: String,
1221 /// Indicates if a static library is available on the system
1222 pub is_static_available: bool,
1223}
1224
1225impl InternalLib {
1226 const fn new(name: String, is_static_available: bool) -> Self {
1227 InternalLib {
1228 name,
1229 is_static_available,
1230 }
1231 }
1232}
1233
1234#[derive(Debug)]
1235/// A system dependency
1236pub struct Library {
1237 /// Name of the library
1238 pub name: String,
1239 /// From where the library settings have been retrieved
1240 pub source: Source,
1241 /// libraries the linker should link on
1242 pub libs: Vec<InternalLib>,
1243 /// directories where the compiler should look for libraries
1244 pub link_paths: Vec<PathBuf>,
1245 /// absolute paths to library files (e.g., from pkg-config output like `/path/to/libfoo.a`)
1246 pub link_files: Vec<PathBuf>,
1247 /// frameworks the linker should link on
1248 pub frameworks: Vec<String>,
1249 /// directories where the compiler should look for frameworks
1250 pub framework_paths: Vec<PathBuf>,
1251 /// directories where the compiler should look for header files
1252 pub include_paths: Vec<PathBuf>,
1253 /// flags that should be passed to the linker
1254 pub ld_args: Vec<Vec<String>>,
1255 /// macros that should be defined by the compiler
1256 pub defines: HashMap<String, Option<String>>,
1257 /// library version
1258 pub version: String,
1259 /// library is statically linked
1260 pub statik: bool,
1261}
1262
1263impl Library {
1264 fn from_pkg_config(name: &str, l: pkg_config::Library) -> Self {
1265 // taken from: https://github.com/rust-lang/pkg-config-rs/blob/54325785816695df031cef3b26b6a9a203bbc01b/src/lib.rs#L502
1266 let system_roots = if cfg!(target_os = "macos") {
1267 vec![PathBuf::from("/Library"), PathBuf::from("/System")]
1268 } else {
1269 let sysroot = env::var_os("PKG_CONFIG_SYSROOT_DIR")
1270 .or_else(|| env::var_os("SYSROOT"))
1271 .map(PathBuf::from);
1272
1273 if cfg!(target_os = "windows") {
1274 if let Some(sysroot) = sysroot {
1275 vec![sysroot]
1276 } else {
1277 vec![]
1278 }
1279 } else {
1280 vec![sysroot.unwrap_or_else(|| PathBuf::from("/usr"))]
1281 }
1282 };
1283
1284 // With `--static`, pkgconf repeats the same `-L` for every library in
1285 // the `Requires.private` tree, so check each directory and library once.
1286 let mut dirs: Vec<&PathBuf> = Vec::new();
1287 for dir in &l.link_paths {
1288 if !dirs.contains(&dir) {
1289 dirs.push(dir);
1290 }
1291 }
1292 let is_static_available = |name: &String| -> bool {
1293 // MSVC: don't claim a static archive is "available" even when
1294 // it is on disk. Emitting `cargo:rustc-link-lib=static=...`
1295 // makes rustc try to bundle the archive into the rlib, which
1296 // overflows `ar_archive_writer` (u32 limits) on archives the
1297 // size of gst-plugins-rs's `lib*.a`. Without the modifier,
1298 // MSVC `link.exe` resolves `name.lib` natively from the bundle.
1299 // Upstream issue:
1300 // https://github.com/rust-lang/ar_archive_writer/issues/31
1301 if cfg!(all(target_os = "windows", target_env = "msvc")) {
1302 return false;
1303 }
1304 let libnames = {
1305 let mut names = vec![format!("lib{name}.a")];
1306 if cfg!(target_os = "windows") {
1307 names.push(format!("{name}.lib"));
1308 }
1309 names
1310 };
1311
1312 dirs.iter().any(|dir| {
1313 let library_exists = libnames.iter().any(|libname| dir.join(libname).exists());
1314 library_exists && !system_roots.iter().any(|sys| dir.starts_with(sys))
1315 })
1316 };
1317
1318 let mut cache: HashMap<&String, bool> = HashMap::new();
1319 let libs = l
1320 .libs
1321 .iter()
1322 .map(|lib| {
1323 let available = *cache.entry(lib).or_insert_with(|| is_static_available(lib));
1324 InternalLib::new(lib.to_owned(), available)
1325 })
1326 .collect();
1327
1328 Self {
1329 name: name.to_string(),
1330 source: Source::PkgConfig,
1331 libs,
1332 link_paths: l.link_paths,
1333 link_files: l.link_files,
1334 include_paths: l.include_paths,
1335 ld_args: l.ld_args,
1336 frameworks: l.frameworks,
1337 framework_paths: l.framework_paths,
1338 defines: l.defines,
1339 version: l.version,
1340 statik: false,
1341 }
1342 }
1343
1344 fn from_env_variables(name: &str) -> Self {
1345 Self {
1346 name: name.to_string(),
1347 source: Source::EnvVariables,
1348 libs: Vec::new(),
1349 link_paths: Vec::new(),
1350 link_files: Vec::new(),
1351 include_paths: Vec::new(),
1352 ld_args: Vec::new(),
1353 frameworks: Vec::new(),
1354 framework_paths: Vec::new(),
1355 defines: HashMap::new(),
1356 version: String::new(),
1357 statik: false,
1358 }
1359 }
1360
1361 /// Calls a function changing the environment so that `pkg-config` will try to
1362 /// look first in the provided path.
1363 pub fn wrap_pkg_config<T, R>(
1364 pkg_config_paths: impl PathOrList<T>,
1365 f: impl FnOnce() -> Result<R, pkg_config::Error>,
1366 ) -> Result<R, pkg_config::Error> {
1367 // Save current PKG_CONFIG_PATH, so we can restore it.
1368 //
1369 // Nix's pkg-config wrapper injects the store paths of all build inputs
1370 // and gives `PKG_CONFIG_PATH_FOR_TARGET` precedence over
1371 // `PKG_CONFIG_PATH`. For this to work inside Nix shells we need to
1372 // handle the `_FOR_TARGET` variant as well.
1373 //
1374 // The `pkg-config` crate also prefers the target and host specific
1375 // variants of `PKG_CONFIG_PATH` when they are set.
1376 let mut vars = vec![
1377 ("PKG_CONFIG_PATH".to_string(), true),
1378 ("PKG_CONFIG_PATH_FOR_TARGET".to_string(), false),
1379 ];
1380 if let (Ok(target), Ok(host)) = (env::var("TARGET"), env::var("HOST")) {
1381 let kind = if host == target { "HOST" } else { "TARGET" };
1382 let target_u = target.replace('-', "_");
1383 vars.push((format!("PKG_CONFIG_PATH_{}", target), false));
1384 if target_u != target {
1385 vars.push((format!("PKG_CONFIG_PATH_{}", target_u), false));
1386 }
1387 vars.push((format!("{}_PKG_CONFIG_PATH", kind), false));
1388 }
1389 let _restore = EnvRestore(
1390 vars.iter()
1391 .map(|(var, _)| (var.clone(), env::var_os(var)))
1392 .collect(),
1393 );
1394
1395 for (var, always) in &vars {
1396 let value = env::var_os(var);
1397 let paths: Vec<_> = value.iter().flat_map(env::split_paths).collect();
1398 let joined = pkg_config_paths.join_paths(&paths);
1399 if value.is_none() && (!always || joined.is_empty()) {
1400 continue;
1401 }
1402
1403 // pkg-config 0.29.2 eats `\` while expanding `${pcfiledir}`, producing
1404 // corrupt flags (e.g. `C:Userslibpkgconfig`). Forward slashes work on
1405 // Windows and avoid this; there `\` is only ever a path separator.
1406 #[cfg(windows)]
1407 let joined = OsString::from(joined.to_string_lossy().replace('\\', "/"));
1408
1409 env::set_var(var, joined);
1410 }
1411
1412 f()
1413 }
1414
1415 /// Create a `Library` by probing `pkg-config` on an internal directory.
1416 /// This helper is meant to be used by `Config::add_build_internal` closures
1417 /// after having built the lib to return the library information to system-deps.
1418 ///
1419 /// This library will be statically linked.
1420 ///
1421 /// # Arguments
1422 ///
1423 /// * `pkg_config_dir`: the directory where the library `.pc` file is located
1424 /// * `lib`: the name of the library to look for
1425 /// * `version`: the minimum version of `lib` required, or `"0"` to accept any version
1426 ///
1427 /// # Examples
1428 ///
1429 /// ```
1430 /// let mut config = system_deps::Config::new();
1431 /// config.add_build_internal("mylib", |lib, version| {
1432 /// // Actually build the library here that fulfills the passed in version requirements
1433 /// system_deps::Library::from_internal_pkg_config("build-dir",
1434 /// lib, "1.2.4")
1435 /// });
1436 /// ```
1437 pub fn from_internal_pkg_config<T>(
1438 pkg_config_paths: impl PathOrList<T>,
1439 lib: &str,
1440 version: &str,
1441 ) -> Result<Self, BuildInternalClosureError> {
1442 let pkg_lib = Self::wrap_pkg_config(pkg_config_paths, || {
1443 // The `pkg-config` crate passes the bare name besides the version
1444 // constraint, and with `--static` pkgconf 1.8 then prints the private
1445 // flags twice. `>= 0` would only reject empty or non-numeric versions.
1446 let mut config = pkg_config::Config::new();
1447 if version != "0" {
1448 config.atleast_version(version);
1449 }
1450 config
1451 .print_system_libs(false)
1452 .cargo_metadata(false)
1453 .statik(true)
1454 .probe(lib)
1455 })?;
1456
1457 let mut lib = Self::from_pkg_config(lib, pkg_lib);
1458 lib.statik = true;
1459 Ok(lib)
1460 }
1461}
1462
1463/// Whether the resolved `pkg-config` accepts `--dont-define-prefix`. Cached.
1464#[cfg(windows)]
1465fn pkg_config_accepts_dont_define_prefix() -> bool {
1466 use std::sync::OnceLock;
1467 static SUPPORTED: OnceLock<bool> = OnceLock::new();
1468 *SUPPORTED.get_or_init(|| {
1469 let exe = env::var_os("PKG_CONFIG").unwrap_or_else(|| "pkg-config".into());
1470 std::process::Command::new(exe)
1471 .args(["--dont-define-prefix", "--version"])
1472 .output()
1473 .map(|out| out.status.success())
1474 .unwrap_or(false)
1475 })
1476}
1477
1478/// A trait that can represent both a reference to a Path like object or a list of paths.
1479/// Used in `Library::wrap_pkg_config` and `Library::from_internal_pkg_config` to specify
1480/// the list of `pkg-config` paths that should take priority.
1481pub trait PathOrList<T> {
1482 /// Creates an string of paths appropiately joined for an environment variable.
1483 /// The paths in `self` will go before the paths in `other`.
1484 fn join_paths(&self, other: impl AsRef<[PathBuf]>) -> OsString;
1485}
1486
1487impl<T: AsRef<Path>> PathOrList<T> for T {
1488 fn join_paths(&self, other: impl AsRef<[PathBuf]>) -> OsString {
1489 let other = other.as_ref().iter().map(|p| p.as_path());
1490 env::join_paths(iter::once(self.as_ref()).chain(other))
1491 .expect("Path contains invalid character")
1492 }
1493}
1494
1495impl<T: AsRef<Path>, S: Borrow<[T]>> PathOrList<(T, S)> for &S {
1496 fn join_paths(&self, other: impl AsRef<[PathBuf]>) -> OsString {
1497 let slice: &[T] = (*self).borrow();
1498 let other = other.as_ref().iter().map(|p| p.as_path());
1499 env::join_paths(slice.iter().map(|p| p.as_ref()).chain(other))
1500 .expect("Path contains invalid character")
1501 }
1502}
1503
1504impl<T: PathOrList<S>, S> PathOrList<(T, S)> for Option<T> {
1505 fn join_paths(&self, other: impl AsRef<[PathBuf]>) -> OsString {
1506 match self {
1507 Some(s) => s.join_paths(other),
1508 None => env::join_paths(other.as_ref().iter().map(|p| p.as_os_str()))
1509 .expect("Path contains invalid character"),
1510 }
1511 }
1512}
1513
1514#[derive(Debug)]
1515enum EnvVariables {
1516 Environment,
1517 #[cfg(test)]
1518 Mock(HashMap<&'static str, String>),
1519}
1520
1521trait EnvVariablesExt<T> {
1522 fn contains(&self, var: T) -> bool {
1523 self.get(var).is_some()
1524 }
1525
1526 fn get(&self, var: T) -> Option<String>;
1527
1528 fn has_value(&self, var: T, val: &str) -> bool {
1529 match self.get(var) {
1530 Some(v) => v == val,
1531 None => false,
1532 }
1533 }
1534}
1535
1536impl EnvVariablesExt<&str> for EnvVariables {
1537 fn get(&self, var: &str) -> Option<String> {
1538 match self {
1539 EnvVariables::Environment => env::var(var).ok(),
1540 #[cfg(test)]
1541 EnvVariables::Mock(vars) => vars.get(var).cloned(),
1542 }
1543 }
1544}
1545
1546impl EnvVariablesExt<&EnvVariable> for EnvVariables {
1547 fn get(&self, var: &EnvVariable) -> Option<String> {
1548 let s = var.to_string();
1549 let var: &str = s.as_ref();
1550 self.get(var)
1551 }
1552}
1553
1554#[derive(Debug, PartialEq)]
1555enum BuildFlag {
1556 Include(String),
1557 SearchNative(String),
1558 SearchFramework(String),
1559 Lib(String, bool), // true if static
1560 LibFramework(String),
1561 RerunIfEnvChanged(EnvVariable),
1562 LinkArg(Vec<String>),
1563}
1564
1565impl fmt::Display for BuildFlag {
1566 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1567 match self {
1568 BuildFlag::Include(paths) => write!(f, "include={paths}"),
1569 BuildFlag::SearchNative(lib) => write!(f, "rustc-link-search=native={lib}"),
1570 BuildFlag::SearchFramework(lib) => write!(f, "rustc-link-search=framework={lib}"),
1571 BuildFlag::Lib(lib, statik) => {
1572 if *statik {
1573 write!(f, "rustc-link-lib=static={lib}")
1574 } else {
1575 write!(f, "rustc-link-lib={lib}")
1576 }
1577 }
1578 BuildFlag::LibFramework(lib) => write!(f, "rustc-link-lib=framework={lib}"),
1579 BuildFlag::RerunIfEnvChanged(env) => write!(f, "rerun-if-env-changed={env}"),
1580 BuildFlag::LinkArg(ld_option) => {
1581 write!(f, "rustc-link-arg=-Wl,{}", ld_option.join(","))
1582 }
1583 }
1584 }
1585}
1586
1587#[derive(Debug, PartialEq)]
1588struct BuildFlags(Vec<BuildFlag>);
1589
1590impl BuildFlags {
1591 const fn new() -> Self {
1592 Self(Vec::new())
1593 }
1594
1595 fn add(&mut self, flag: BuildFlag) {
1596 self.0.push(flag);
1597 }
1598}
1599
1600impl fmt::Display for BuildFlags {
1601 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1602 for flag in self.0.iter() {
1603 writeln!(f, "cargo:{flag}")?;
1604 }
1605 Ok(())
1606 }
1607}
1608
1609fn split_paths(value: &str) -> Vec<PathBuf> {
1610 if !value.is_empty() {
1611 let paths = env::split_paths(&value);
1612 paths.map(|p| Path::new(&p).into()).collect()
1613 } else {
1614 Vec::new()
1615 }
1616}
1617
1618fn split_string(value: &str) -> Vec<String> {
1619 if !value.is_empty() {
1620 value.split(' ').map(|s| s.to_string()).collect()
1621 } else {
1622 Vec::new()
1623 }
1624}
1625
1626#[derive(Debug, PartialEq, Default)]
1627enum BuildInternal {
1628 Auto,
1629 Always,
1630 #[default]
1631 Never,
1632}
1633
1634impl FromStr for BuildInternal {
1635 type Err = ParseError;
1636
1637 fn from_str(s: &str) -> Result<Self, Self::Err> {
1638 match s {
1639 "auto" => Ok(Self::Auto),
1640 "always" => Ok(Self::Always),
1641 "never" => Ok(Self::Never),
1642 v => Err(ParseError::VariantNotFound(v.to_owned())),
1643 }
1644 }
1645}
1646
1647#[derive(Debug, PartialEq)]
1648enum ParseError {
1649 VariantNotFound(String),
1650}
1651
1652impl std::error::Error for ParseError {}
1653
1654impl fmt::Display for ParseError {
1655 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1656 match self {
1657 Self::VariantNotFound(v) => write!(f, "Unknown variant: `{v}`"),
1658 }
1659 }
1660}