rsconf/lib.rs
1//! The `rsconf` crate contains `build.rs` helper utilities and funcitionality to assist with
2//! managing complicated build scripts, interacting with the native/system headers and libraries,
3//! exposing rust constants based off of system headers, and conditionally compiling rust code based
4//! off the presence or absence of certain functionality in the system headers and libraries.
5//!
6//! This crate can be used standalone or in conjunction with the [`cc`
7//! crate](https://docs.rs/cc/latest/cc/) when introspecting the build system's environment.
8//!
9//! In addition to facilitating easier ffi and other native system interop, `rsconf` also exposes a
10//! strongly typed API for interacting with `cargo` at build-time and influencing its behavior,
11//! including more user-friendly alternatives to the low-level `println!("cargo:xxx")` "api" used to
12//! enable features, enable `#[cfg(...)]` conditional compilation blocks or define `cfg` values, and
13//! more.
14
15mod tempdir;
16#[cfg(test)]
17mod tests;
18
19use cc::Build;
20use std::borrow::Cow;
21use std::ffi::{OsStr, OsString};
22use std::io::prelude::*;
23use std::path::PathBuf;
24use std::process::{Command, Output};
25use std::sync::atomic::{AtomicI32, Ordering};
26use tempdir::TempDir;
27
28static FILE_COUNTER: AtomicI32 = AtomicI32::new(0);
29type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
30
31/// Exposes an interface for testing whether the target system supports a particular feature or
32/// provides certain functionality. This is the bulk of the `rsconf` api.
33pub struct Target {
34 /// Whether or not we are compiling with `cl.exe` (and not `clang.exe`) under `xxx-pc-windows-msvc`.
35 is_cl: bool,
36 temp: TempDir,
37 toolchain: Build,
38 verbose: bool,
39}
40
41macro_rules! snippet {
42 ($name:expr) => {
43 include_str!(concat!("../snippets/", $name))
44 };
45}
46
47/// An error encountered during the compliation stage.
48///
49/// This is currently not public because we only return it as [`BoxedError`].
50#[derive(Debug)]
51struct CompilationError {
52 output: Output,
53}
54
55impl std::fmt::Display for CompilationError {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.write_fmt(format_args!(
58 "Compilation error: {}",
59 String::from_utf8_lossy(&self.output.stderr)
60 ))
61 }
62}
63
64impl std::error::Error for CompilationError {}
65
66fn output_or_err(output: Output) -> Result<(String, String), BoxedError> {
67 if output.status.success() {
68 Ok((
69 String::from_utf8(output.stdout)?,
70 String::from_utf8(output.stderr)?,
71 ))
72 } else {
73 Err(Box::new(CompilationError { output }))
74 }
75}
76
77#[derive(Copy, Clone, Debug, PartialEq, Eq)]
78enum BuildMode {
79 Executable,
80 ObjectFile,
81}
82
83/// Specifies how a dependency library is linked.
84#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
85pub enum LinkType {
86 /// Cargo is instructed to link the library without specifying/overriding how linking is
87 /// performed. If an environment variable `LIBNAME_STATIC` is present, the dependency will be
88 /// statically linked. (This way, downstream consumers of the crate may influence how the
89 /// dependency is linked without modifying the build script and/or features.)
90 ///
91 /// Cargo is instructed to automatically rerun the build script if an environment variable by
92 /// this name exists; you do not have to call [`rebuild_if_env_changed()`] yourself.
93 #[default]
94 Default,
95 /// Cargo will be instructed to explicitly dynamically link against the target library,
96 /// overriding the default configuration specified by the configuration or the toolchain.
97 Dynamic,
98 /// Cargo will be instructed to explicitly statically link against the target library,
99 /// overriding the default configuration specified by the configuration or the toolchain.
100 Static,
101}
102
103impl LinkType {
104 fn emit_link_line(&self, lib: &str) {
105 match self {
106 LinkType::Static => println!("cargo:rustc-link-lib=static={lib}"),
107 LinkType::Dynamic => println!("cargo:rustc-link-lib=dylib={lib}"),
108 LinkType::Default => {
109 // We do not specify the build type unless the LIBNAME_STATIC environment variable
110 // is defined (and not set to 0), in which was we emit a static linkage instruction.
111 let name = format!("{}_STATIC", lib.to_ascii_uppercase());
112 println!("cargo:rerun-if-env-changed={name}");
113 match std::env::var(name).as_deref() {
114 Err(_) | Ok("0") => println!("cargo:rustc-link-lib={lib}"),
115 _ => LinkType::Static.emit_link_line(lib),
116 }
117 }
118 }
119 }
120}
121
122/// Instruct Cargo to link the target object against `library`.
123pub fn link_library(library: &str, how: LinkType) {
124 how.emit_link_line(library)
125}
126
127/// Instruct Cargo to link the target object against `libraries` in the order provided.
128pub fn link_libraries(libraries: &[&str], how: LinkType) {
129 for lib in libraries {
130 how.emit_link_line(lib)
131 }
132}
133
134/// Instruct Cargo to rerun the build script if the provided path changes.
135///
136/// Change detection is based off the modification time (mtime). If the path is to a directory, the
137/// build script is re-run if any files under that directory are modified.
138///
139/// By default, Cargo reruns the build script if any file in the source tree is modified. To make it
140/// ignore changes, specify a file. To make it ignore all changes, call this with `"build.rs"` as
141/// the target.
142pub fn rebuild_if_path_changed(path: &str) {
143 println!("cargo:rerun-if-changed={path}");
144}
145
146/// Instruct Cargo to rerun the build script if any of the provided paths change.
147///
148/// See [`rebuild_if_path_changed()`] for more information.
149pub fn rebuild_if_paths_changed(paths: &[&str]) {
150 for path in paths {
151 rebuild_if_path_changed(path)
152 }
153}
154
155/// Instruct Cargo to rerun the build script if the named environment variable changes.
156pub fn rebuild_if_env_changed(var: &str) {
157 println!("cargo:rerun-if-env-changed={var}");
158}
159
160/// Instruct Cargo to rerun the build script if any of the named environment variables change.
161pub fn rebuild_if_envs_changed(vars: &[&str]) {
162 for var in vars {
163 rebuild_if_env_changed(var);
164 }
165}
166
167/// Emit a compile-time warning.
168///
169/// This is typically only shown for the current crate when building with `cargo build`, but
170/// warnings for non-path dependencies can be shown by using `cargo build -vv`.
171#[macro_export]
172macro_rules! warn {
173 ($msg:tt $(, $($arg:tt)*)?) => {{
174 println!(concat!("cargo:warning=", $msg) $(, $($arg)*)?)
175 }};
176}
177
178/// Enables a feature flag that compiles code annotated with `#[cfg(feature = "name")]`.
179///
180/// The feature does not have to be named in `Cargo.toml` to be used here or in your code, but any
181/// features dynamically enabled via this script will not participate in dependency resolution.
182///
183/// As of rust 1.80, features enabled in `build.rs` but not declared in `Cargo.toml` might trigger
184/// build-time warnings; use [`declare_feature()`] instead to avoid this warning.
185pub fn enable_feature(name: &str) {
186 declare_feature(name, true)
187}
188
189/// Informs the compiler of a `feature` with the name `name`, possibly enabled.
190///
191/// The feature does not have to be named in `Cargo.toml` to be used here or in your code, but any
192/// features dynamically enabled via this script will not participate in dependency resolution.
193pub fn declare_feature(name: &str, enabled: bool) {
194 if name.chars().any(|c| c == '"') {
195 panic!("Invalid feature name: {name}");
196 }
197 declare_cfg_values("feature", &[name]);
198 if enabled {
199 println!("cargo:rustc-cfg=feature=\"{name}\"");
200 }
201}
202
203/// Informs the compiler of a `cfg` with the name `name`, possibly enabled.
204///
205/// Enables conditional compilation of code behind `#[cfg(name)]` or with `if cfg!(name)`
206/// (without quotes around `name`).
207///
208/// As of rust 1.80, using `#[cfg(foo)]` when said feature is not enabled results in a
209/// compile-time warning as rust tries to protect against inadvertent use of invalid/unknown
210/// features. Unlike [`enable_cfg()`], this function informs `rustc` about the presence of a feature
211/// called `name` even when it's not enabled, so that `#[cfg(foo)]` or `#[cfg(not(foo))] do not
212/// cause warnings when the `foo` cfg is not enabled.
213///
214/// See also: [`declare_cfg_values()`].
215pub fn declare_cfg(name: &str, enabled: bool) {
216 if name.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') {
217 panic!("Invalid cfg name {name}");
218 }
219 // Use #[cfg(version = "1.80.0")] when RFC 2523 finally lands
220 if rustc_version()
221 .map(|v| !v.cmp(&(1, 80, 0)).is_lt())
222 .unwrap_or(true)
223 {
224 println!("cargo:rustc-check-cfg=cfg({name})");
225 }
226 if enabled {
227 println!("cargo:rustc-cfg={name}");
228 }
229}
230
231/// Enables Cargo/rustc feature with the name `name`.
232///
233/// Allows conditional compilation of code behind `#[cfg(name)]` or with `if cfg!(name)` (without
234/// quotes around `name`).
235///
236/// See [`set_cfg_value()`] to set a `(name, value)` tuple to enable conditional compilation of the
237/// form `#[cfg(name = "value")]` for cases where `name` is not a boolean cfg but rather takes any
238/// of several discrete values.
239///
240/// Note the different from `#[cfg(feature = "name")]`! The configuration is invisible to end users
241/// of your code (i.e. `name` does not appear anywhere in `Cargo.toml`) and does not participate in
242/// dependency resolution.
243pub fn enable_cfg(name: &str) {
244 declare_cfg(name, true);
245}
246
247// TODO: Add a builder method to encompass the functionality of declare_cfg()/set_cfg()/
248// declare_cfg_values()/set_cfg_value(). Something like
249// add_cfg("name").with_values(["a", "b", "c"])
250// followed by .enable() or .set_value("a")
251
252/// Inform the compiler of a cfg with name `name` and all its known valid values.
253///
254/// Call this before calling [`set_cfg_value()`] to avoid compiler warnings about unrecognized cfg
255/// values under rust 1.80+.
256pub fn declare_cfg_values(name: &str, values: &[&str]) {
257 if name.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') {
258 panic!("Invalid cfg name {name}");
259 }
260 // Use #[cfg(version = "1.80.0")] when RFC 2523 finally lands
261 if rustc_version()
262 .map(|v| !v.cmp(&(1, 80, 0)).is_lt())
263 .unwrap_or(true)
264 {
265 let payload = values
266 .iter()
267 .inspect(|value| {
268 if value.chars().any(|c| c == '"') {
269 panic!("Invalid value {value} for cfg {name}");
270 }
271 })
272 .map(|v| format!("\"{v}\""))
273 .collect::<Vec<_>>()
274 .join(",");
275 println!("cargo:rustc-check-cfg=cfg({name}, values({payload}))");
276 }
277}
278
279/// Activates conditional compilation for code behind `#[cfg(name = "value")]` or with `if cfg!(name
280/// = "value")`.
281///
282/// As with [`enable_cfg()`], this is entirely internal to your code: `name` should not appear in
283/// `Cargo.toml` and this configuration does not participate in dependency resolution (which takes
284/// place before your build script is called).
285///
286/// Call [`declare_cfg_values()`] beforehand to inform the compiler of all possible values for this
287/// cfg or else rustc 1.80+ will issue a compile-time warning about unrecognized cfg values.
288pub fn set_cfg_value(name: &str, value: &str) {
289 if value.chars().any(|c| c == '"') {
290 panic!("Invalid value {value} for cfg {name}");
291 }
292 println!("cargo:rustc-cfg={name}={value}\"");
293}
294
295/// Makes an environment variable available to your code at build time, letting you use the value as
296/// a compile-time constant with `env!(NAME)`.
297pub fn set_env_value(name: &str, value: &str) {
298 if value.chars().any(|c| c == '"') {
299 panic!("Invalid value {value} for env var {name}");
300 }
301 println!("cargo:rustc-env={name}={value}");
302}
303
304/// Add a path to the list of directories rust will search when attempting to find a library to link
305/// against.
306///
307/// The path does not have to exist as it could be created by the build script at a later date or
308/// could be targeting a different platform altogether.
309pub fn add_library_search_path(dir: &str) {
310 println!("cargo:rustc-link-search={dir}");
311}
312
313impl Target {
314 const NONE: &'static [&'static str] = &[];
315 #[inline(always)]
316 #[allow(non_snake_case)]
317 fn NULL_CB(_: &str, _: &str) {}
318
319 /// Create a new rsconf instance using the default [`cc::Build`] toolchain for the current
320 /// compilation target.
321 ///
322 /// Use [`Target::new_from()`] to use a configured [`cc::Build`] instance instead.
323 pub fn new() -> std::io::Result<Target> {
324 let toolchain = cc::Build::new();
325 Target::new_from(toolchain)
326 }
327
328 /// Create a new rsconf instance from the configured [`cc::Build`] instance `toolchain`.
329 ///
330 /// All tests inherit their base configuration from `toolchain`, so make sure it is configured
331 /// with the appropriate header and library search paths as needed.
332 pub fn new_from(mut toolchain: cc::Build) -> std::io::Result<Target> {
333 let temp = if let Some(out_dir) = std::env::var_os("OUT_DIR") {
334 TempDir::new_in(out_dir)?
335 } else {
336 // Configure Build's OUT_DIR if not set (e.g. for testing)
337 let temp = TempDir::new()?;
338 toolchain.out_dir(&temp);
339 temp
340 };
341
342 let is_cl = cfg!(windows) && toolchain.get_compiler().is_like_msvc();
343
344 Ok(Self {
345 is_cl,
346 temp,
347 toolchain,
348 verbose: false,
349 })
350 }
351
352 /// Enables or disables verbose mode.
353 ///
354 /// In verbose mode, output of rsconf calls to the compiler are displayed to stdout and stderr.
355 /// It is not enabled by default.
356 ///
357 /// Note that `cargo` suppresses all `build.rs` output in case of successful execution by
358 /// default; intentionally fail the build (e.g. add a `panic!()` call) or compile with `cargo
359 /// build -vv` to see verbose output.
360 pub fn set_verbose(&mut self, verbose: bool) {
361 self.verbose = verbose;
362 }
363
364 fn new_temp<S: AsRef<str>>(&self, stub: S, ext: &str) -> PathBuf {
365 let file_num = FILE_COUNTER.fetch_add(1, Ordering::Release);
366 let stub = stub.as_ref();
367 let mut path = self.temp.to_owned();
368 path.push(format!("{stub}-test-{file_num}{ext}"));
369 path
370 }
371
372 fn build<S: AsRef<str>, C>(
373 &self,
374 stub: &str,
375 mode: BuildMode,
376 code: &str,
377 libraries: &[S],
378 callback: C,
379 ) -> Result<PathBuf, BoxedError>
380 where
381 C: FnOnce(&str, &str),
382 {
383 let stub = fs_sanitize(stub);
384
385 let in_path = self.new_temp(&stub, ".c");
386 std::fs::File::create(&in_path)?.write_all(code.as_bytes())?;
387 let exe_ext = if cfg!(unix) { ".out" } else { ".exe" };
388 let obj_ext = if cfg!(unix) { ".o" } else { ".obj" };
389 let out_path = match mode {
390 BuildMode::Executable => self.new_temp(&stub, exe_ext),
391 BuildMode::ObjectFile => self.new_temp(&stub, obj_ext),
392 };
393 let mut cmd = self.toolchain.try_get_compiler()?.to_command();
394 cmd.current_dir(&self.temp);
395
396 let exe = mode == BuildMode::Executable;
397 let link = exe || !libraries.is_empty();
398 let output = if cfg!(unix) || !self.is_cl {
399 cmd.args([in_path.as_os_str(), OsStr::new("-o"), out_path.as_os_str()]);
400 if !link {
401 cmd.arg("-c");
402 } else if !libraries.is_empty() {
403 for library in libraries {
404 cmd.arg(format!("-l{}", library.as_ref()));
405 }
406 }
407 cmd
408 } else {
409 cmd.arg(in_path);
410 let mut output = OsString::from(if exe { "/Fe:" } else { "/Fo:" });
411 output.push(&out_path);
412 cmd.arg(output);
413 if !link {
414 cmd.arg("/c");
415 } else if !libraries.is_empty() {
416 cmd.arg("/link");
417 for library in libraries {
418 let mut library = Cow::from(library.as_ref());
419 if !library.contains('.') {
420 let owned = library + ".lib";
421 library = owned;
422 }
423 cmd.arg(library.as_ref());
424 }
425 }
426 cmd
427 }
428 .output()?;
429
430 // We want to output text in verbose mode but writing directly to stdout doesn't get
431 // intercepted by the cargo test harness. In test mode, we use the slower `println!()`/
432 // `eprintln!()` macros together w/ from_utf8_lossy() to suppress unnecessary output when
433 // we're not investigating the details with `cargo test -- --nocapture`, but we use the
434 // faster approach when we're being used in an actual build script.
435 #[cfg(test)]
436 if self.verbose {
437 println!("{}", String::from_utf8_lossy(&output.stdout));
438 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
439 }
440 #[cfg(not(test))]
441 if self.verbose {
442 std::io::stdout().lock().write_all(&output.stdout).ok();
443 std::io::stderr().lock().write_all(&output.stderr).ok();
444 }
445 // Handle custom `CompilationError` output if we failed to compile.
446 let output = output_or_err(output)?;
447 callback(&output.0, &output.1);
448
449 // Return the path to the resulting exe
450 assert!(out_path.exists());
451 Ok(out_path)
452 }
453
454 /// Checks whether a definition for type `name` exists without pulling in any headers.
455 ///
456 /// This operation does not link the output; only the header file is inspected.
457 pub fn has_type(&self, name: &str) -> bool {
458 let snippet = format!(snippet!("has_type.c"), "", name);
459 self.build(
460 name,
461 BuildMode::ObjectFile,
462 &snippet,
463 Self::NONE,
464 Self::NULL_CB,
465 )
466 .is_ok()
467 }
468
469 /// Checks whether a definition for type `name` exists in the supplied header or headers.
470 ///
471 /// The `headers` are included in the order they are provided for testing. See
472 /// [`has_type()`](Self::has_type) for more info.
473 pub fn has_type_in(&self, name: &str, headers: &[&str]) -> bool {
474 let stub = format!("{}_multi", headers.first().unwrap_or(&"has_type_in"));
475 let snippet = format!(snippet!("has_type.c"), to_includes(headers), name);
476 self.build(
477 &stub,
478 BuildMode::ObjectFile,
479 &snippet,
480 Self::NONE,
481 Self::NULL_CB,
482 )
483 .is_ok()
484 }
485
486 /// Checks whether or not the the requested `symbol` is exported by libc/by default (without
487 /// linking against any additional libraries).
488 ///
489 /// See [`has_symbol_in()`](Self::has_symbol_in) to link against one or more libraries and test.
490 ///
491 /// This only checks for symbols exported by the C abi (so mangled names are required) and does
492 /// not check for compile-time definitions provided by header files.
493 ///
494 /// See [`has_type()`](Self::has_type) to check for compile-time definitions. This
495 /// function will return false if `library` could not be found or could not be linked; see
496 /// [`has_library()`](Self::has_library) to test if `library` can be linked separately.
497 pub fn has_symbol(&self, symbol: &str) -> bool {
498 let snippet = format!(snippet!("has_symbol.c"), symbol);
499 let libs: &'static [&'static str] = &[];
500 self.build(symbol, BuildMode::Executable, &snippet, libs, Self::NULL_CB)
501 .is_ok()
502 }
503
504 /// Like [`has_symbol()`] but links against a library or any number of `libraries`.
505 ///
506 /// You might need to supply multiple libraries if `symbol` is in a library that has its own
507 /// transitive dependencies that must also be linked for compilation to succeed. Note that
508 /// libraries are linked in the order they are provided.
509 ///
510 /// [`has_symbol()`]: Self::has_symbol()
511 pub fn has_symbol_in(&self, symbol: &str, libraries: &[&str]) -> bool {
512 let snippet = format!(snippet!("has_symbol.c"), symbol);
513 self.build(
514 symbol,
515 BuildMode::Executable,
516 &snippet,
517 libraries,
518 Self::NULL_CB,
519 )
520 .is_ok()
521 }
522
523 /// Checks for the presence of all the named symbols in the libraries provided.
524 ///
525 /// Libraries are linked in the order provided. See [`has_symbol()`] and [`has_symbol_in()`] for
526 /// more information.
527 ///
528 /// [`has_symbol()`]: Self::has_symbol()
529 /// [`has_symbol_in()`]: Self::has_symbol_in()
530 pub fn has_symbols_in(&self, symbols: &[&str], libraries: &[&str]) -> bool {
531 symbols
532 .iter()
533 .copied()
534 .all(|symbol| self.has_symbol_in(symbol, libraries))
535 }
536
537 /// Tests whether or not it was possible to link against `library`.
538 ///
539 /// If it is not possible to link against `library` without also linking against its transitive
540 /// dependencies, use [`has_libraries()`](Self::has_libraries) to link against multiple
541 /// libraries (in the order provided).
542 ///
543 /// You should normally pass the name of the library without any prefixes or suffixes. If a
544 /// suffix is provided, it will not be removed.
545 ///
546 /// You may pass a full path to the library (again minus the extension) instead of just the
547 /// library name in order to try linking against a library not in the library search path.
548 /// Alternatively, configure the [`cc::Build`] instance with the search paths as needed before
549 /// passing it to [`Target::new()`].
550 ///
551 /// Under Windows, if `library` does not have an extension it will be suffixed with `.lib` prior
552 /// to testing linking. (This way it works under under both `cl.exe` and `clang.exe`.)
553 pub fn has_library(&self, library: &str) -> bool {
554 let snippet = snippet!("empty.c");
555 self.build(
556 library,
557 BuildMode::ObjectFile,
558 snippet,
559 &[library],
560 Self::NULL_CB,
561 )
562 .is_ok()
563 }
564
565 /// Tests whether or not it was possible to link against all of `libraries`.
566 ///
567 /// See [`has_library()`](Self::has_library()) for more information.
568 ///
569 /// The libraries will be linked in the order they are provided in when testing, which may
570 /// influence the outcome.
571 pub fn has_libraries(&self, libraries: &[&str]) -> bool {
572 let stub = libraries.first().copied().unwrap_or("has_libraries");
573 let snippet = snippet!("empty.c");
574 self.build(
575 stub,
576 BuildMode::ObjectFile,
577 snippet,
578 libraries,
579 Self::NULL_CB,
580 )
581 .is_ok()
582 }
583
584 /// Returns the first library from those provided that can be successfully linked.
585 ///
586 /// Returns a reference to the first library name that was passed in that was ultimately found
587 /// and linked successfully on the target system or `None` otherwise. See
588 /// [`has_library()`](Self::has_library()) for more information.
589 pub fn find_first_library<'a>(&self, libraries: &'a [&str]) -> Option<&'a str> {
590 for lib in libraries {
591 if self.has_library(lib) {
592 return Some(*lib);
593 }
594 }
595 None
596 }
597
598 /// Returns the first library from those provided that can be successfully linked and contains
599 /// all named `symbols`.
600 ///
601 /// Returns a reference to the first library name that was passed in that was ultimately found
602 /// on the target system and contains all the symbol names provided, or `None` if no such
603 /// library was found. See [`has_library()`](Self::has_library()) and [`has_symbol()`] for more
604 /// information.
605 ///
606 /// [`has_symbol()`]: Self::has_symbol()
607 pub fn find_first_library_with<'a>(
608 &self,
609 libraries: &'a [&str],
610 symbols: &[&str],
611 ) -> Option<&'a str> {
612 for lib in libraries {
613 if !self.has_library(lib) {
614 continue;
615 }
616 if self.has_symbols_in(symbols, &[lib]) {
617 return Some(lib);
618 }
619 }
620 None
621 }
622
623 /// Checks whether the [`cc::Build`] passed to [`Target::new()`] as configured can pull in the
624 /// named `header` file.
625 ///
626 /// If including `header` requires pulling in additional headers before it to compile, use
627 /// [`has_headers()`](Self::has_headers) instead to include multiple headers in the order
628 /// they're specified.
629 pub fn has_header(&self, header: &str) -> bool {
630 let snippet = format!(snippet!("has_header.c"), to_include(header));
631 self.build(
632 header,
633 BuildMode::ObjectFile,
634 &snippet,
635 Self::NONE,
636 Self::NULL_CB,
637 )
638 .is_ok()
639 }
640
641 /// Checks whether the [`cc::Build`] passed to [`Target::new()`] as configured can pull in the
642 /// named `headers` in the order they're provided.
643 pub fn has_headers(&self, headers: &[&str]) -> bool {
644 let stub = headers.first().copied().unwrap_or("has_headers");
645 let snippet = format!(snippet!("has_header.c"), to_includes(headers));
646 self.build(
647 stub,
648 BuildMode::ObjectFile,
649 &snippet,
650 Self::NONE,
651 Self::NULL_CB,
652 )
653 .is_ok()
654 }
655
656 /// A convenience function that links against `library` if it is found and linkable.
657 ///
658 /// This is internally a call to [`has_library()`](Self::has_library()) followed by a
659 /// conditional call to [`link_library()`].
660 pub fn try_link_library(&self, library: &str, how: LinkType) -> bool {
661 if self.has_library(library) {
662 link_library(library, how);
663 return true;
664 }
665 false
666 }
667
668 /// A convenience function that links against `libraries` only if they are all found and
669 /// linkable.
670 ///
671 /// This is internally a call to [`has_libraries()`](Self::has_libraries()) followed by a
672 /// conditional call to [`link_libraries()`].
673 pub fn try_link_libraries(&self, libraries: &[&str], how: LinkType) -> bool {
674 if self.has_libraries(libraries) {
675 link_libraries(libraries, how);
676 return true;
677 }
678 false
679 }
680
681 /// Evaluates whether or not `define` is an extant preprocessor definition.
682 ///
683 /// This is the C equivalent of `#ifdef xxxx` and does not check if there is a value associated
684 /// with the definition. (You can use [`r#if()`](Self::if()) to test if a define has a particular
685 /// value.)
686 pub fn ifdef(&self, define: &str, headers: &[&str]) -> bool {
687 let snippet = format!(snippet!("ifdef.c"), to_includes(headers), define);
688 self.build(
689 define,
690 BuildMode::ObjectFile,
691 &snippet,
692 Self::NONE,
693 Self::NULL_CB,
694 )
695 .is_ok()
696 }
697
698 /// Evaluates whether or not `condition` evaluates to true at the C preprocessor time.
699 ///
700 /// This can be used with `condition` set to `defined(FOO)` to perform the equivalent of
701 /// [`ifdef()`](Self::ifdef) or it can be used to check for specific values e.g. with
702 /// `condition` set to something like `FOO != 0`.
703 pub fn r#if(&self, condition: &str, headers: &[&str]) -> bool {
704 let snippet = format!(snippet!("if.c"), to_includes(headers), condition);
705 self.build(
706 condition,
707 BuildMode::ObjectFile,
708 &snippet,
709 Self::NONE,
710 Self::NULL_CB,
711 )
712 .is_ok()
713 }
714
715 /// Attempts to retrieve the definition of `ident` as an `i32` value.
716 ///
717 /// Returns `Ok` in case `ident` was defined, has a concrete value, is a compile-time constant
718 /// (i.e. does not need to be linked to retrieve the value), and is a valid `i32` value.
719 ///
720 /// # Cross-compliation note:
721 ///
722 /// The `get_xxx_value()` methods do not currently support cross-compilation scenarios as they
723 /// require being able to run a binary compiled for the target platform.
724 pub fn get_i32_value(&self, ident: &str, headers: &[&str]) -> Result<i32, BoxedError> {
725 let snippet = format!(snippet!("get_i32_value.c"), to_includes(headers), ident);
726 let exe = self.build(
727 ident,
728 BuildMode::Executable,
729 &snippet,
730 Self::NONE,
731 Self::NULL_CB,
732 )?;
733
734 let output = Command::new(exe).output().map_err(|err| {
735 format!(
736 "Failed to run the test executable: {err}!\n{}",
737 "Note that get_i32_value() does not support cross-compilation!"
738 )
739 })?;
740 Ok(std::str::from_utf8(&output.stdout)?.parse()?)
741 }
742
743 /// Attempts to retrieve the definition of `ident` as a `u32` value.
744 ///
745 /// Returns `Ok` in case `ident` was defined, has a concrete value, is a compile-time constant
746 /// (i.e. does not need to be linked to retrieve the value), and is a valid `u32` value.
747 ///
748 /// # Cross-compliation note:
749 ///
750 /// The `get_xxx_value()` methods do not currently support cross-compilation scenarios as they
751 /// require being able to run a binary compiled for the target platform.
752 pub fn get_u32_value(&self, ident: &str, headers: &[&str]) -> Result<u32, BoxedError> {
753 let snippet = format!(snippet!("get_u32_value.c"), to_includes(headers), ident);
754 let exe = self.build(
755 ident,
756 BuildMode::Executable,
757 &snippet,
758 Self::NONE,
759 Self::NULL_CB,
760 )?;
761
762 let output = Command::new(exe).output().map_err(|err| {
763 format!(
764 "Failed to run the test executable: {err}!\n{}",
765 "Note that get_u32_value() does not support cross-compilation!"
766 )
767 })?;
768 Ok(std::str::from_utf8(&output.stdout)?.parse()?)
769 }
770
771 /// Attempts to retrieve the definition of `ident` as an `i64` value.
772 ///
773 /// Returns `Ok` in case `ident` was defined, has a concrete value, is a compile-time constant
774 /// (i.e. does not need to be linked to retrieve the value), and is a valid `i64` value.
775 ///
776 /// # Cross-compliation note:
777 ///
778 /// The `get_xxx_value()` methods do not currently support cross-compilation scenarios as they
779 /// require being able to run a binary compiled for the target platform.
780 pub fn get_i64_value(&self, ident: &str, headers: &[&str]) -> Result<i64, BoxedError> {
781 let snippet = format!(snippet!("get_i64_value.c"), to_includes(headers), ident);
782 let exe = self.build(
783 ident,
784 BuildMode::Executable,
785 &snippet,
786 Self::NONE,
787 Self::NULL_CB,
788 )?;
789
790 let output = Command::new(exe).output().map_err(|err| {
791 format!(
792 "Failed to run the test executable: {err}!\n{}",
793 "Note that get_i64_value() does not support cross-compilation!"
794 )
795 })?;
796 Ok(std::str::from_utf8(&output.stdout)?.parse()?)
797 }
798
799 /// Attempts to retrieve the definition of `ident` as a `u64` value.
800 ///
801 /// Returns `Ok` in case `ident` was defined, has a concrete value, is a compile-time constant
802 /// (i.e. does not need to be linked to retrieve the value), and is a valid `u64` value.
803 ///
804 /// # Cross-compliation note:
805 ///
806 /// The `get_xxx_value()` methods do not currently support cross-compilation scenarios as they
807 /// require being able to run a binary compiled for the target platform.
808 pub fn get_u64_value(&self, ident: &str, headers: &[&str]) -> Result<u64, BoxedError> {
809 let snippet = format!(snippet!("get_u64_value.c"), to_includes(headers), ident);
810 let exe = self.build(
811 ident,
812 BuildMode::Executable,
813 &snippet,
814 Self::NONE,
815 Self::NULL_CB,
816 )?;
817
818 let output = Command::new(exe).output().map_err(|err| {
819 format!(
820 "Failed to run the test executable: {err}!\n{}",
821 "Note that get_u64_value() does not support cross-compilation!"
822 )
823 })?;
824 Ok(std::str::from_utf8(&output.stdout)?.parse()?)
825 }
826
827 /// Retrieve the definition of a C preprocessor macro or define.
828 ///
829 /// For "function macros" like `max(x, y)`, make sure to supply parentheses and pass in
830 /// placeholders for the parameters (like the `x` and `y` in the example); they will be returned
831 /// as-is in the expanded output.
832 pub fn get_macro_value(
833 &self,
834 ident: &str,
835 headers: &[&str],
836 ) -> Result<Option<String>, BoxedError> {
837 // We use `ident` twice: to check if it's defined then to get its value
838 // For "function macros", the first should be without parentheses!
839 let bare_name = if let Some(idx) = ident.find('(') {
840 std::str::from_utf8(&ident.as_bytes()[..idx]).unwrap()
841 } else {
842 ident
843 };
844 let snippet = format!(
845 snippet!("get_macro_value.c"),
846 to_includes(headers),
847 bare_name,
848 ident
849 );
850 let mut result = None;
851 let callback = |stdout: &str, stderr: &str| {
852 let buffer = if self.is_cl { &stdout } else { &stderr };
853 if let Some(start) = buffer.find("EXFIL:::").map(|i| i + "EXFIL:::".len()) {
854 let start = std::str::from_utf8(&buffer.as_bytes()[start..]).unwrap();
855 let end = start
856 .find(":::EXFIL")
857 .expect("Did not find terminating :::EXFIL sequence!");
858 result = Some(
859 std::str::from_utf8(&start.as_bytes()[..end])
860 .unwrap()
861 .to_string(),
862 );
863 }
864 };
865 self.build(ident, BuildMode::ObjectFile, &snippet, Self::NONE, callback)
866 .map_err(|err| {
867 format!(
868 "Test compilation failure. Is ident `{}` valid?\n{}",
869 bare_name, err
870 )
871 })?;
872 Ok(result)
873 }
874
875 /// Retrieve the definition of a C preprocessor macro or define, recursively in case it is
876 /// defined in terms of another `#define`.
877 ///
878 /// For "function macros" like `max(x, y)`, make sure to pass in placeholders for the parameters
879 /// (they will be returned as-is in the expanded output).
880 pub fn get_macro_value_recursive(
881 &self,
882 ident: &str,
883 headers: &[&str],
884 ) -> Result<Option<String>, BoxedError> {
885 let mut result = self.get_macro_value(ident, headers)?;
886 while result.is_some() {
887 // We shouldn't bubble up recursive errors because a macro can expand to a value that
888 // isn't a valid macro name (such as an expression wrapped in parentheses).
889 match self.get_macro_value(result.as_ref().unwrap(), headers) {
890 Ok(Some(r)) => result = Some(r),
891 _ => break,
892 };
893 }
894 Ok(result)
895 }
896}
897
898impl From<cc::Build> for Target {
899 fn from(build: cc::Build) -> Self {
900 Self::new_from(build).unwrap()
901 }
902}
903
904/// Sanitizes a string for use in a file name
905fn fs_sanitize(s: &str) -> Cow<'_, str> {
906 if s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
907 return Cow::Borrowed(s);
908 }
909
910 let mut out = String::with_capacity(s.len());
911 for c in s.chars() {
912 if !c.is_ascii_alphanumeric() {
913 out.push('_');
914 } else {
915 out.push(c);
916 }
917 }
918 Cow::Owned(out)
919}
920
921/// Convert header filename `header` to a `#include <..>` statement.
922fn to_include(header: &str) -> String {
923 format!("#include <{}>", header)
924}
925
926/// Convert one or more header filenames `headers` to `#include <..>` statements.
927fn to_includes(headers: &[&str]) -> String {
928 let mut vec = Vec::with_capacity(headers.len());
929 vec.extend(headers.iter().copied().map(to_include));
930 vec.join("\n")
931}
932
933/// Returns the `(Major, Minor, Patch)` version of the in-use `rustc` compiler.
934///
935/// Returns `None` in case of unexpected output format and panics in the event of runtime invariants
936/// being violated (i.e. non-executable RUSTC_WRAPPER, non-UTF-8 output, etc).
937fn rustc_version() -> Option<(u8, u8, u8)> {
938 use std::env;
939 use std::sync::OnceLock;
940
941 static RUSTC_VERSION: OnceLock<Option<(u8, u8, u8)>> = OnceLock::new();
942
943 RUSTC_VERSION
944 .get_or_init(|| -> Option<(u8, u8, u8)> {
945 let rustc = env::var_os("RUSTC").unwrap_or_else(|| OsString::from("rustc"));
946 let mut cmd = match env::var_os("RUSTC_WRAPPER").filter(|w| !w.is_empty()) {
947 Some(wrapper) => {
948 let mut cmd = Command::new(wrapper);
949 cmd.arg(rustc);
950 cmd
951 }
952 None => Command::new(rustc),
953 };
954 let cmd = cmd.arg("--version");
955
956 let output = cmd.output().expect("Failed to execute rustc!");
957 let mut parts = std::str::from_utf8(&output.stdout)
958 .expect("Failed to parse `rustc --version` to UTF-8!")
959 .strip_prefix("rustc ")
960 // 1.80.0 or 1.80.0-nightly
961 .and_then(|output| output.split(|c| c == ' ' || c == '-').next())?
962 .split('.')
963 .map_while(|v| u8::from_str_radix(v, 10).ok());
964
965 Some((parts.next()?, parts.next()?, parts.next()?))
966 })
967 .clone()
968}
969
970#[test]
971fn rustc_version_test() {
972 assert!(matches!(rustc_version(), Some((_major, _minor, _patch))));
973}