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}