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}