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}