1 | //! A build dependency for Cargo libraries to find system artifacts through the |
2 | //! `pkg-config` utility. |
3 | //! |
4 | //! This library will shell out to `pkg-config` as part of build scripts and |
5 | //! probe the system to determine how to link to a specified library. The |
6 | //! `Config` structure serves as a method of configuring how `pkg-config` is |
7 | //! invoked in a builder style. |
8 | //! |
9 | //! A number of environment variables are available to globally configure how |
10 | //! this crate will invoke `pkg-config`: |
11 | //! |
12 | //! * `FOO_NO_PKG_CONFIG` - if set, this will disable running `pkg-config` when |
13 | //! probing for the library named `foo`. |
14 | //! |
15 | //! * `PKG_CONFIG_ALLOW_CROSS` - The `pkg-config` command usually doesn't |
16 | //! support cross-compilation, and this crate prevents it from selecting |
17 | //! incompatible versions of libraries. |
18 | //! Setting `PKG_CONFIG_ALLOW_CROSS=1` disables this protection, which is |
19 | //! likely to cause linking errors, unless `pkg-config` has been configured |
20 | //! to use appropriate sysroot and search paths for the target platform. |
21 | //! |
22 | //! There are also a number of environment variables which can configure how a |
23 | //! library is linked to (dynamically vs statically). These variables control |
24 | //! whether the `--static` flag is passed. Note that this behavior can be |
25 | //! overridden by configuring explicitly on `Config`. The variables are checked |
26 | //! in the following order: |
27 | //! |
28 | //! * `FOO_STATIC` - pass `--static` for the library `foo` |
29 | //! * `FOO_DYNAMIC` - do not pass `--static` for the library `foo` |
30 | //! * `PKG_CONFIG_ALL_STATIC` - pass `--static` for all libraries |
31 | //! * `PKG_CONFIG_ALL_DYNAMIC` - do not pass `--static` for all libraries |
32 | //! |
33 | //! After running `pkg-config` all appropriate Cargo metadata will be printed on |
34 | //! stdout if the search was successful. |
35 | //! |
36 | //! # Example |
37 | //! |
38 | //! Find the system library named `foo`, with minimum version 1.2.3: |
39 | //! |
40 | //! ```no_run |
41 | //! fn main() { |
42 | //! pkg_config::Config::new().atleast_version("1.2.3" ).probe("foo" ).unwrap(); |
43 | //! } |
44 | //! ``` |
45 | //! |
46 | //! Find the system library named `foo`, with no version requirement (not |
47 | //! recommended): |
48 | //! |
49 | //! ```no_run |
50 | //! fn main() { |
51 | //! pkg_config::probe_library("foo" ).unwrap(); |
52 | //! } |
53 | //! ``` |
54 | //! |
55 | //! Configure how library `foo` is linked to. |
56 | //! |
57 | //! ```no_run |
58 | //! fn main() { |
59 | //! pkg_config::Config::new().atleast_version("1.2.3" ).statik(true).probe("foo" ).unwrap(); |
60 | //! } |
61 | //! ``` |
62 | |
63 | #![doc (html_root_url = "https://docs.rs/pkg-config/0.3" )] |
64 | |
65 | use std::collections::HashMap; |
66 | use std::env; |
67 | use std::error; |
68 | use std::ffi::{OsStr, OsString}; |
69 | use std::fmt; |
70 | use std::io; |
71 | use std::ops::{Bound, RangeBounds}; |
72 | use std::path::PathBuf; |
73 | use std::process::{Command, Output}; |
74 | use std::str; |
75 | |
76 | #[derive (Clone, Debug)] |
77 | pub struct Config { |
78 | statik: Option<bool>, |
79 | min_version: Bound<String>, |
80 | max_version: Bound<String>, |
81 | extra_args: Vec<OsString>, |
82 | cargo_metadata: bool, |
83 | env_metadata: bool, |
84 | print_system_libs: bool, |
85 | print_system_cflags: bool, |
86 | } |
87 | |
88 | #[derive (Clone, Debug)] |
89 | pub struct Library { |
90 | /// Libraries specified by -l |
91 | pub libs: Vec<String>, |
92 | /// Library search paths specified by -L |
93 | pub link_paths: Vec<PathBuf>, |
94 | /// Library file paths specified without -l |
95 | pub link_files: Vec<PathBuf>, |
96 | /// Darwin frameworks specified by -framework |
97 | pub frameworks: Vec<String>, |
98 | /// Darwin framework search paths specified by -F |
99 | pub framework_paths: Vec<PathBuf>, |
100 | /// C/C++ header include paths specified by -I |
101 | pub include_paths: Vec<PathBuf>, |
102 | /// Linker options specified by -Wl |
103 | pub ld_args: Vec<Vec<String>>, |
104 | /// C/C++ definitions specified by -D |
105 | pub defines: HashMap<String, Option<String>>, |
106 | /// Version specified by .pc file's Version field |
107 | pub version: String, |
108 | /// Ensure that this struct can only be created via its private `[Library::new]` constructor. |
109 | /// Users of this crate can only access the struct via `[Config::probe]`. |
110 | _priv: (), |
111 | } |
112 | |
113 | /// Represents all reasons `pkg-config` might not succeed or be run at all. |
114 | pub enum Error { |
115 | /// Aborted because of `*_NO_PKG_CONFIG` environment variable. |
116 | /// |
117 | /// Contains the name of the responsible environment variable. |
118 | EnvNoPkgConfig(String), |
119 | |
120 | /// Detected cross compilation without a custom sysroot. |
121 | /// |
122 | /// Ignore the error with `PKG_CONFIG_ALLOW_CROSS=1`, |
123 | /// which may let `pkg-config` select libraries |
124 | /// for the host's architecture instead of the target's. |
125 | CrossCompilation, |
126 | |
127 | /// Failed to run `pkg-config`. |
128 | /// |
129 | /// Contains the command and the cause. |
130 | Command { command: String, cause: io::Error }, |
131 | |
132 | /// `pkg-config` did not exit sucessfully after probing a library. |
133 | /// |
134 | /// Contains the command and output. |
135 | Failure { command: String, output: Output }, |
136 | |
137 | /// `pkg-config` did not exit sucessfully on the first attempt to probe a library. |
138 | /// |
139 | /// Contains the command and output. |
140 | ProbeFailure { |
141 | name: String, |
142 | command: String, |
143 | output: Output, |
144 | }, |
145 | |
146 | #[doc (hidden)] |
147 | // please don't match on this, we're likely to add more variants over time |
148 | __Nonexhaustive, |
149 | } |
150 | |
151 | impl error::Error for Error {} |
152 | |
153 | impl fmt::Debug for Error { |
154 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { |
155 | // Failed `unwrap()` prints Debug representation, but the default debug format lacks helpful instructions for the end users |
156 | <Error as fmt::Display>::fmt(self, f) |
157 | } |
158 | } |
159 | |
160 | impl fmt::Display for Error { |
161 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { |
162 | match *self { |
163 | Error::EnvNoPkgConfig(ref name) => write!(f, "Aborted because {} is set" , name), |
164 | Error::CrossCompilation => f.write_str( |
165 | "pkg-config has not been configured to support cross-compilation. \n\ |
166 | \n\ |
167 | Install a sysroot for the target platform and configure it via \n\ |
168 | PKG_CONFIG_SYSROOT_DIR and PKG_CONFIG_PATH, or install a \n\ |
169 | cross-compiling wrapper for pkg-config and set it via \n\ |
170 | PKG_CONFIG environment variable." , |
171 | ), |
172 | Error::Command { |
173 | ref command, |
174 | ref cause, |
175 | } => { |
176 | match cause.kind() { |
177 | io::ErrorKind::NotFound => { |
178 | let crate_name = |
179 | std::env::var("CARGO_PKG_NAME" ).unwrap_or_else(|_| "sys" .to_owned()); |
180 | let instructions = if cfg!(target_os = "macos" ) || cfg!(target_os = "ios" ) { |
181 | "Try `brew install pkg-config` if you have Homebrew. \n" |
182 | } else if cfg!(unix) { |
183 | "Try `apt install pkg-config`, or `yum install pkg-config`, \n\ |
184 | or `pkg install pkg-config`, or `apk add pkgconfig` \ |
185 | depending on your distribution. \n" |
186 | } else { |
187 | "" // There's no easy fix for Windows users |
188 | }; |
189 | write!(f, "Could not run ` {command}` \n\ |
190 | The pkg-config command could not be found. \n\ |
191 | \n\ |
192 | Most likely, you need to install a pkg-config package for your OS. \n\ |
193 | {instructions}\ |
194 | \n\ |
195 | If you've already installed it, ensure the pkg-config command is one of the \n\ |
196 | directories in the PATH environment variable. \n\ |
197 | \n\ |
198 | If you did not expect this build to link to a pre-installed system library, \n\ |
199 | then check documentation of the {crate_name} crate for an option to \n\ |
200 | build the library from source, or disable features or dependencies \n\ |
201 | that require pkg-config." , command = command, instructions = instructions, crate_name = crate_name) |
202 | } |
203 | _ => write!(f, "Failed to run command ` {}`, because: {}" , command, cause), |
204 | } |
205 | } |
206 | Error::ProbeFailure { |
207 | ref name, |
208 | ref command, |
209 | ref output, |
210 | } => { |
211 | write!( |
212 | f, |
213 | "` {}` did not exit successfully: {}\nerror: could not find system library ' {}' required by the ' {}' crate \n" , |
214 | command, output.status, name, env::var("CARGO_PKG_NAME" ).unwrap_or_default(), |
215 | )?; |
216 | format_output(output, f) |
217 | } |
218 | Error::Failure { |
219 | ref command, |
220 | ref output, |
221 | } => { |
222 | write!( |
223 | f, |
224 | "` {}` did not exit successfully: {}" , |
225 | command, output.status |
226 | )?; |
227 | format_output(output, f) |
228 | } |
229 | Error::__Nonexhaustive => panic!(), |
230 | } |
231 | } |
232 | } |
233 | |
234 | fn format_output(output: &Output, f: &mut fmt::Formatter) -> fmt::Result { |
235 | let stdout: Cow<'_, str> = String::from_utf8_lossy(&output.stdout); |
236 | if !stdout.is_empty() { |
237 | write!(f, " \n--- stdout \n{}" , stdout)?; |
238 | } |
239 | let stderr: Cow<'_, str> = String::from_utf8_lossy(&output.stderr); |
240 | if !stderr.is_empty() { |
241 | write!(f, " \n--- stderr \n{}" , stderr)?; |
242 | } |
243 | Ok(()) |
244 | } |
245 | |
246 | /// Deprecated in favor of the probe_library function |
247 | #[doc (hidden)] |
248 | pub fn find_library(name: &str) -> Result<Library, String> { |
249 | probe_library(name).map_err(|e: Error| e.to_string()) |
250 | } |
251 | |
252 | /// Simple shortcut for using all default options for finding a library. |
253 | pub fn probe_library(name: &str) -> Result<Library, Error> { |
254 | Config::new().probe(name) |
255 | } |
256 | |
257 | #[doc (hidden)] |
258 | #[deprecated (note = "use config.target_supported() instance method instead" )] |
259 | pub fn target_supported() -> bool { |
260 | Config::new().target_supported() |
261 | } |
262 | |
263 | /// Run `pkg-config` to get the value of a variable from a package using |
264 | /// `--variable`. |
265 | /// |
266 | /// The content of `PKG_CONFIG_SYSROOT_DIR` is not injected in paths that are |
267 | /// returned by `pkg-config --variable`, which makes them unsuitable to use |
268 | /// during cross-compilation unless specifically designed to be used |
269 | /// at that time. |
270 | pub fn get_variable(package: &str, variable: &str) -> Result<String, Error> { |
271 | let arg: String = format!("--variable= {}" , variable); |
272 | let cfg: Config = Config::new(); |
273 | let out: Vec = cfg.run(name:package, &[&arg])?; |
274 | Ok(str::from_utf8(&out).unwrap().trim_end().to_owned()) |
275 | } |
276 | |
277 | impl Config { |
278 | /// Creates a new set of configuration options which are all initially set |
279 | /// to "blank". |
280 | pub fn new() -> Config { |
281 | Config { |
282 | statik: None, |
283 | min_version: Bound::Unbounded, |
284 | max_version: Bound::Unbounded, |
285 | extra_args: vec![], |
286 | print_system_cflags: true, |
287 | print_system_libs: true, |
288 | cargo_metadata: true, |
289 | env_metadata: true, |
290 | } |
291 | } |
292 | |
293 | /// Indicate whether the `--static` flag should be passed. |
294 | /// |
295 | /// This will override the inference from environment variables described in |
296 | /// the crate documentation. |
297 | pub fn statik(&mut self, statik: bool) -> &mut Config { |
298 | self.statik = Some(statik); |
299 | self |
300 | } |
301 | |
302 | /// Indicate that the library must be at least version `vers`. |
303 | pub fn atleast_version(&mut self, vers: &str) -> &mut Config { |
304 | self.min_version = Bound::Included(vers.to_string()); |
305 | self.max_version = Bound::Unbounded; |
306 | self |
307 | } |
308 | |
309 | /// Indicate that the library must be equal to version `vers`. |
310 | pub fn exactly_version(&mut self, vers: &str) -> &mut Config { |
311 | self.min_version = Bound::Included(vers.to_string()); |
312 | self.max_version = Bound::Included(vers.to_string()); |
313 | self |
314 | } |
315 | |
316 | /// Indicate that the library's version must be in `range`. |
317 | pub fn range_version<'a, R>(&mut self, range: R) -> &mut Config |
318 | where |
319 | R: RangeBounds<&'a str>, |
320 | { |
321 | self.min_version = match range.start_bound() { |
322 | Bound::Included(vers) => Bound::Included(vers.to_string()), |
323 | Bound::Excluded(vers) => Bound::Excluded(vers.to_string()), |
324 | Bound::Unbounded => Bound::Unbounded, |
325 | }; |
326 | self.max_version = match range.end_bound() { |
327 | Bound::Included(vers) => Bound::Included(vers.to_string()), |
328 | Bound::Excluded(vers) => Bound::Excluded(vers.to_string()), |
329 | Bound::Unbounded => Bound::Unbounded, |
330 | }; |
331 | self |
332 | } |
333 | |
334 | /// Add an argument to pass to pkg-config. |
335 | /// |
336 | /// It's placed after all of the arguments generated by this library. |
337 | pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Config { |
338 | self.extra_args.push(arg.as_ref().to_os_string()); |
339 | self |
340 | } |
341 | |
342 | /// Define whether metadata should be emitted for cargo allowing it to |
343 | /// automatically link the binary. Defaults to `true`. |
344 | pub fn cargo_metadata(&mut self, cargo_metadata: bool) -> &mut Config { |
345 | self.cargo_metadata = cargo_metadata; |
346 | self |
347 | } |
348 | |
349 | /// Define whether metadata should be emitted for cargo allowing to |
350 | /// automatically rebuild when environment variables change. Defaults to |
351 | /// `true`. |
352 | pub fn env_metadata(&mut self, env_metadata: bool) -> &mut Config { |
353 | self.env_metadata = env_metadata; |
354 | self |
355 | } |
356 | |
357 | /// Enable or disable the `PKG_CONFIG_ALLOW_SYSTEM_LIBS` environment |
358 | /// variable. |
359 | /// |
360 | /// This env var is enabled by default. |
361 | pub fn print_system_libs(&mut self, print: bool) -> &mut Config { |
362 | self.print_system_libs = print; |
363 | self |
364 | } |
365 | |
366 | /// Enable or disable the `PKG_CONFIG_ALLOW_SYSTEM_CFLAGS` environment |
367 | /// variable. |
368 | /// |
369 | /// This env var is enabled by default. |
370 | pub fn print_system_cflags(&mut self, print: bool) -> &mut Config { |
371 | self.print_system_cflags = print; |
372 | self |
373 | } |
374 | |
375 | /// Deprecated in favor fo the `probe` function |
376 | #[doc (hidden)] |
377 | pub fn find(&self, name: &str) -> Result<Library, String> { |
378 | self.probe(name).map_err(|e| e.to_string()) |
379 | } |
380 | |
381 | /// Run `pkg-config` to find the library `name`. |
382 | /// |
383 | /// This will use all configuration previously set to specify how |
384 | /// `pkg-config` is run. |
385 | pub fn probe(&self, name: &str) -> Result<Library, Error> { |
386 | let abort_var_name = format!(" {}_NO_PKG_CONFIG" , envify(name)); |
387 | if self.env_var_os(&abort_var_name).is_some() { |
388 | return Err(Error::EnvNoPkgConfig(abort_var_name)); |
389 | } else if !self.target_supported() { |
390 | return Err(Error::CrossCompilation); |
391 | } |
392 | |
393 | let mut library = Library::new(); |
394 | |
395 | let output = self |
396 | .run(name, &["--libs" , "--cflags" ]) |
397 | .map_err(|e| match e { |
398 | Error::Failure { command, output } => Error::ProbeFailure { |
399 | name: name.to_owned(), |
400 | command, |
401 | output, |
402 | }, |
403 | other => other, |
404 | })?; |
405 | library.parse_libs_cflags(name, &output, self); |
406 | |
407 | let output = self.run(name, &["--modversion" ])?; |
408 | library.parse_modversion(str::from_utf8(&output).unwrap()); |
409 | |
410 | Ok(library) |
411 | } |
412 | |
413 | /// True if pkg-config is used for the host system, or configured for cross-compilation |
414 | pub fn target_supported(&self) -> bool { |
415 | let target = env::var_os("TARGET" ).unwrap_or_default(); |
416 | let host = env::var_os("HOST" ).unwrap_or_default(); |
417 | |
418 | // Only use pkg-config in host == target situations by default (allowing an |
419 | // override). |
420 | if host == target { |
421 | return true; |
422 | } |
423 | |
424 | // pkg-config may not be aware of cross-compilation, and require |
425 | // a wrapper script that sets up platform-specific prefixes. |
426 | match self.targetted_env_var("PKG_CONFIG_ALLOW_CROSS" ) { |
427 | // don't use pkg-config if explicitly disabled |
428 | Some(ref val) if val == "0" => false, |
429 | Some(_) => true, |
430 | None => { |
431 | // if not disabled, and pkg-config is customized, |
432 | // then assume it's prepared for cross-compilation |
433 | self.targetted_env_var("PKG_CONFIG" ).is_some() |
434 | || self.targetted_env_var("PKG_CONFIG_SYSROOT_DIR" ).is_some() |
435 | } |
436 | } |
437 | } |
438 | |
439 | /// Deprecated in favor of the top level `get_variable` function |
440 | #[doc (hidden)] |
441 | pub fn get_variable(package: &str, variable: &str) -> Result<String, String> { |
442 | get_variable(package, variable).map_err(|e| e.to_string()) |
443 | } |
444 | |
445 | fn targetted_env_var(&self, var_base: &str) -> Option<OsString> { |
446 | match (env::var("TARGET" ), env::var("HOST" )) { |
447 | (Ok(target), Ok(host)) => { |
448 | let kind = if host == target { "HOST" } else { "TARGET" }; |
449 | let target_u = target.replace('-' , "_" ); |
450 | |
451 | self.env_var_os(&format!(" {}_ {}" , var_base, target)) |
452 | .or_else(|| self.env_var_os(&format!(" {}_ {}" , var_base, target_u))) |
453 | .or_else(|| self.env_var_os(&format!(" {}_ {}" , kind, var_base))) |
454 | .or_else(|| self.env_var_os(var_base)) |
455 | } |
456 | (Err(env::VarError::NotPresent), _) | (_, Err(env::VarError::NotPresent)) => { |
457 | self.env_var_os(var_base) |
458 | } |
459 | (Err(env::VarError::NotUnicode(s)), _) | (_, Err(env::VarError::NotUnicode(s))) => { |
460 | panic!( |
461 | "HOST or TARGET environment variable is not valid unicode: {:?}" , |
462 | s |
463 | ) |
464 | } |
465 | } |
466 | } |
467 | |
468 | fn env_var_os(&self, name: &str) -> Option<OsString> { |
469 | if self.env_metadata { |
470 | println!("cargo:rerun-if-env-changed= {}" , name); |
471 | } |
472 | env::var_os(name) |
473 | } |
474 | |
475 | fn is_static(&self, name: &str) -> bool { |
476 | self.statik.unwrap_or_else(|| self.infer_static(name)) |
477 | } |
478 | |
479 | fn run(&self, name: &str, args: &[&str]) -> Result<Vec<u8>, Error> { |
480 | let pkg_config_exe = self.targetted_env_var("PKG_CONFIG" ); |
481 | let fallback_exe = if pkg_config_exe.is_none() { |
482 | Some(OsString::from("pkgconf" )) |
483 | } else { |
484 | None |
485 | }; |
486 | let exe = pkg_config_exe.unwrap_or_else(|| OsString::from("pkg-config" )); |
487 | |
488 | let mut cmd = self.command(exe, name, args); |
489 | |
490 | match cmd.output().or_else(|e| { |
491 | if let Some(exe) = fallback_exe { |
492 | self.command(exe, name, args).output() |
493 | } else { |
494 | Err(e) |
495 | } |
496 | }) { |
497 | Ok(output) => { |
498 | if output.status.success() { |
499 | Ok(output.stdout) |
500 | } else { |
501 | Err(Error::Failure { |
502 | command: format!(" {:?}" , cmd), |
503 | output, |
504 | }) |
505 | } |
506 | } |
507 | Err(cause) => Err(Error::Command { |
508 | command: format!(" {:?}" , cmd), |
509 | cause, |
510 | }), |
511 | } |
512 | } |
513 | |
514 | fn command(&self, exe: OsString, name: &str, args: &[&str]) -> Command { |
515 | let mut cmd = Command::new(exe); |
516 | if self.is_static(name) { |
517 | cmd.arg("--static" ); |
518 | } |
519 | cmd.args(args).args(&self.extra_args); |
520 | |
521 | if let Some(value) = self.targetted_env_var("PKG_CONFIG_PATH" ) { |
522 | cmd.env("PKG_CONFIG_PATH" , value); |
523 | } |
524 | if let Some(value) = self.targetted_env_var("PKG_CONFIG_LIBDIR" ) { |
525 | cmd.env("PKG_CONFIG_LIBDIR" , value); |
526 | } |
527 | if let Some(value) = self.targetted_env_var("PKG_CONFIG_SYSROOT_DIR" ) { |
528 | cmd.env("PKG_CONFIG_SYSROOT_DIR" , value); |
529 | } |
530 | if self.print_system_libs { |
531 | cmd.env("PKG_CONFIG_ALLOW_SYSTEM_LIBS" , "1" ); |
532 | } |
533 | if self.print_system_cflags { |
534 | cmd.env("PKG_CONFIG_ALLOW_SYSTEM_CFLAGS" , "1" ); |
535 | } |
536 | cmd.arg(name); |
537 | match self.min_version { |
538 | Bound::Included(ref version) => { |
539 | cmd.arg(&format!(" {} >= {}" , name, version)); |
540 | } |
541 | Bound::Excluded(ref version) => { |
542 | cmd.arg(&format!(" {} > {}" , name, version)); |
543 | } |
544 | _ => (), |
545 | } |
546 | match self.max_version { |
547 | Bound::Included(ref version) => { |
548 | cmd.arg(&format!(" {} <= {}" , name, version)); |
549 | } |
550 | Bound::Excluded(ref version) => { |
551 | cmd.arg(&format!(" {} < {}" , name, version)); |
552 | } |
553 | _ => (), |
554 | } |
555 | cmd |
556 | } |
557 | |
558 | fn print_metadata(&self, s: &str) { |
559 | if self.cargo_metadata { |
560 | println!("cargo: {}" , s); |
561 | } |
562 | } |
563 | |
564 | fn infer_static(&self, name: &str) -> bool { |
565 | let name = envify(name); |
566 | if self.env_var_os(&format!(" {}_STATIC" , name)).is_some() { |
567 | true |
568 | } else if self.env_var_os(&format!(" {}_DYNAMIC" , name)).is_some() { |
569 | false |
570 | } else if self.env_var_os("PKG_CONFIG_ALL_STATIC" ).is_some() { |
571 | true |
572 | } else if self.env_var_os("PKG_CONFIG_ALL_DYNAMIC" ).is_some() { |
573 | false |
574 | } else { |
575 | false |
576 | } |
577 | } |
578 | } |
579 | |
580 | // Implement Default manualy since Bound does not implement Default. |
581 | impl Default for Config { |
582 | fn default() -> Config { |
583 | Config { |
584 | statik: None, |
585 | min_version: Bound::Unbounded, |
586 | max_version: Bound::Unbounded, |
587 | extra_args: vec![], |
588 | print_system_cflags: false, |
589 | print_system_libs: false, |
590 | cargo_metadata: false, |
591 | env_metadata: false, |
592 | } |
593 | } |
594 | } |
595 | |
596 | impl Library { |
597 | fn new() -> Library { |
598 | Library { |
599 | libs: Vec::new(), |
600 | link_paths: Vec::new(), |
601 | link_files: Vec::new(), |
602 | include_paths: Vec::new(), |
603 | ld_args: Vec::new(), |
604 | frameworks: Vec::new(), |
605 | framework_paths: Vec::new(), |
606 | defines: HashMap::new(), |
607 | version: String::new(), |
608 | _priv: (), |
609 | } |
610 | } |
611 | |
612 | /// Extract the &str to pass to cargo:rustc-link-lib from a filename (just the file name, not including directories) |
613 | /// using target-specific logic. |
614 | fn extract_lib_from_filename<'a>(target: &str, filename: &'a str) -> Option<&'a str> { |
615 | fn test_suffixes<'b>(filename: &'b str, suffixes: &[&str]) -> Option<&'b str> { |
616 | for suffix in suffixes { |
617 | if filename.ends_with(suffix) { |
618 | return Some(&filename[..filename.len() - suffix.len()]); |
619 | } |
620 | } |
621 | None |
622 | } |
623 | |
624 | let prefix = "lib" ; |
625 | if target.contains("msvc" ) { |
626 | // According to link.exe documentation: |
627 | // https://learn.microsoft.com/en-us/cpp/build/reference/link-input-files?view=msvc-170 |
628 | // |
629 | // LINK doesn't use file extensions to make assumptions about the contents of a file. |
630 | // Instead, LINK examines each input file to determine what kind of file it is. |
631 | // |
632 | // However, rustc appends `.lib` to the string it receives from the -l command line argument, |
633 | // which it receives from Cargo via cargo:rustc-link-lib: |
634 | // https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L828 |
635 | // https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L843 |
636 | // So the only file extension that works for MSVC targets is `.lib` |
637 | return test_suffixes(filename, &[".lib" ]); |
638 | } else if target.contains("windows" ) && target.contains("gnu" ) { |
639 | // GNU targets for Windows, including gnullvm, use `LinkerFlavor::Gcc` internally in rustc, |
640 | // which tells rustc to use the GNU linker. rustc does not prepend/append to the string it |
641 | // receives via the -l command line argument before passing it to the linker: |
642 | // https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L446 |
643 | // https://github.com/rust-lang/rust/blob/657f246812ab2684e3c3954b1c77f98fd59e0b21/compiler/rustc_codegen_ssa/src/back/linker.rs#L457 |
644 | // GNU ld can work with more types of files than just the .lib files that MSVC's link.exe needs. |
645 | // GNU ld will prepend the `lib` prefix to the filename if necessary, so it is okay to remove |
646 | // the `lib` prefix from the filename. The `.a` suffix *requires* the `lib` prefix. |
647 | // https://sourceware.org/binutils/docs-2.39/ld.html#index-direct-linking-to-a-dll |
648 | if filename.starts_with(prefix) { |
649 | let filename = &filename[prefix.len()..]; |
650 | return test_suffixes(filename, &[".dll.a" , ".dll" , ".lib" , ".a" ]); |
651 | } else { |
652 | return test_suffixes(filename, &[".dll.a" , ".dll" , ".lib" ]); |
653 | } |
654 | } else if target.contains("apple" ) { |
655 | if filename.starts_with(prefix) { |
656 | let filename = &filename[prefix.len()..]; |
657 | return test_suffixes(filename, &[".a" , ".so" , ".dylib" ]); |
658 | } |
659 | return None; |
660 | } else { |
661 | if filename.starts_with(prefix) { |
662 | let filename = &filename[prefix.len()..]; |
663 | return test_suffixes(filename, &[".a" , ".so" ]); |
664 | } |
665 | return None; |
666 | } |
667 | } |
668 | |
669 | fn parse_libs_cflags(&mut self, name: &str, output: &[u8], config: &Config) { |
670 | let mut is_msvc = false; |
671 | let target = env::var("TARGET" ); |
672 | if let Ok(target) = &target { |
673 | if target.contains("msvc" ) { |
674 | is_msvc = true; |
675 | } |
676 | } |
677 | |
678 | let system_roots = if cfg!(target_os = "macos" ) { |
679 | vec![PathBuf::from("/Library" ), PathBuf::from("/System" )] |
680 | } else { |
681 | let sysroot = config |
682 | .env_var_os("PKG_CONFIG_SYSROOT_DIR" ) |
683 | .or_else(|| config.env_var_os("SYSROOT" )) |
684 | .map(PathBuf::from); |
685 | |
686 | if cfg!(target_os = "windows" ) { |
687 | if let Some(sysroot) = sysroot { |
688 | vec![sysroot] |
689 | } else { |
690 | vec![] |
691 | } |
692 | } else { |
693 | vec![sysroot.unwrap_or_else(|| PathBuf::from("/usr" ))] |
694 | } |
695 | }; |
696 | |
697 | let mut dirs = Vec::new(); |
698 | let statik = config.is_static(name); |
699 | |
700 | let words = split_flags(output); |
701 | |
702 | // Handle single-character arguments like `-I/usr/include` |
703 | let parts = words |
704 | .iter() |
705 | .filter(|l| l.len() > 2) |
706 | .map(|arg| (&arg[0..2], &arg[2..])); |
707 | for (flag, val) in parts { |
708 | match flag { |
709 | "-L" => { |
710 | let meta = format!("rustc-link-search=native= {}" , val); |
711 | config.print_metadata(&meta); |
712 | dirs.push(PathBuf::from(val)); |
713 | self.link_paths.push(PathBuf::from(val)); |
714 | } |
715 | "-F" => { |
716 | let meta = format!("rustc-link-search=framework= {}" , val); |
717 | config.print_metadata(&meta); |
718 | self.framework_paths.push(PathBuf::from(val)); |
719 | } |
720 | "-I" => { |
721 | self.include_paths.push(PathBuf::from(val)); |
722 | } |
723 | "-l" => { |
724 | // These are provided by the CRT with MSVC |
725 | if is_msvc && ["m" , "c" , "pthread" ].contains(&val) { |
726 | continue; |
727 | } |
728 | |
729 | if statik && is_static_available(val, &system_roots, &dirs) { |
730 | let meta = format!("rustc-link-lib=static= {}" , val); |
731 | config.print_metadata(&meta); |
732 | } else { |
733 | let meta = format!("rustc-link-lib= {}" , val); |
734 | config.print_metadata(&meta); |
735 | } |
736 | |
737 | self.libs.push(val.to_string()); |
738 | } |
739 | "-D" => { |
740 | let mut iter = val.split('=' ); |
741 | self.defines.insert( |
742 | iter.next().unwrap().to_owned(), |
743 | iter.next().map(|s| s.to_owned()), |
744 | ); |
745 | } |
746 | _ => {} |
747 | } |
748 | } |
749 | |
750 | // Handle multi-character arguments with space-separated value like `-framework foo` |
751 | let mut iter = words.iter().flat_map(|arg| { |
752 | if arg.starts_with("-Wl," ) { |
753 | arg[4..].split(',' ).collect() |
754 | } else { |
755 | vec![arg.as_ref()] |
756 | } |
757 | }); |
758 | while let Some(part) = iter.next() { |
759 | match part { |
760 | "-framework" => { |
761 | if let Some(lib) = iter.next() { |
762 | let meta = format!("rustc-link-lib=framework= {}" , lib); |
763 | config.print_metadata(&meta); |
764 | self.frameworks.push(lib.to_string()); |
765 | } |
766 | } |
767 | "-isystem" | "-iquote" | "-idirafter" => { |
768 | if let Some(inc) = iter.next() { |
769 | self.include_paths.push(PathBuf::from(inc)); |
770 | } |
771 | } |
772 | _ => { |
773 | let path = std::path::Path::new(part); |
774 | if path.is_file() { |
775 | // Cargo doesn't have a means to directly specify a file path to link, |
776 | // so split up the path into the parent directory and library name. |
777 | // TODO: pass file path directly when link-arg library type is stabilized |
778 | // https://github.com/rust-lang/rust/issues/99427 |
779 | if let (Some(dir), Some(file_name), Ok(target)) = |
780 | (path.parent(), path.file_name(), &target) |
781 | { |
782 | match Self::extract_lib_from_filename( |
783 | target, |
784 | &file_name.to_string_lossy(), |
785 | ) { |
786 | Some(lib_basename) => { |
787 | let link_search = |
788 | format!("rustc-link-search= {}" , dir.display()); |
789 | config.print_metadata(&link_search); |
790 | |
791 | let link_lib = format!("rustc-link-lib= {}" , lib_basename); |
792 | config.print_metadata(&link_lib); |
793 | self.link_files.push(PathBuf::from(path)); |
794 | } |
795 | None => { |
796 | 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); |
797 | } |
798 | } |
799 | } |
800 | } |
801 | } |
802 | } |
803 | } |
804 | |
805 | let linker_options = words.iter().filter(|arg| arg.starts_with("-Wl," )); |
806 | for option in linker_options { |
807 | let mut pop = false; |
808 | let mut ld_option = vec![]; |
809 | for subopt in option[4..].split(',' ) { |
810 | if pop { |
811 | pop = false; |
812 | continue; |
813 | } |
814 | |
815 | if subopt == "-framework" { |
816 | pop = true; |
817 | continue; |
818 | } |
819 | |
820 | ld_option.push(subopt); |
821 | } |
822 | |
823 | let meta = format!("rustc-link-arg=-Wl, {}" , ld_option.join("," )); |
824 | config.print_metadata(&meta); |
825 | |
826 | self.ld_args |
827 | .push(ld_option.into_iter().map(String::from).collect()); |
828 | } |
829 | } |
830 | |
831 | fn parse_modversion(&mut self, output: &str) { |
832 | self.version.push_str(output.lines().next().unwrap().trim()); |
833 | } |
834 | } |
835 | |
836 | fn envify(name: &str) -> String { |
837 | nameimpl Iterator .chars() |
838 | .map(|c: char| c.to_ascii_uppercase()) |
839 | .map(|c: char| if c == '-' { '_' } else { c }) |
840 | .collect() |
841 | } |
842 | |
843 | /// System libraries should only be linked dynamically |
844 | fn is_static_available(name: &str, system_roots: &[PathBuf], dirs: &[PathBuf]) -> bool { |
845 | let libname: String = format!("lib {}.a" , name); |
846 | |
847 | dirs.iter().any(|dir: &PathBuf| { |
848 | !system_roots.iter().any(|sys: &PathBuf| dir.starts_with(base:sys)) && dir.join(&libname).exists() |
849 | }) |
850 | } |
851 | |
852 | /// Split output produced by pkg-config --cflags and / or --libs into separate flags. |
853 | /// |
854 | /// Backslash in output is used to preserve literal meaning of following byte. Different words are |
855 | /// separated by unescaped space. Other whitespace characters generally should not occur unescaped |
856 | /// at all, apart from the newline at the end of output. For compatibility with what others |
857 | /// consumers of pkg-config output would do in this scenario, they are used here for splitting as |
858 | /// well. |
859 | fn split_flags(output: &[u8]) -> Vec<String> { |
860 | let mut word = Vec::new(); |
861 | let mut words = Vec::new(); |
862 | let mut escaped = false; |
863 | |
864 | for &b in output { |
865 | match b { |
866 | _ if escaped => { |
867 | escaped = false; |
868 | word.push(b); |
869 | } |
870 | b' \\' => escaped = true, |
871 | b' \t' | b' \n' | b' \r' | b' ' => { |
872 | if !word.is_empty() { |
873 | words.push(String::from_utf8(word).unwrap()); |
874 | word = Vec::new(); |
875 | } |
876 | } |
877 | _ => word.push(b), |
878 | } |
879 | } |
880 | |
881 | if !word.is_empty() { |
882 | words.push(String::from_utf8(word).unwrap()); |
883 | } |
884 | |
885 | words |
886 | } |
887 | |
888 | #[cfg (test)] |
889 | mod tests { |
890 | use super::*; |
891 | |
892 | #[test ] |
893 | #[cfg (target_os = "macos" )] |
894 | fn system_library_mac_test() { |
895 | use std::path::Path; |
896 | |
897 | let system_roots = vec![PathBuf::from("/Library" ), PathBuf::from("/System" )]; |
898 | |
899 | assert!(!is_static_available( |
900 | "PluginManager" , |
901 | &system_roots, |
902 | &[PathBuf::from("/Library/Frameworks" )] |
903 | )); |
904 | assert!(!is_static_available( |
905 | "python2.7" , |
906 | &system_roots, |
907 | &[PathBuf::from( |
908 | "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/config" |
909 | )] |
910 | )); |
911 | assert!(!is_static_available( |
912 | "ffi_convenience" , |
913 | &system_roots, |
914 | &[PathBuf::from( |
915 | "/Library/Ruby/Gems/2.0.0/gems/ffi-1.9.10/ext/ffi_c/libffi-x86_64/.libs" |
916 | )] |
917 | )); |
918 | |
919 | // Homebrew is in /usr/local, and it's not a part of the OS |
920 | if Path::new("/usr/local/lib/libpng16.a" ).exists() { |
921 | assert!(is_static_available( |
922 | "png16" , |
923 | &system_roots, |
924 | &[PathBuf::from("/usr/local/lib" )] |
925 | )); |
926 | |
927 | let libpng = Config::new() |
928 | .range_version("1" .."99" ) |
929 | .probe("libpng16" ) |
930 | .unwrap(); |
931 | assert!(libpng.version.find(' \n' ).is_none()); |
932 | } |
933 | } |
934 | |
935 | #[test ] |
936 | #[cfg (target_os = "linux" )] |
937 | fn system_library_linux_test() { |
938 | assert!(!is_static_available( |
939 | "util" , |
940 | &[PathBuf::from("/usr" )], |
941 | &[PathBuf::from("/usr/lib/x86_64-linux-gnu" )] |
942 | )); |
943 | assert!(!is_static_available( |
944 | "dialog" , |
945 | &[PathBuf::from("/usr" )], |
946 | &[PathBuf::from("/usr/lib" )] |
947 | )); |
948 | } |
949 | |
950 | fn test_library_filename(target: &str, filename: &str) { |
951 | assert_eq!( |
952 | Library::extract_lib_from_filename(target, filename), |
953 | Some("foo" ) |
954 | ); |
955 | } |
956 | |
957 | #[test ] |
958 | fn link_filename_linux() { |
959 | let target = "x86_64-unknown-linux-gnu" ; |
960 | test_library_filename(target, "libfoo.a" ); |
961 | test_library_filename(target, "libfoo.so" ); |
962 | } |
963 | |
964 | #[test ] |
965 | fn link_filename_apple() { |
966 | let target = "x86_64-apple-darwin" ; |
967 | test_library_filename(target, "libfoo.a" ); |
968 | test_library_filename(target, "libfoo.so" ); |
969 | test_library_filename(target, "libfoo.dylib" ); |
970 | } |
971 | |
972 | #[test ] |
973 | fn link_filename_msvc() { |
974 | let target = "x86_64-pc-windows-msvc" ; |
975 | // static and dynamic libraries have the same .lib suffix |
976 | test_library_filename(target, "foo.lib" ); |
977 | } |
978 | |
979 | #[test ] |
980 | fn link_filename_mingw() { |
981 | let target = "x86_64-pc-windows-gnu" ; |
982 | test_library_filename(target, "foo.lib" ); |
983 | test_library_filename(target, "libfoo.a" ); |
984 | test_library_filename(target, "foo.dll" ); |
985 | test_library_filename(target, "foo.dll.a" ); |
986 | } |
987 | } |
988 | |