| //! A build dependency for Cargo libraries to find system artifacts through the |
| //! `pkg-config` utility. |
| //! |
| //! This library will shell out to `pkg-config` as part of build scripts and |
| //! probe the system to determine how to link to a specified library. The |
| //! `Config` structure serves as a method of configuring how `pkg-config` is |
| //! invoked in a builder style. |
| //! |
| //! After running `pkg-config` all appropriate Cargo metadata will be printed on |
| //! stdout if the search was successful. |
| //! |
| //! # Environment variables |
| //! |
| //! A number of environment variables are available to globally configure how |
| //! this crate will invoke `pkg-config`: |
| //! |
| //! * `FOO_NO_PKG_CONFIG` - if set, this will disable running `pkg-config` when |
| //! probing for the library named `foo`. |
| //! |
| //! ### Linking |
| //! |
| //! There are also a number of environment variables which can configure how a |
| //! library is linked to (dynamically vs statically). These variables control |
| //! whether the `--static` flag is passed. Note that this behavior can be |
| //! overridden by configuring explicitly on `Config`. The variables are checked |
| //! in the following order: |
| //! |
| //! * `FOO_STATIC` - pass `--static` for the library `foo` |
| //! * `FOO_DYNAMIC` - do not pass `--static` for the library `foo` |
| //! * `PKG_CONFIG_ALL_STATIC` - pass `--static` for all libraries |
| //! * `PKG_CONFIG_ALL_DYNAMIC` - do not pass `--static` for all libraries |
| //! |
| //! ### Cross-compilation |
| //! |
| //! In cross-compilation context, it is useful to manage separately |
| //! `PKG_CONFIG_PATH` and a few other variables for the `host` and the `target` |
| //! platform. |
| //! |
| //! The supported variables are: `PKG_CONFIG_PATH`, `PKG_CONFIG_LIBDIR`, and |
| //! `PKG_CONFIG_SYSROOT_DIR`. |
| //! |
| //! Each of these variables can also be supplied with certain prefixes and |
| //! suffixes, in the following prioritized order: |
| //! |
| //! 1. `<var>_<target>` - for example, `PKG_CONFIG_PATH_x86_64-unknown-linux-gnu` |
| //! 2. `<var>_<target_with_underscores>` - for example, |
| //! `PKG_CONFIG_PATH_x86_64_unknown_linux_gnu` |
| //! 3. `<build-kind>_<var>` - for example, `HOST_PKG_CONFIG_PATH` or |
| //! `TARGET_PKG_CONFIG_PATH` |
| //! 4. `<var>` - a plain `PKG_CONFIG_PATH` |
| //! |
| //! This crate will allow `pkg-config` to be used in cross-compilation |
| //! if `PKG_CONFIG_SYSROOT_DIR` or `PKG_CONFIG` is set. You can set |
| //! `PKG_CONFIG_ALLOW_CROSS=1` to bypass the compatibility check, but please |
| //! note that enabling use of `pkg-config` in cross-compilation without |
| //! appropriate sysroot and search paths set is likely to break builds. |
| //! |
| //! # Example |
| //! |
| //! Find the system library named `foo`, with minimum version 1.2.3: |
| //! |
| //! ```no_run |
| //! fn main() { |
| //! pkg_config::Config::new().atleast_version("1.2.3").probe("foo").unwrap(); |
| //! } |
| //! ``` |
| //! |
| //! Find the system library named `foo`, with no version requirement (not |
| //! recommended): |
| //! |
| //! ```no_run |
| //! fn main() { |
| //! pkg_config::probe_library("foo").unwrap(); |
| //! } |
| //! ``` |
| //! |
| //! Configure how library `foo` is linked to. |
| //! |
| //! ```no_run |
| //! fn main() { |
| //! pkg_config::Config::new().atleast_version("1.2.3").statik(true).probe("foo").unwrap(); |
| //! } |
| //! ``` |
| |
| #![doc(html_root_url = "https://docs.rs/pkg-config/0.3")] |
| |
| use std::collections::HashMap; |
| use std::env; |
| use std::error; |
| use std::ffi::{OsStr, OsString}; |
| use std::fmt; |
| use std::fmt::Display; |
| use std::io; |
| use std::ops::{Bound, RangeBounds}; |
| use std::path::PathBuf; |
| use std::process::{Command, Output}; |
| use std::str; |
| |
| /// Wrapper struct to polyfill methods introduced in 1.57 (`get_envs`, `get_args` etc). |
| /// This is needed to reconstruct the pkg-config command for output in a copy- |
| /// paste friendly format via `Display`. |
| struct WrappedCommand { |
| inner: Command, |
| program: OsString, |
| env_vars: Vec<(OsString, OsString)>, |
| args: Vec<OsString>, |
| } |
| |
| #[derive(Clone, Debug)] |
| pub struct Config { |
| statik: Option<bool>, |
| min_version: Bound<String>, |
| max_version: Bound<String>, |
| extra_args: Vec<OsString>, |
| cargo_metadata: bool, |
| env_metadata: bool, |
| print_system_libs: bool, |
| print_system_cflags: bool, |
| } |
| |
| #[derive(Clone, Debug)] |
| pub struct Library { |
| /// Libraries specified by -l |
| pub libs: Vec<String>, |
| /// Library search paths specified by -L |
| pub link_paths: Vec<PathBuf>, |
| /// Library file paths specified without -l |
| pub link_files: Vec<PathBuf>, |
| /// Darwin frameworks specified by -framework |
| pub frameworks: Vec<String>, |
| /// Darwin framework search paths specified by -F |
| pub framework_paths: Vec<PathBuf>, |
| /// C/C++ header include paths specified by -I |
| pub include_paths: Vec<PathBuf>, |
| /// Linker options specified by -Wl |
| pub ld_args: Vec<Vec<String>>, |
| /// C/C++ definitions specified by -D |
| pub defines: HashMap<String, Option<String>>, |
| /// Version specified by .pc file's Version field |
| pub version: String, |
| /// Ensure that this struct can only be created via its private `[Library::new]` constructor. |
| /// Users of this crate can only access the struct via `[Config::probe]`. |
| _priv: (), |
| } |
| |
| /// Represents all reasons `pkg-config` might not succeed or be run at all. |
| pub enum Error { |
| /// Aborted because of `*_NO_PKG_CONFIG` environment variable. |
| /// |
| /// Contains the name of the responsible environment variable. |
| EnvNoPkgConfig(String), |
| |
| /// Detected cross compilation without a custom sysroot. |
| /// |
| /// Ignore the error with `PKG_CONFIG_ALLOW_CROSS=1`, |
| /// which may let `pkg-config` select libraries |
| /// for the host's architecture instead of the target's. |
| CrossCompilation, |
| |
| /// Failed to run `pkg-config`. |
| /// |
| /// Contains the command and the cause. |
| Command { command: String, cause: io::Error }, |
| |
| /// `pkg-config` did not exit successfully after probing a library. |
| /// |
| /// Contains the command and output. |
| Failure { command: String, output: Output }, |
| |
| /// `pkg-config` did not exit successfully on the first attempt to probe a library. |
| /// |
| /// Contains the command and output. |
| ProbeFailure { |
| name: String, |
| command: String, |
| output: Output, |
| }, |
| |
| #[doc(hidden)] |
| // please don't match on this, we're likely to add more variants over time |
| __Nonexhaustive, |
| } |
| |
| impl WrappedCommand { |
| fn new<S: AsRef<OsStr>>(program: S) -> Self { |
| Self { |
| inner: Command::new(program.as_ref()), |
| program: program.as_ref().to_os_string(), |
| env_vars: Vec::new(), |
| args: Vec::new(), |
| } |
| } |
| |
| fn args<I, S>(&mut self, args: I) -> &mut Self |
| where |
| I: IntoIterator<Item = S> + Clone, |
| S: AsRef<OsStr>, |
| { |
| self.inner.args(args.clone()); |
| self.args |
| .extend(args.into_iter().map(|arg| arg.as_ref().to_os_string())); |
| |
| self |
| } |
| |
| fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self { |
| self.inner.arg(arg.as_ref()); |
| self.args.push(arg.as_ref().to_os_string()); |
| |
| self |
| } |
| |
| fn env<K, V>(&mut self, key: K, value: V) -> &mut Self |
| where |
| K: AsRef<OsStr>, |
| V: AsRef<OsStr>, |
| { |
| self.inner.env(key.as_ref(), value.as_ref()); |
| self.env_vars |
| .push((key.as_ref().to_os_string(), value.as_ref().to_os_string())); |
| |
| self |
| } |
| |
| fn output(&mut self) -> io::Result<Output> { |
| self.inner.output() |
| } |
| } |
| |
| /// Output a command invocation that can be copy-pasted into the terminal. |
| /// `Command`'s existing debug implementation is not used for that reason, |
| /// as it can sometimes lead to output such as: |
| /// `PKG_CONFIG_ALLOW_SYSTEM_CFLAGS="1" PKG_CONFIG_ALLOW_SYSTEM_LIBS="1" "pkg-config" "--libs" "--cflags" "mylibrary"` |
| /// Which cannot be copy-pasted into terminals such as nushell, and is a bit noisy. |
| /// This will look something like: |
| /// `PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs --cflags mylibrary` |
| impl Display for WrappedCommand { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| // Format all explicitly defined environment variables |
| let envs = self |
| .env_vars |
| .iter() |
| .map(|(env, arg)| format!("{}={}", env.to_string_lossy(), arg.to_string_lossy())) |
| .collect::<Vec<String>>() |
| .join(" "); |
| |
| // Format all pkg-config arguments |
| let args = self |
| .args |
| .iter() |
| .map(|arg| arg.to_string_lossy().to_string()) |
| .collect::<Vec<String>>() |
| .join(" "); |
| |
| write!(f, "{} {} {}", envs, self.program.to_string_lossy(), args) |
| } |
| } |
| |
| impl error::Error for Error {} |
| |
| impl fmt::Debug for Error { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { |
| // Failed `unwrap()` prints Debug representation, but the default debug format lacks helpful instructions for the end users |
| <Error as fmt::Display>::fmt(self, f) |
| } |
| } |
| |
| impl fmt::Display for Error { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { |
| match *self { |
| Error::EnvNoPkgConfig(ref name) => write!(f, "Aborted because {} is set", name), |
| Error::CrossCompilation => f.write_str( |
| "pkg-config has not been configured to support cross-compilation.\n\ |
| \n\ |
| Install a sysroot for the target platform and configure it via\n\ |
| PKG_CONFIG_SYSROOT_DIR and PKG_CONFIG_PATH, or install a\n\ |
| cross-compiling wrapper for pkg-config and set it via\n\ |
| PKG_CONFIG environment variable.", |
| ), |
| Error::Command { |
| ref command, |
| ref cause, |
| } => { |
| match cause.kind() { |
| io::ErrorKind::NotFound => { |
| let crate_name = |
| std::env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "sys".to_owned()); |
| let instructions = if cfg!(target_os = "macos") || cfg!(target_os = "ios") { |
| "Try `brew install pkg-config` if you have Homebrew.\n" |
| } else if cfg!(unix) { |
| "Try `apt install pkg-config`, or `yum install pkg-config`,\n\ |
| or `pkg install pkg-config`, or `apk add pkgconfig` \ |
| depending on your distribution.\n" |
| } else { |
| "" // There's no easy fix for Windows users |
| }; |
| write!(f, "Could not run `{command}`\n\ |
| The pkg-config command could not be found.\n\ |
| \n\ |
| Most likely, you need to install a pkg-config package for your OS.\n\ |
| {instructions}\ |
| \n\ |
| If you've already installed it, ensure the pkg-config command is one of the\n\ |
| directories in the PATH environment variable.\n\ |
| \n\ |
| If you did not expect this build to link to a pre-installed system library,\n\ |
| then check documentation of the {crate_name} crate for an option to\n\ |
| build the library from source, or disable features or dependencies\n\ |
| that require pkg-config.", command = command, instructions = instructions, crate_name = crate_name) |
| } |
| _ => write!(f, "Failed to run command `{}`, because: {}", command, cause), |
| } |
| } |
| Error::ProbeFailure { |
| ref name, |
| ref command, |
| ref output, |
| } => { |
| let crate_name = |
| env::var("CARGO_PKG_NAME").unwrap_or(String::from("<NO CRATE NAME>")); |
| |
| writeln!(f, "")?; |
| |
| // Give a short explanation of what the error is |
| writeln!( |
| f, |
| "pkg-config {}", |
| match output.status.code() { |
| Some(code) => format!("exited with status code {}", code), |
| None => "was terminated by signal".to_string(), |
| } |
| )?; |
| |
| // Give the command run so users can reproduce the error |
| writeln!(f, "> {}\n", command)?; |
| |
| // Explain how it was caused |
| writeln!( |
| f, |
| "The system library `{}` required by crate `{}` was not found.", |
| name, crate_name |
| )?; |
| writeln!( |
| f, |
| "The file `{}.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory.", |
| name |
| )?; |
| |
| // There will be no status code if terminated by signal |
| if let Some(_code) = output.status.code() { |
| // Nix uses a wrapper script for pkg-config that sets the custom |
| // environment variable PKG_CONFIG_PATH_FOR_TARGET |
| let search_locations = ["PKG_CONFIG_PATH_FOR_TARGET", "PKG_CONFIG_PATH"]; |
| |
| // Find a search path to use |
| let mut search_data = None; |
| for location in search_locations.iter() { |
| if let Ok(search_path) = env::var(location) { |
| search_data = Some((location, search_path)); |
| break; |
| } |
| } |
| |
| // Guess the most reasonable course of action |
| let hint = if let Some((search_location, search_path)) = search_data { |
| writeln!( |
| f, |
| "{} contains the following:\n{}", |
| search_location, |
| search_path |
| .split(':') |
| .map(|path| format!(" - {}", path)) |
| .collect::<Vec<String>>() |
| .join("\n"), |
| )?; |
| |
| format!("you may need to install a package such as {name}, {name}-dev or {name}-devel.", name=name) |
| } else { |
| // Even on Nix, setting PKG_CONFIG_PATH seems to be a viable option |
| writeln!(f, "The PKG_CONFIG_PATH environment variable is not set.")?; |
| |
| format!( |
| "if you have installed the library, try setting PKG_CONFIG_PATH to the directory containing `{}.pc`.", |
| name |
| ) |
| }; |
| |
| // Try and nudge the user in the right direction so they don't get stuck |
| writeln!(f, "\nHINT: {}", hint)?; |
| } |
| |
| Ok(()) |
| } |
| Error::Failure { |
| ref command, |
| ref output, |
| } => { |
| write!( |
| f, |
| "`{}` did not exit successfully: {}", |
| command, output.status |
| )?; |
| format_output(output, f) |
| } |
| Error::__Nonexhaustive => panic!(), |
| } |
| } |
| } |
| |
| fn format_output(output: &Output, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| let stdout = String::from_utf8_lossy(&output.stdout); |
| if !stdout.is_empty() { |
| write!(f, "\n--- stdout\n{}", stdout)?; |
| } |
| let stderr = String::from_utf8_lossy(&output.stderr); |
| if !stderr.is_empty() { |
| write!(f, "\n--- stderr\n{}", stderr)?; |
| } |
| Ok(()) |
| } |
| |
| /// Deprecated in favor of the probe_library function |
| #[doc(hidden)] |
| pub fn find_library(name: &str) -> Result<Library, String> { |
| probe_library(name).map_err(|e| e.to_string()) |
| } |
| |
| /// Simple shortcut for using all default options for finding a library. |
| pub fn probe_library(name: &str) -> Result<Library, Error> { |
| Config::new().probe(name) |
| } |
| |
| #[doc(hidden)] |
| #[deprecated(note = "use config.target_supported() instance method instead")] |
| pub fn target_supported() -> bool { |
| Config::new().target_supported() |
| } |
| |
| /// Run `pkg-config` to get the value of a variable from a package using |
| /// `--variable`. |
| /// |
| /// The content of `PKG_CONFIG_SYSROOT_DIR` is not injected in paths that are |
| /// returned by `pkg-config --variable`, which makes them unsuitable to use |
| /// during cross-compilation unless specifically designed to be used |
| /// at that time. |
| pub fn get_variable(package: &str, variable: &str) -> Result<String, Error> { |
| let arg = format!("--variable={}", variable); |
| let cfg = Config::new(); |
| let out = cfg.run(package, &[&arg])?; |
| Ok(str::from_utf8(&out).unwrap().trim_end().to_owned()) |
| } |
| |
| impl Config { |
| /// Creates a new set of configuration options which are all initially set |
| /// to "blank". |
| pub fn new() -> Config { |
| Config { |
| statik: None, |
| min_version: Bound::Unbounded, |
| max_version: Bound::Unbounded, |
| extra_args: vec![], |
| print_system_cflags: true, |
| print_system_libs: true, |
| cargo_metadata: true, |
| env_metadata: true, |
| } |
| } |
| |
| /// Indicate whether the `--static` flag should be passed. |
| /// |
| /// This will override the inference from environment variables described in |
| /// the crate documentation. |
| pub fn statik(&mut self, statik: bool) -> &mut Config { |
| self.statik = Some(statik); |
| self |
| } |
| |
| /// Indicate that the library must be at least version `vers`. |
| pub fn atleast_version(&mut self, vers: &str) -> &mut Config { |
| self.min_version = Bound::Included(vers.to_string()); |
| self.max_version = Bound::Unbounded; |
| self |
| } |
| |
| /// Indicate that the library must be equal to version `vers`. |
| pub fn exactly_version(&mut self, vers: &str) -> &mut Config { |
| self.min_version = Bound::Included(vers.to_string()); |
| self.max_version = Bound::Included(vers.to_string()); |
| self |
| } |
| |
| /// Indicate that the library's version must be in `range`. |
| pub fn range_version<'a, R>(&mut self, range: R) -> &mut Config |
| where |
| R: RangeBounds<&'a str>, |
| { |
| self.min_version = match range.start_bound() { |
| Bound::Included(vers) => Bound::Included(vers.to_string()), |
| Bound::Excluded(vers) => Bound::Excluded(vers.to_string()), |
| Bound::Unbounded => Bound::Unbounded, |
| }; |
| self.max_version = match range.end_bound() { |
| Bound::Included(vers) => Bound::Included(vers.to_string()), |
| Bound::Excluded(vers) => Bound::Excluded(vers.to_string()), |
| Bound::Unbounded => Bound::Unbounded, |
| }; |
| self |
| } |
| |
| /// Add an argument to pass to pkg-config. |
| /// |
| /// It's placed after all of the arguments generated by this library. |
| pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Config { |
| self.extra_args.push(arg.as_ref().to_os_string()); |
| self |
| } |
| |
| /// Define whether metadata should be emitted for cargo allowing it to |
| /// automatically link the binary. Defaults to `true`. |
| pub fn cargo_metadata(&mut self, cargo_metadata: bool) -> &mut Config { |
| self.cargo_metadata = cargo_metadata; |
| self |
| } |
| |
| /// Define whether metadata should be emitted for cargo allowing to |
| /// automatically rebuild when environment variables change. Defaults to |
| /// `true`. |
| pub fn env_metadata(&mut self, env_metadata: bool) -> &mut Config { |
| self.env_metadata = env_metadata; |
| self |
| } |
| |
| /// Enable or disable the `PKG_CONFIG_ALLOW_SYSTEM_LIBS` environment |
| /// variable. |
| /// |
| /// This env var is enabled by default. |
| pub fn print_system_libs(&mut self, print: bool) -> &mut Config { |
| self.print_system_libs = print; |
| self |
| } |
| |
| /// Enable or disable the `PKG_CONFIG_ALLOW_SYSTEM_CFLAGS` environment |
| /// variable. |
| /// |
| /// This env var is enabled by default. |
| pub fn print_system_cflags(&mut self, print: bool) -> &mut Config { |
| self.print_system_cflags = print; |
| self |
| } |
| |
| /// Deprecated in favor fo the `probe` function |
| #[doc(hidden)] |
| pub fn find(&self, name: &str) -> Result<Library, String> { |
| self.probe(name).map_err(|e| e.to_string()) |
| } |
| |
| /// Run `pkg-config` to find the library `name`. |
| /// |
| /// This will use all configuration previously set to specify how |
| /// `pkg-config` is run. |
| pub fn probe(&self, name: &str) -> Result<Library, Error> { |
| let abort_var_name = format!("{}_NO_PKG_CONFIG", envify(name)); |
| if self.env_var_os(&abort_var_name).is_some() { |
| return Err(Error::EnvNoPkgConfig(abort_var_name)); |
| } else if !self.target_supported() { |
| return Err(Error::CrossCompilation); |
| } |
| |
| let mut library = Library::new(); |
| |
| let output = self |
| .run(name, &["--libs", "--cflags"]) |
| .map_err(|e| match e { |
| Error::Failure { command, output } => Error::ProbeFailure { |
| name: name.to_owned(), |
| command, |
| output, |
| }, |
| other => other, |
| })?; |
| library.parse_libs_cflags(name, &output, self); |
| |
| let output = self.run(name, &["--modversion"])?; |
| library.parse_modversion(str::from_utf8(&output).unwrap()); |
| |
| Ok(library) |
| } |
| |
| /// True if pkg-config is used for the host system, or configured for cross-compilation |
| pub fn target_supported(&self) -> bool { |
| let target = env::var_os("TARGET").unwrap_or_default(); |
| let host = env::var_os("HOST").unwrap_or_default(); |
| |
| // Only use pkg-config in host == target situations by default (allowing an |
| // override). |
| if host == target { |
| return true; |
| } |
| |
| // pkg-config may not be aware of cross-compilation, and require |
| // a wrapper script that sets up platform-specific prefixes. |
| match self.targeted_env_var("PKG_CONFIG_ALLOW_CROSS") { |
| // don't use pkg-config if explicitly disabled |
| Some(ref val) if val == "0" => false, |
| Some(_) => true, |
| None => { |
| // if not disabled, and pkg-config is customized, |
| // then assume it's prepared for cross-compilation |
| self.targeted_env_var("PKG_CONFIG").is_some() |
| || self.targeted_env_var("PKG_CONFIG_SYSROOT_DIR").is_some() |
| } |
| } |
| } |
| |
| /// Deprecated in favor of the top level `get_variable` function |
| #[doc(hidden)] |
| pub fn get_variable(package: &str, variable: &str) -> Result<String, String> { |
| get_variable(package, variable).map_err(|e| e.to_string()) |
| } |
| |
| fn targeted_env_var(&self, var_base: &str) -> Option<OsString> { |
| match (env::var("TARGET"), env::var("HOST")) { |
| (Ok(target), Ok(host)) => { |
| let kind = if host == target { "HOST" } else { "TARGET" }; |
| let target_u = target.replace('-', "_"); |
| |
| self.env_var_os(&format!("{}_{}", var_base, target)) |
| .or_else(|| self.env_var_os(&format!("{}_{}", var_base, target_u))) |
| .or_else(|| self.env_var_os(&format!("{}_{}", kind, var_base))) |
| .or_else(|| self.env_var_os(var_base)) |
| } |
| (Err(env::VarError::NotPresent), _) | (_, Err(env::VarError::NotPresent)) => { |
| self.env_var_os(var_base) |
| } |
| (Err(env::VarError::NotUnicode(s)), _) | (_, Err(env::VarError::NotUnicode(s))) => { |
| panic!( |
| "HOST or TARGET environment variable is not valid unicode: {:?}", |
| s |
| ) |
| } |
| } |
| } |
| |
| fn env_var_os(&self, name: &str) -> Option<OsString> { |
| if self.env_metadata { |
| println!("cargo:rerun-if-env-changed={}", name); |
| } |
| env::var_os(name) |
| } |
| |
| fn is_static(&self, name: &str) -> bool { |
| self.statik.unwrap_or_else(|| self.infer_static(name)) |
| } |
| |
| fn run(&self, name: &str, args: &[&str]) -> Result<Vec<u8>, Error> { |
| let pkg_config_exe = self.targeted_env_var("PKG_CONFIG"); |
| let fallback_exe = if pkg_config_exe.is_none() { |
| Some(OsString::from("pkgconf")) |
| } else { |
| None |
| }; |
| let exe = pkg_config_exe.unwrap_or_else(|| OsString::from("pkg-config")); |
| |
| let mut cmd = self.command(exe, name, args); |
| |
| match cmd.output().or_else(|e| { |
| if let Some(exe) = fallback_exe { |
| self.command(exe, name, args).output() |
| } else { |
| Err(e) |
| } |
| }) { |
| Ok(output) => { |
| if output.status.success() { |
| Ok(output.stdout) |
| } else { |
| Err(Error::Failure { |
| command: format!("{}", cmd), |
| output, |
| }) |
| } |
| } |
| Err(cause) => Err(Error::Command { |
| command: format!("{}", cmd), |
| cause, |
| }), |
| } |
| } |
| |
| fn command(&self, exe: OsString, name: &str, args: &[&str]) -> WrappedCommand { |
| let mut cmd = WrappedCommand::new(exe); |
| if self.is_static(name) { |
| cmd.arg("--static"); |
| } |
| cmd.args(args).args(&self.extra_args); |
| |
| if let Some(value) = self.targeted_env_var("PKG_CONFIG_PATH") { |
| cmd.env("PKG_CONFIG_PATH", value); |
| } |
| if let Some(value) = self.targeted_env_var("PKG_CONFIG_LIBDIR") { |
| cmd.env("PKG_CONFIG_LIBDIR", value); |
| } |
| if let Some(value) = self.targeted_env_var("PKG_CONFIG_SYSROOT_DIR") { |
| cmd.env("PKG_CONFIG_SYSROOT_DIR", value); |
| } |
| if self.print_system_libs { |
| cmd.env("PKG_CONFIG_ALLOW_SYSTEM_LIBS", "1"); |
| } |
| if self.print_system_cflags { |
| cmd.env("PKG_CONFIG_ALLOW_SYSTEM_CFLAGS", "1"); |
| } |
| cmd.arg(name); |
| match self.min_version { |
| Bound::Included(ref version) => { |
| cmd.arg(&format!("{} >= {}", name, version)); |
| } |
| Bound::Excluded(ref version) => { |
| cmd.arg(&format!("{} > {}", name, version)); |
| } |
| _ => (), |
| } |
| match self.max_version { |
| Bound::Included(ref version) => { |
| cmd.arg(&format!("{} <= {}", name, version)); |
| } |
| Bound::Excluded(ref version) => { |
| cmd.arg(&format!("{} < {}", name, version)); |
| } |
| _ => (), |
| } |
| cmd |
| } |
| |
| fn print_metadata(&self, s: &str) { |
| if self.cargo_metadata { |
| println!("cargo:{}", s); |
| } |
| } |
| |
| fn infer_static(&self, name: &str) -> bool { |
| let name = envify(name); |
| if self.env_var_os(&format!("{}_STATIC", name)).is_some() { |
| true |
| } else if self.env_var_os(&format!("{}_DYNAMIC", name)).is_some() { |
| false |
| } else if self.env_var_os("PKG_CONFIG_ALL_STATIC").is_some() { |
| true |
| } else if self.env_var_os("PKG_CONFIG_ALL_DYNAMIC").is_some() { |
| false |
| } else { |
| false |
| } |
| } |
| } |
| |
| // Implement Default manually since Bound does not implement Default. |
| impl Default for Config { |
| fn default() -> Config { |
| Config { |
| statik: None, |
| min_version: Bound::Unbounded, |
| max_version: Bound::Unbounded, |
| extra_args: vec![], |
| print_system_cflags: false, |
| print_system_libs: false, |
| cargo_metadata: false, |
| env_metadata: false, |
| } |
| } |
| } |
| |
| impl Library { |
| fn new() -> Library { |
| Library { |
| libs: Vec::new(), |
| link_paths: Vec::new(), |
| link_files: Vec::new(), |
| include_paths: Vec::new(), |
| ld_args: Vec::new(), |
| frameworks: Vec::new(), |
| framework_paths: Vec::new(), |
| defines: HashMap::new(), |
| version: String::new(), |
| _priv: (), |
| } |
| } |
| |
| /// Extract the &str to pass to cargo:rustc-link-lib from a filename (just the file name, not including directories) |
| /// using target-specific logic. |
| fn extract_lib_from_filename<'a>(target: &str, filename: &'a str) -> Option<&'a str> { |
| fn test_suffixes<'b>(filename: &'b str, suffixes: &[&str]) -> Option<&'b str> { |
| for suffix in suffixes { |
| if filename.ends_with(suffix) { |
| return Some(&filename[..filename.len() - suffix.len()]); |
| } |
| } |
| None |
| } |
| |
| let prefix = "lib"; |
| if target.contains("windows") { |
| if target.contains("gnu") && filename.starts_with(prefix) { |
| // GNU targets for Windows, including gnullvm, use `LinkerFlavor::Gcc` internally in rustc, |
| // which tells rustc to use the GNU linker. rustc does not prepend/append to the string it |
| // receives via the -l command line argument before passing it to the linker: |
| // https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L446 |
| // https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L457 |
| // GNU ld can work with more types of files than just the .lib files that MSVC's link.exe needs. |
| // GNU ld will prepend the `lib` prefix to the filename if necessary, so it is okay to remove |
| // the `lib` prefix from the filename. The `.a` suffix *requires* the `lib` prefix. |
| // https://sourceware.org/binutils/docs-2.39/ld.html#index-direct-linking-to-a-dll |
| let filename = &filename[prefix.len()..]; |
| return test_suffixes(filename, &[".dll.a", ".dll", ".lib", ".a"]); |
| } else { |
| // According to link.exe documentation: |
| // https://learn.microsoft.com/en-us/cpp/build/reference/link-input-files?view=msvc-170 |
| // |
| // LINK doesn't use file extensions to make assumptions about the contents of a file. |
| // Instead, LINK examines each input file to determine what kind of file it is. |
| // |
| // However, rustc appends `.lib` to the string it receives from the -l command line argument, |
| // which it receives from Cargo via cargo:rustc-link-lib: |
| // https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L828 |
| // https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L843 |
| // So the only file extension that works for MSVC targets is `.lib` |
| // However, for externally created libraries, there's no |
| // guarantee that the extension is ".lib" so we need to |
| // consider all options. |
| // See: |
| // https://github.com/mesonbuild/meson/issues/8153 |
| // https://github.com/rust-lang/rust/issues/114013 |
| return test_suffixes(filename, &[".dll.a", ".dll", ".lib", ".a"]); |
| } |
| } else if target.contains("apple") { |
| if filename.starts_with(prefix) { |
| let filename = &filename[prefix.len()..]; |
| return test_suffixes(filename, &[".a", ".so", ".dylib"]); |
| } |
| return None; |
| } else { |
| if filename.starts_with(prefix) { |
| let filename = &filename[prefix.len()..]; |
| return test_suffixes(filename, &[".a", ".so"]); |
| } |
| return None; |
| } |
| } |
| |
| fn parse_libs_cflags(&mut self, name: &str, output: &[u8], config: &Config) { |
| let target = env::var("TARGET"); |
| let is_msvc = target |
| .as_ref() |
| .map(|target| target.contains("msvc")) |
| .unwrap_or(false); |
| |
| let system_roots = if cfg!(target_os = "macos") { |
| vec![PathBuf::from("/Library"), PathBuf::from("/System")] |
| } else { |
| let sysroot = config |
| .env_var_os("PKG_CONFIG_SYSROOT_DIR") |
| .or_else(|| config.env_var_os("SYSROOT")) |
| .map(PathBuf::from); |
| |
| if cfg!(target_os = "windows") { |
| if let Some(sysroot) = sysroot { |
| vec![sysroot] |
| } else { |
| vec![] |
| } |
| } else { |
| vec![sysroot.unwrap_or_else(|| PathBuf::from("/usr"))] |
| } |
| }; |
| |
| let mut dirs = Vec::new(); |
| let statik = config.is_static(name); |
| |
| let words = split_flags(output); |
| |
| // Handle single-character arguments like `-I/usr/include` |
| let parts = words |
| .iter() |
| .filter(|l| l.len() > 2) |
| .map(|arg| (&arg[0..2], &arg[2..])); |
| for (flag, val) in parts { |
| match flag { |
| "-L" => { |
| let meta = format!("rustc-link-search=native={}", val); |
| config.print_metadata(&meta); |
| dirs.push(PathBuf::from(val)); |
| self.link_paths.push(PathBuf::from(val)); |
| } |
| "-F" => { |
| let meta = format!("rustc-link-search=framework={}", val); |
| config.print_metadata(&meta); |
| self.framework_paths.push(PathBuf::from(val)); |
| } |
| "-I" => { |
| self.include_paths.push(PathBuf::from(val)); |
| } |
| "-l" => { |
| // These are provided by the CRT with MSVC |
| if is_msvc && ["m", "c", "pthread"].contains(&val) { |
| continue; |
| } |
| |
| if val.starts_with(':') { |
| // Pass this flag to linker directly. |
| let meta = format!("rustc-link-arg={}{}", flag, val); |
| config.print_metadata(&meta); |
| } else if statik && is_static_available(val, &system_roots, &dirs) { |
| let meta = format!("rustc-link-lib=static={}", val); |
| config.print_metadata(&meta); |
| } else { |
| let meta = format!("rustc-link-lib={}", val); |
| config.print_metadata(&meta); |
| } |
| |
| self.libs.push(val.to_string()); |
| } |
| "-D" => { |
| let mut iter = val.split('='); |
| self.defines.insert( |
| iter.next().unwrap().to_owned(), |
| iter.next().map(|s| s.to_owned()), |
| ); |
| } |
| "-u" => { |
| let meta = format!("rustc-link-arg=-Wl,-u,{}", val); |
| config.print_metadata(&meta); |
| } |
| _ => {} |
| } |
| } |
| |
| // Handle multi-character arguments with space-separated value like `-framework foo` |
| let mut iter = words.iter().flat_map(|arg| { |
| if arg.starts_with("-Wl,") { |
| arg[4..].split(',').collect() |
| } else { |
| vec![arg.as_ref()] |
| } |
| }); |
| while let Some(part) = iter.next() { |
| match part { |
| "-framework" => { |
| if let Some(lib) = iter.next() { |
| let meta = format!("rustc-link-lib=framework={}", lib); |
| config.print_metadata(&meta); |
| self.frameworks.push(lib.to_string()); |
| } |
| } |
| "-isystem" | "-iquote" | "-idirafter" => { |
| if let Some(inc) = iter.next() { |
| self.include_paths.push(PathBuf::from(inc)); |
| } |
| } |
| "-undefined" | "--undefined" => { |
| if let Some(symbol) = iter.next() { |
| let meta = format!("rustc-link-arg=-Wl,{},{}", part, symbol); |
| config.print_metadata(&meta); |
| } |
| } |
| _ => { |
| let path = std::path::Path::new(part); |
| if path.is_file() { |
| // Cargo doesn't have a means to directly specify a file path to link, |
| // so split up the path into the parent directory and library name. |
| // TODO: pass file path directly when link-arg library type is stabilized |
| // https://github.com/rust-lang/rust/issues/99427 |
| if let (Some(dir), Some(file_name), Ok(target)) = |
| (path.parent(), path.file_name(), &target) |
| { |
| match Self::extract_lib_from_filename( |
| target, |
| &file_name.to_string_lossy(), |
| ) { |
| Some(lib_basename) => { |
| let link_search = |
| format!("rustc-link-search={}", dir.display()); |
| config.print_metadata(&link_search); |
| |
| let link_lib = format!("rustc-link-lib={}", lib_basename); |
| config.print_metadata(&link_lib); |
| self.link_files.push(PathBuf::from(path)); |
| } |
| None => { |
| println!("cargo:warning=File path {} found in pkg-config file for {}, but could not extract library base name to pass to linker command line", path.display(), name); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| let linker_options = words.iter().filter(|arg| arg.starts_with("-Wl,")); |
| for option in linker_options { |
| let mut pop = false; |
| let mut ld_option = vec![]; |
| for subopt in option[4..].split(',') { |
| if pop { |
| pop = false; |
| continue; |
| } |
| |
| if subopt == "-framework" { |
| pop = true; |
| continue; |
| } |
| |
| ld_option.push(subopt); |
| } |
| |
| let meta = format!("rustc-link-arg=-Wl,{}", ld_option.join(",")); |
| config.print_metadata(&meta); |
| |
| self.ld_args |
| .push(ld_option.into_iter().map(String::from).collect()); |
| } |
| } |
| |
| fn parse_modversion(&mut self, output: &str) { |
| self.version.push_str(output.lines().next().unwrap().trim()); |
| } |
| } |
| |
| fn envify(name: &str) -> String { |
| name.chars() |
| .map(|c| c.to_ascii_uppercase()) |
| .map(|c| if c == '-' { '_' } else { c }) |
| .collect() |
| } |
| |
| /// System libraries should only be linked dynamically |
| fn is_static_available(name: &str, system_roots: &[PathBuf], dirs: &[PathBuf]) -> bool { |
| let libnames = { |
| let mut names = vec![format!("lib{}.a", name)]; |
| |
| if cfg!(target_os = "windows") { |
| names.push(format!("{}.lib", name)); |
| } |
| |
| names |
| }; |
| |
| dirs.iter().any(|dir| { |
| let library_exists = libnames.iter().any(|libname| dir.join(&libname).exists()); |
| library_exists && !system_roots.iter().any(|sys| dir.starts_with(sys)) |
| }) |
| } |
| |
| /// Split output produced by pkg-config --cflags and / or --libs into separate flags. |
| /// |
| /// Backslash in output is used to preserve literal meaning of following byte. Different words are |
| /// separated by unescaped space. Other whitespace characters generally should not occur unescaped |
| /// at all, apart from the newline at the end of output. For compatibility with what others |
| /// consumers of pkg-config output would do in this scenario, they are used here for splitting as |
| /// well. |
| fn split_flags(output: &[u8]) -> Vec<String> { |
| let mut word = Vec::new(); |
| let mut words = Vec::new(); |
| let mut escaped = false; |
| |
| for &b in output { |
| match b { |
| _ if escaped => { |
| escaped = false; |
| word.push(b); |
| } |
| b'\\' => escaped = true, |
| b'\t' | b'\n' | b'\r' | b' ' => { |
| if !word.is_empty() { |
| words.push(String::from_utf8(word).unwrap()); |
| word = Vec::new(); |
| } |
| } |
| _ => word.push(b), |
| } |
| } |
| |
| if !word.is_empty() { |
| words.push(String::from_utf8(word).unwrap()); |
| } |
| |
| words |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| #[cfg(target_os = "macos")] |
| fn system_library_mac_test() { |
| use std::path::Path; |
| |
| let system_roots = vec![PathBuf::from("/Library"), PathBuf::from("/System")]; |
| |
| assert!(!is_static_available( |
| "PluginManager", |
| &system_roots, |
| &[PathBuf::from("/Library/Frameworks")] |
| )); |
| assert!(!is_static_available( |
| "python2.7", |
| &system_roots, |
| &[PathBuf::from( |
| "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config" |
| )] |
| )); |
| assert!(!is_static_available( |
| "ffi_convenience", |
| &system_roots, |
| &[PathBuf::from( |
| "/Library/Ruby/Gems/2.0.0/gems/ffi-1.9.10/ext/ffi_c/libffi-x86_64/.libs" |
| )] |
| )); |
| |
| // Homebrew is in /usr/local, and it's not a part of the OS |
| if Path::new("/usr/local/lib/libpng16.a").exists() { |
| assert!(is_static_available( |
| "png16", |
| &system_roots, |
| &[PathBuf::from("/usr/local/lib")] |
| )); |
| |
| let libpng = Config::new() |
| .range_version("1".."99") |
| .probe("libpng16") |
| .unwrap(); |
| assert!(libpng.version.find('\n').is_none()); |
| } |
| } |
| |
| #[test] |
| #[cfg(target_os = "linux")] |
| fn system_library_linux_test() { |
| assert!(!is_static_available( |
| "util", |
| &[PathBuf::from("/usr")], |
| &[PathBuf::from("/usr/lib/x86_64-linux-gnu")] |
| )); |
| assert!(!is_static_available( |
| "dialog", |
| &[PathBuf::from("/usr")], |
| &[PathBuf::from("/usr/lib")] |
| )); |
| } |
| |
| fn test_library_filename(target: &str, filename: &str) { |
| assert_eq!( |
| Library::extract_lib_from_filename(target, filename), |
| Some("foo") |
| ); |
| } |
| |
| #[test] |
| fn link_filename_linux() { |
| let target = "x86_64-unknown-linux-gnu"; |
| test_library_filename(target, "libfoo.a"); |
| test_library_filename(target, "libfoo.so"); |
| } |
| |
| #[test] |
| fn link_filename_apple() { |
| let target = "x86_64-apple-darwin"; |
| test_library_filename(target, "libfoo.a"); |
| test_library_filename(target, "libfoo.so"); |
| test_library_filename(target, "libfoo.dylib"); |
| } |
| |
| #[test] |
| fn link_filename_msvc() { |
| let target = "x86_64-pc-windows-msvc"; |
| // static and dynamic libraries have the same .lib suffix |
| test_library_filename(target, "foo.lib"); |
| } |
| |
| #[test] |
| fn link_filename_mingw() { |
| let target = "x86_64-pc-windows-gnu"; |
| test_library_filename(target, "foo.lib"); |
| test_library_filename(target, "libfoo.a"); |
| test_library_filename(target, "foo.dll"); |
| test_library_filename(target, "foo.dll.a"); |
| } |
| } |