Skip to main content

rustversion_detect/
lib.rs

1//! Parses the output of `rustc --version` for use in build scripts.
2//!
3//! The parsed output is cached in a static variable,
4//! so `rustc` will only be invoked once no matter how many build scripts use this crate.
5//! This gives an advantage over using [`autocfg` crate][`autocfg`] or performing ad-hoc detection,
6//! as these require re-running `rustc` for each build scripts.
7//!
8//! This crate originated as a fork of [dtolnay's `rustversion` crate][`rustversion`],
9//! but with the proc-macro code removed and version detection moved from the build script to runtime.
10//! The core version detection logic has been kept up to date with upstream changes.
11//! It currently mirrors rustversion v1.0.23.
12//!
13//! Moving the version detection logic to runtime means that this crate
14//! does not need its own build script,
15//! reducing compile times compared to using the [`rustversion`] macros in a build script.
16//!
17//! [`rustversion`]: https://github.com/dtolnay/rustversion
18//! [`autocfg`]: https://github.com/cuviper/autocfg
19//!
20//! # Dependency
21//! Add the following to your build script:
22//! ```toml
23//! [build-dependencies]
24//! rustversion-detect = "0.3"
25//! ```
26//!
27//! # Examples
28//! ```
29//! pub fn main() {
30//!     // by default rust re-runs the build script if any source file changes
31//!     // this directive indicates that the build script only needs
32//!     // to be rerun if the compiler flags change (or if the build script itself changes)
33//!     println!("cargo:rerun-if-changed=build.rs");
34//!
35//!     // Rust 1.80 requires listing all possibilities using this directive
36//!     println!("cargo:rustc-check-cfg=cfg(use_nightly)");
37//!
38//!     let version = rustversion_detect::detect_version().unwrap();
39//!     if version.is_nightly() {
40//!         println!("cargo:rustc-cfg=use_nightly");
41//!     }
42//! }
43//! ```
44// These lints indicate serious problems which I would normally mark as #[deny(...)].
45// However, failing the build could cause problems for users of this library.
46#![warn(missing_docs)]
47use std::error::Error;
48use std::fmt::{self, Display};
49
50mod build;
51pub mod date;
52pub mod version;
53
54pub use crate::date::Date;
55pub use crate::version::{Channel, RustVersion, StableVersionSpec};
56
57/// Detect the current version by executing `rustc`.
58///
59/// This should only be called at build time (usually a build script),
60/// since the rust compiler is likely unavailable at runtime.
61/// It will execute whatever command is present in the `RUSTC` environment variable,
62/// so should not be run in an untrusted environment.
63///
64/// Once the version is successfully detected,
65/// it will be cached for future runs.
66///
67/// # Errors
68/// Returns an error if unable to execute the result compiler
69/// or unable to parse the result.
70pub fn detect_version() -> Result<crate::RustVersion, VersionDetectionError> {
71    {
72        let lock = state::state_mutex()
73            .read()
74            .unwrap_or_else(std::sync::PoisonError::into_inner);
75        if let Some(cached) = &*lock {
76            return Ok(*cached);
77        }
78        // release the lock & fallthrough to detection
79    }
80    match build::determine_version() {
81        Ok(success) => {
82            {
83                let mut lock = state::state_mutex()
84                    .write()
85                    .unwrap_or_else(std::sync::PoisonError::into_inner);
86                *lock = Some(success);
87            }
88            Ok(success)
89        }
90        Err(failure) => Err(failure),
91    }
92}
93
94/// Indicates failure to detect the compiler's rust version.
95#[derive(Debug)]
96pub struct VersionDetectionError {
97    desc: String,
98    cause: Option<std::io::Error>,
99}
100impl VersionDetectionError {
101    pub(crate) fn new(desc: String) -> Self {
102        VersionDetectionError { desc, cause: None }
103    }
104
105    pub(crate) fn with_cause(desc: String, cause: std::io::Error) -> Self {
106        VersionDetectionError {
107            desc,
108            cause: Some(cause),
109        }
110    }
111}
112impl Display for VersionDetectionError {
113    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114        write!(f, "{}", self.desc)?;
115        if let Some(ref cause) = self.cause {
116            write!(f, ": {}", cause)?;
117        }
118        Ok(())
119    }
120}
121impl Error for VersionDetectionError {
122    fn source(&self) -> Option<&(dyn Error + 'static)> {
123        self.cause.as_ref().map(|x| x as _)
124    }
125}
126
127/// Caches the detected rust version.
128#[allow(unused_imports)]
129mod state {
130    use std::sync::{Once, RwLock};
131
132    #[allow(deprecated)] // Only available since 1.32
133    static CACHED_STATE_INIT: Once = std::sync::ONCE_INIT;
134    static mut CACHED_STATE: Option<RwLock<Option<crate::RustVersion>>> = None;
135
136    pub fn state_mutex() -> &'static RwLock<Option<crate::RustVersion>> {
137        CACHED_STATE_INIT.call_once(|| {
138            // SAFETY: Will only be called once
139            unsafe {
140                CACHED_STATE = Some(RwLock::new(None));
141            }
142        });
143        // SAFETY: After completion of `Once::call_once`,
144        // the mutex is fully initialized and not `None`
145        unsafe {
146            match CACHED_STATE {
147                Some(ref mutex) => mutex,
148                None => std::hint::unreachable_unchecked(),
149            }
150        }
151    }
152}