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.2"
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
50/// Re-exports `maybe_const_fn!` from the other crate.
51///
52/// Needs to be redeclaraton rather than a re-export in order
53/// to properly give the deprecation warning. See rust-lang/rust#30827
54/// This is fine since the docs are hidden
55/// and it will be removed soon anyways.
56#[cfg(feature = "compat-maybe-const-fn")]
57#[deprecated(note = "Should be in a separate crate")]
58#[doc(hidden)]
59#[macro_export]
60macro_rules! maybe_const_fn {
61    ($($x:tt)*) => {
62        $crate::__impl_maybe_const_fn::maybe_const_fn!($($x)*);
63    };
64}
65
66/// Private re-export of `maybe_const_fn` crate.
67///
68/// Needed because the `maybe_const_fn!` macro delegates to it.
69/// Delegation is used rather than a direct re-export in order to properly
70/// trigger the deprecation warning.
71#[doc(hidden)]
72#[cfg(feature = "compat-maybe-const-fn")]
73pub extern crate maybe_const_fn as __impl_maybe_const_fn;
74
75mod build;
76pub mod date;
77pub mod version;
78
79pub use crate::date::Date;
80pub use crate::version::{Channel, RustVersion, StableVersionSpec};
81
82/// Detect the current version by executing `rustc`.
83///
84/// This should only be called at build time (usually a build script),
85/// since the rust compiler is likely unavailable at runtime.
86/// It will execute whatever command is present in the `RUSTC` environment variable,
87/// so should not be run in an untrusted environment.
88///
89/// Once the version is successfully detected,
90/// it will be cached for future runs.
91///
92/// # Errors
93/// Returns an error if unable to execute the result compiler
94/// or unable to parse the result.
95pub fn detect_version() -> Result<crate::RustVersion, VersionDetectionError> {
96    {
97        let lock = state::state_mutex()
98            .read()
99            .unwrap_or_else(std::sync::PoisonError::into_inner);
100        if let Some(cached) = &*lock {
101            return Ok(*cached);
102        }
103        // release the lock & fallthrough to detection
104    }
105    match build::determine_version() {
106        Ok(success) => {
107            {
108                let mut lock = state::state_mutex()
109                    .write()
110                    .unwrap_or_else(std::sync::PoisonError::into_inner);
111                *lock = Some(success);
112            }
113            Ok(success)
114        }
115        Err(failure) => Err(failure),
116    }
117}
118
119/// Indicates failure to detect the compiler's rust version.
120#[derive(Debug)]
121pub struct VersionDetectionError {
122    desc: String,
123    cause: Option<std::io::Error>,
124}
125impl VersionDetectionError {
126    pub(crate) fn new(desc: String) -> Self {
127        VersionDetectionError { desc, cause: None }
128    }
129
130    pub(crate) fn with_cause(desc: String, cause: std::io::Error) -> Self {
131        VersionDetectionError {
132            desc,
133            cause: Some(cause),
134        }
135    }
136}
137impl Display for VersionDetectionError {
138    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
139        write!(f, "{}", self.desc)?;
140        if let Some(ref cause) = self.cause {
141            write!(f, ": {}", cause)?;
142        }
143        Ok(())
144    }
145}
146impl Error for VersionDetectionError {
147    fn source(&self) -> Option<&(dyn Error + 'static)> {
148        self.cause.as_ref().map(|x| x as _)
149    }
150}
151
152/// Caches the detected rust version.
153#[allow(unused_imports)]
154mod state {
155    use std::sync::{Once, RwLock};
156
157    #[allow(deprecated)] // Only available since 1.32
158    static CACHED_STATE_INIT: Once = std::sync::ONCE_INIT;
159    static mut CACHED_STATE: Option<RwLock<Option<crate::RustVersion>>> = None;
160
161    pub fn state_mutex() -> &'static RwLock<Option<crate::RustVersion>> {
162        CACHED_STATE_INIT.call_once(|| {
163            // SAFETY: Will only be called once
164            unsafe {
165                CACHED_STATE = Some(RwLock::new(None));
166            }
167        });
168        // SAFETY: After completion of `Once::call_once`,
169        // the mutex is fully initialized and not `None`
170        unsafe {
171            match CACHED_STATE {
172                Some(ref mutex) => mutex,
173                None => std::hint::unreachable_unchecked(),
174            }
175        }
176    }
177}