Refactor Rust version checking

Conditional compilation depending on Rust version using `cfg` had the
disadvantage that we had to write the same code multiple times, compile
it multiple times, execute it multiple times, update it multiple
times... Apart from obvious maintenance issues the build script wasn't
generating the list of allowed `cfg`s so those had to be maintained
manually in `Cargo.toml`. This was fixable by printing an appropriate
line but it's best to do it together with the other changes.

Because we cannot export `cfg` flags from a crate to different crates we
take a completely different approach: we define a macro called
`rust_version` that takes a very naturally looking condition such as
`if >= 1.70 {}`. This macro is auto-generated so that it produces
different results based on the compiler version - it either expands to
first block or the second block (after `else`).

This way, the other crates can simply call the macro when needed.

Unfortunately some minimal maintenance is still needed: to update the
max version number when a newer version is used. (Note that code will
still work with higher versions, it only limits which conditions can be
used in downstream code.) This can be automated with the pin update
script or we could just put the pin file into the `internals` directory
and read the value from there. Not automating isn't terrible either
since anyone adding a cfg with higher version will see a nice error
about unknown version of Rust and can update it manually.

Because this changes syntax to a more naturally looking version number,
as a side effect the `cond_const` macro could be also updated to use the
new macro under the hood, providing much nicer experience - it is no
longer needed to provide human-readable version of the version string to
put in the note about `const`ness requiring a newer version. As such the
note is now always there using a single source of truth.

It's also a great moment to introduce this change right now since
there's currently no conditional compilation used in `bitcoin` crate
making the changes minimal.
This commit is contained in:
Martin Habovstiak 2024-08-18 13:36:10 +02:00
parent 7d5ce89dad
commit ad34a98c61
11 changed files with 153 additions and 170 deletions

View File

@ -95,6 +95,9 @@ dependencies = [
[[package]] [[package]]
name = "bitcoin-io" name = "bitcoin-io"
version = "0.1.2" version = "0.1.2"
dependencies = [
"bitcoin-internals",
]
[[package]] [[package]]
name = "bitcoin-primitives" name = "bitcoin-primitives"

View File

@ -94,6 +94,9 @@ dependencies = [
[[package]] [[package]]
name = "bitcoin-io" name = "bitcoin-io"
version = "0.1.2" version = "0.1.2"
dependencies = [
"bitcoin-internals",
]
[[package]] [[package]]
name = "bitcoin-primitives" name = "bitcoin-primitives"

View File

@ -1,37 +0,0 @@
fn main() {
let rustc = std::env::var_os("RUSTC");
let rustc = rustc.as_ref().map(std::path::Path::new).unwrap_or_else(|| "rustc".as_ref());
let output = std::process::Command::new(rustc)
.arg("--version")
.output()
.unwrap_or_else(|error| panic!("failed to run `{:?} --version`: {:?}", rustc, error));
assert!(output.status.success(), "{:?} -- version returned non-zero exit code", rustc);
let stdout = String::from_utf8(output.stdout).expect("rustc produced non-UTF-8 output");
let version_prefix = "rustc ";
if !stdout.starts_with(version_prefix) {
panic!("unexpected rustc output: {}", stdout);
}
let version = &stdout[version_prefix.len()..];
let end = version.find(&[' ', '-'] as &[_]).unwrap_or(version.len());
let version = &version[..end];
let mut version_components = version.split('.');
let major = version_components.next().unwrap();
assert_eq!(major, "1", "unexpected Rust major version");
let minor = version_components
.next()
.unwrap_or("0")
.parse::<u64>()
.expect("invalid Rust minor version");
let msrv = std::env::var("CARGO_PKG_RUST_VERSION").unwrap();
let mut msrv = msrv.split(".");
let msrv_major = msrv.next().unwrap();
assert_eq!(msrv_major, "1", "unexpected Rust major version");
let msrv_minor = msrv.next().unwrap().parse::<u64>().unwrap();
// print cfg for all interesting versions less than or equal to minor
for version in msrv_minor..=minor {
println!("cargo:rustc-cfg=rust_v_1_{}", version);
}
}

View File

@ -34,4 +34,4 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
[lints.rust] [lints.rust]
unexpected_cfgs = { level = "deny", check-cfg = ['cfg(rust_v_1_64)'] } unexpected_cfgs = { level = "deny" }

View File

@ -1,3 +1,7 @@
const MAX_USED_VERSION: u64 = 80;
use std::io;
fn main() { fn main() {
let rustc = std::env::var_os("RUSTC"); let rustc = std::env::var_os("RUSTC");
let rustc = rustc.as_ref().map(std::path::Path::new).unwrap_or_else(|| "rustc".as_ref()); let rustc = rustc.as_ref().map(std::path::Path::new).unwrap_or_else(|| "rustc".as_ref());
@ -30,8 +34,45 @@ fn main() {
assert_eq!(msrv_major, "1", "unexpected Rust major version"); assert_eq!(msrv_major, "1", "unexpected Rust major version");
let msrv_minor = msrv.next().unwrap().parse::<u64>().unwrap(); let msrv_minor = msrv.next().unwrap().parse::<u64>().unwrap();
// print cfg for all interesting versions less than or equal to minor let out_dir = std::env::var_os("OUT_DIR").expect("missing OUT_DIR env var");
let out_dir = std::path::PathBuf::from(out_dir);
let macro_file = std::fs::File::create(out_dir.join("rust_version.rs")).expect("failed to create rust_version.rs");
let macro_file = io::BufWriter::new(macro_file);
write_macro(macro_file, msrv_minor, minor).expect("failed to write to rust_version.rs");
}
fn write_macro(mut macro_file: impl io::Write, msrv_minor: u64, minor: u64) -> io::Result<()> {
writeln!(macro_file, "/// Expands code based on Rust version this is compiled under.")?;
writeln!(macro_file, "///")?;
writeln!(macro_file, "/// Example:")?;
writeln!(macro_file, "/// ```")?;
writeln!(macro_file, "/// bitcoin_internals::rust_version! {{")?;
writeln!(macro_file, "/// if >= 1.70 {{")?;
writeln!(macro_file, "/// println!(\"This is Rust 1.70+\");")?;
writeln!(macro_file, "/// }} else {{")?;
writeln!(macro_file, "/// println!(\"This is Rust < 1.70\");")?;
writeln!(macro_file, "/// }}")?;
writeln!(macro_file, "/// }}")?;
writeln!(macro_file, "/// ```")?;
writeln!(macro_file, "///")?;
writeln!(macro_file, "/// The `else` branch is optional.")?;
writeln!(macro_file, "/// Currently only the `>=` operator is supported.")?;
writeln!(macro_file, "#[macro_export]")?;
writeln!(macro_file, "macro_rules! rust_version {{")?;
for version in msrv_minor..=minor { for version in msrv_minor..=minor {
println!("cargo:rustc-cfg=rust_v_1_{}", version); writeln!(macro_file, " (if >= 1.{} {{ $($if_yes:tt)* }} $(else {{ $($if_no:tt)* }})?) => {{", version)?;
writeln!(macro_file, " $($if_yes)*")?;
writeln!(macro_file, " }};")?;
} }
for version in (minor + 1)..(MAX_USED_VERSION + 1) {
writeln!(macro_file, " (if >= 1.{} {{ $($if_yes:tt)* }} $(else {{ $($if_no:tt)* }})?) => {{", version)?;
writeln!(macro_file, " $($($if_no)*)?")?;
writeln!(macro_file, " }};")?;
}
writeln!(macro_file, " (if >= $unknown:tt $($rest:tt)*) => {{")?;
writeln!(macro_file, " compile_error!(concat!(\"unknown Rust version \", stringify!($unknown)));")?;
writeln!(macro_file, " }};")?;
writeln!(macro_file, "}}")?;
writeln!(macro_file, "pub use rust_version;")?;
macro_file.flush()
} }

View File

@ -46,7 +46,7 @@ mod safety_boundary {
// from_raw_parts is const-unstable until 1.64 // from_raw_parts is const-unstable until 1.64
cond_const! { cond_const! {
/// Returns a reference to the underlying data. /// Returns a reference to the underlying data.
pub const(in rust_v_1_64 = "1.64") fn as_slice(&self) -> &[T] { pub const(in 1.64) fn as_slice(&self) -> &[T] {
let ptr = &self.data as *const _ as *const T; let ptr = &self.data as *const _ as *const T;
unsafe { core::slice::from_raw_parts(ptr, self.len) } unsafe { core::slice::from_raw_parts(ptr, self.len) }
} }

View File

@ -56,36 +56,34 @@ pub use concat_bytes_to_arr;
#[macro_export] #[macro_export]
/// Enables const fn in specified Rust version /// Enables const fn in specified Rust version
macro_rules! cond_const { macro_rules! cond_const {
($($(#[$attr:meta])* $vis:vis const(in $ver:ident $(= $human_ver:literal)?) fn $name:ident$(<$($gen:tt)*>)?($($args:tt)*) $(-> $ret:ty)? $body:block)+ ) => { ($($(#[$attr:meta])* $vis:vis const(in $version:tt) fn $name:ident$(<$($gen:tt)*>)?($($args:tt)*) $(-> $ret:ty)? $body:block)+ ) => {
$( $(
#[cfg($ver)] $crate::rust_version::rust_version! {
if >= $version {
$(#[$attr])* $(#[$attr])*
$( #[doc = concat!("\nNote: the function is only `const` in Rust ", stringify!($version), ".")]
#[doc = "\nNote: the function is only `const` in Rust "]
#[doc = $human_ver]
#[doc = "."]
)?
$vis const fn $name$(<$($gen)*>)?($($args)*) $(-> $ret)? $body $vis const fn $name$(<$($gen)*>)?($($args)*) $(-> $ret)? $body
} else {
#[cfg(not($ver))]
$(#[$attr])* $(#[$attr])*
#[doc = concat!("\nNote: the function is `const` in Rust ", stringify!($version), ".")]
$vis fn $name$(<$($gen)*>)?($($args)*) $(-> $ret)? $body $vis fn $name$(<$($gen)*>)?($($args)*) $(-> $ret)? $body
}
}
)+ )+
}; };
($($(#[$attr:meta])* $vis:vis const(in $ver:ident $(= $human_ver:literal)?) unsafe fn $name:ident$(<$($gen:tt)*>)?($($args:tt)*) $(-> $ret:ty)? $body:block)+ ) => { ($($(#[$attr:meta])* $vis:vis const(in $version:tt) unsafe fn $name:ident$(<$($gen:tt)*>)?($($args:tt)*) $(-> $ret:ty)? $body:block)+ ) => {
$( $(
#[cfg($ver)] $crate::rust_version::rust_version! {
if >= $version {
$(#[$attr])* $(#[$attr])*
$( #[doc = concat!("\nNote: the function is only `const` in Rust ", stringify!($version), ".")]
#[doc = "\nNote: the function is only `const` in Rust "]
#[doc = $human_ver]
#[doc = " and newer."]
)?
$vis const unsafe fn $name$(<$($gen)*>)?($($args)*) $(-> $ret)? $body $vis const unsafe fn $name$(<$($gen)*>)?($($args)*) $(-> $ret)? $body
} else {
#[cfg(not($ver))]
$(#[$attr])* $(#[$attr])*
#[doc = concat!("\nNote: the function is `const` in Rust ", stringify!($version), ".")]
$vis unsafe fn $name$(<$($gen)*>)?($($args)*) $(-> $ret)? $body $vis unsafe fn $name$(<$($gen)*>)?($($args)*) $(-> $ret)? $body
}
}
)+ )+
}; };
} }

View File

@ -27,6 +27,13 @@ pub extern crate serde_json;
#[cfg(feature = "test-serde")] #[cfg(feature = "test-serde")]
pub extern crate bincode; pub extern crate bincode;
// The pub module is a workaround for strange error:
// "macro-expanded `macro_export` macros from the current crate cannot be referred to by absolute paths"
#[doc(hidden)]
pub mod rust_version {
include!(concat!(env!("OUT_DIR"), "/rust_version.rs"));
}
pub mod array_vec; pub mod array_vec;
pub mod const_tools; pub mod const_tools;
pub mod error; pub mod error;

View File

@ -18,9 +18,12 @@ default = ["std"]
std = ["alloc"] std = ["alloc"]
alloc = [] alloc = []
[dependencies]
internals = { package = "bitcoin-internals", version = "0.3.0" }
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
[lints.rust] [lints.rust]
unexpected_cfgs = { level = "deny", check-cfg = ['cfg(rust_v_1_72)', 'cfg(rust_v_1_73)', 'cfg(rust_v_1_75)', 'cfg(rust_v_1_78)'] } unexpected_cfgs = { level = "deny" }

View File

@ -1,37 +0,0 @@
fn main() {
let rustc = std::env::var_os("RUSTC");
let rustc = rustc.as_ref().map(std::path::Path::new).unwrap_or_else(|| "rustc".as_ref());
let output = std::process::Command::new(rustc)
.arg("--version")
.output()
.unwrap_or_else(|error| panic!("failed to run `{:?} --version`: {:?}", rustc, error));
assert!(output.status.success(), "{:?} -- version returned non-zero exit code", rustc);
let stdout = String::from_utf8(output.stdout).expect("rustc produced non-UTF-8 output");
let version_prefix = "rustc ";
if !stdout.starts_with(version_prefix) {
panic!("unexpected rustc output: {}", stdout);
}
let version = &stdout[version_prefix.len()..];
let end = version.find(&[' ', '-'] as &[_]).unwrap_or(version.len());
let version = &version[..end];
let mut version_components = version.split('.');
let major = version_components.next().unwrap();
assert_eq!(major, "1", "unexpected Rust major version");
let minor = version_components
.next()
.unwrap_or("0")
.parse::<u64>()
.expect("invalid Rust minor version");
let msrv = std::env::var("CARGO_PKG_RUST_VERSION").unwrap();
let mut msrv = msrv.split(".");
let msrv_major = msrv.next().unwrap();
assert_eq!(msrv_major, "1", "unexpected Rust major version");
let msrv_minor = msrv.next().unwrap().parse::<u64>().unwrap();
// print cfg for all interesting versions less than or equal to minor
for version in msrv_minor..=minor {
println!("cargo:rustc-cfg=rust_v_1_{}", version);
}
}

View File

@ -1,5 +1,6 @@
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
use alloc::boxed::Box; use alloc::boxed::Box;
use internals::rust_version;
/// A bridging wrapper providing the IO traits for types that already implement `std` IO traits. /// A bridging wrapper providing the IO traits for types that already implement `std` IO traits.
#[repr(transparent)] #[repr(transparent)]
@ -304,26 +305,42 @@ macro_rules! impl_our {
}; };
} }
#[cfg(rust_v_1_72)] rust_version! {
if >= 1.72 {
impl_our! { impl_our! {
impl<R: std::io::Read> Read for std::io::BufReader<R> where R: ?Sized impl<R: std::io::Read> Read for std::io::BufReader<R> where R: ?Sized
} }
#[cfg(not(rust_v_1_72))]
impl_our! {
impl<R: std::io::Read> Read for std::io::BufReader<R>
}
#[cfg(rust_v_1_72)]
impl_our! { impl_our! {
impl<R: std::io::Read> BufRead for std::io::BufReader<R> where R: ?Sized impl<R: std::io::Read> BufRead for std::io::BufReader<R> where R: ?Sized
} }
#[cfg(not(rust_v_1_72))] impl_our! {
impl<W: std::io::Write> Write for std::io::BufWriter<W> where W: ?Sized
}
impl_our! {
impl<W: std::io::Write> Write for std::io::LineWriter<W> where W: ?Sized
}
} else {
impl_our! {
impl<R: std::io::Read> Read for std::io::BufReader<R>
}
impl_our! { impl_our! {
impl<R: std::io::Read> BufRead for std::io::BufReader<R> impl<R: std::io::Read> BufRead for std::io::BufReader<R>
} }
impl_our! {
impl<W: std::io::Write> Write for std::io::BufWriter<W>
}
impl_our! {
impl<W: std::io::Write> Write for std::io::LineWriter<W>
}
}
}
impl std::io::Write for super::Sink { impl std::io::Write for super::Sink {
#[inline] #[inline]
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { Ok(buf.len()) } fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { Ok(buf.len()) }
@ -335,26 +352,6 @@ impl std::io::Write for super::Sink {
fn flush(&mut self) -> std::io::Result<()> { Ok(()) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
} }
#[cfg(rust_v_1_72)]
impl_our! {
impl<W: std::io::Write> Write for std::io::BufWriter<W> where W: ?Sized
}
#[cfg(not(rust_v_1_72))]
impl_our! {
impl<W: std::io::Write> Write for std::io::BufWriter<W>
}
#[cfg(rust_v_1_72)]
impl_our! {
impl<W: std::io::Write> Write for std::io::LineWriter<W> where W: ?Sized
}
#[cfg(not(rust_v_1_72))]
impl_our! {
impl<W: std::io::Write> Write for std::io::LineWriter<W>
}
impl_our! { impl_our! {
impl<R: std::io::Read> Read for std::io::Take<R> impl<R: std::io::Read> Read for std::io::Take<R>
} }
@ -399,17 +396,27 @@ impl_our! {
impl BufRead for std::io::Empty impl BufRead for std::io::Empty
} }
#[cfg(rust_v_1_73)] rust_version! {
if >= 1.73 {
impl_our! { impl_our! {
impl Write for std::io::Empty impl Write for std::io::Empty
} }
// No idea why &Empty impls Write but not Read + BufRead // No idea why &Empty impls Write but not Read + BufRead
#[cfg(rust_v_1_73)]
impl_our! { impl_our! {
impl Write for &'_ std::io::Empty impl Write for &'_ std::io::Empty
} }
impl_our! {
impl Read for std::sync::Arc<std::fs::File>
}
impl_our! {
impl Write for std::sync::Arc<std::fs::File>
}
}
}
impl_our! { impl_our! {
impl Read for std::io::Repeat impl Read for std::io::Repeat
} }
@ -418,10 +425,13 @@ impl_our! {
impl Read for std::io::Stdin impl Read for std::io::Stdin
} }
#[cfg(rust_v_1_78)] rust_version! {
if >= 1.78 {
impl_our! { impl_our! {
impl Read for &'_ std::io::Stdin impl Read for &'_ std::io::Stdin
} }
}
}
impl_our! { impl_our! {
impl Write for std::io::Stdout impl Write for std::io::Stdout
@ -463,16 +473,6 @@ impl_our! {
impl Write for &'_ std::fs::File impl Write for &'_ std::fs::File
} }
#[cfg(rust_v_1_73)]
impl_our! {
impl Read for std::sync::Arc<std::fs::File>
}
#[cfg(rust_v_1_73)]
impl_our! {
impl Write for std::sync::Arc<std::fs::File>
}
impl_our! { impl_our! {
impl Read for std::net::TcpStream impl Read for std::net::TcpStream
} }
@ -526,15 +526,17 @@ impl_our! {
impl Write for &'_ std::process::ChildStdin impl Write for &'_ std::process::ChildStdin
} }
#[cfg(rust_v_1_75)] rust_version! {
if >= 1.75 {
impl_our! { impl_our! {
impl Read for std::collections::VecDeque<u8> impl Read for std::collections::VecDeque<u8>
} }
#[cfg(rust_v_1_75)]
impl_our! { impl_our! {
impl BufRead for std::collections::VecDeque<u8> impl BufRead for std::collections::VecDeque<u8>
} }
}
}
impl_our! { impl_our! {
impl Write for std::collections::VecDeque<u8> impl Write for std::collections::VecDeque<u8>