From 365696ea23e9e9cfe6b412bddc8e46d1c886813f Mon Sep 17 00:00:00 2001 From: David Tolnay Date: Tue, 5 Nov 2024 19:43:34 -0500 Subject: [PATCH] Allow disabling std dependency on 1.81+ --- .github/workflows/ci.yml | 3 ++- Cargo.toml | 18 ++++++++++++- build.rs | 9 ++++++- build/probe.rs | 1 + src/aserror.rs | 2 +- src/display.rs | 35 ++++++++++++++++++++++++ src/lib.rs | 12 ++++++--- src/provide.rs | 2 +- tests/crate/Cargo.toml | 16 +++++++++++ tests/crate/test.rs | 58 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 tests/crate/Cargo.toml create mode 100644 tests/crate/test.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2b62000..22278f66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - rust: [nightly, beta, stable, 1.70.0] + rust: [nightly, beta, stable, 1.81.0, 1.70.0] timeout-minutes: 45 steps: - uses: actions/checkout@v4 @@ -39,6 +39,7 @@ jobs: run: echo RUSTFLAGS=${RUSTFLAGS}\ --cfg=thiserror_nightly_testing >> $GITHUB_ENV if: matrix.rust == 'nightly' - run: cargo test --all + - run: cargo test --manifest-path tests/crate/Cargo.toml --no-default-features - uses: actions/upload-artifact@v4 if: matrix.rust == 'nightly' && always() with: diff --git a/Cargo.toml b/Cargo.toml index 23f36094..d1d9cd84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,22 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/dtolnay/thiserror" rust-version = "1.61" +[features] +default = ["std"] + +# Std feature enables support for formatting std::path::{Path, PathBuf} +# conveniently in an error message. +# +# #[derive(Error, Debug)] +# #[error("failed to create configuration file {path}")] +# pub struct MyError { +# pub path: PathBuf, +# pub source: std::io::Error, +# } +# +# Without std, this would need to be written #[error("... {}", path.display())]. +std = [] + [dependencies] thiserror-impl = { version = "=1.0.68", path = "impl" } @@ -21,7 +37,7 @@ rustversion = "1.0.13" trybuild = { version = "1.0.81", features = ["diff"] } [workspace] -members = ["impl"] +members = ["impl", "tests/crate"] [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/build.rs b/build.rs index 8495ef33..5d795e40 100644 --- a/build.rs +++ b/build.rs @@ -57,7 +57,14 @@ fn main() { println!("cargo:rerun-if-env-changed=RUSTC_BOOTSTRAP"); } - let rustc = match rustc_minor_version() { + // core::error::Error stabilized in Rust 1.81 + // https://blog.rust-lang.org/2024/09/05/Rust-1.81.0.html#coreerrorerror + let rustc = rustc_minor_version(); + if cfg!(not(feature = "std")) && rustc.map_or(false, |rustc| rustc < 81) { + println!("cargo:rustc-cfg=feature=\"std\""); + } + + let rustc = match rustc { Some(rustc) => rustc, None => return, }; diff --git a/build/probe.rs b/build/probe.rs index 5fbadcbb..ee126d45 100644 --- a/build/probe.rs +++ b/build/probe.rs @@ -2,6 +2,7 @@ // member access API. If the current toolchain is able to compile it, then // thiserror is able to provide backtrace support. +#![no_std] #![feature(error_generic_member_access)] use core::error::{Error, Request}; diff --git a/src/aserror.rs b/src/aserror.rs index 1bced57a..d66463ad 100644 --- a/src/aserror.rs +++ b/src/aserror.rs @@ -1,5 +1,5 @@ +use core::error::Error; use core::panic::UnwindSafe; -use std::error::Error; #[doc(hidden)] pub trait AsDynError<'a>: Sealed { diff --git a/src/display.rs b/src/display.rs index 7a62ae20..7ccdd810 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,4 +1,5 @@ use core::fmt::Display; +#[cfg(feature = "std")] use std::path::{self, Path, PathBuf}; #[doc(hidden)] @@ -21,6 +22,7 @@ where } } +#[cfg(feature = "std")] impl<'a> AsDisplay<'a> for Path { type Target = path::Display<'a>; @@ -30,6 +32,7 @@ impl<'a> AsDisplay<'a> for Path { } } +#[cfg(feature = "std")] impl<'a> AsDisplay<'a> for PathBuf { type Target = path::Display<'a>; @@ -42,5 +45,37 @@ impl<'a> AsDisplay<'a> for PathBuf { #[doc(hidden)] pub trait Sealed {} impl Sealed for &T {} +#[cfg(feature = "std")] impl Sealed for Path {} +#[cfg(feature = "std")] impl Sealed for PathBuf {} + +// Add a synthetic second impl of AsDisplay to prevent the "single applicable +// impl" rule from making too weird inference decision based on the single impl +// for &T, which could lead to code that compiles with thiserror's std feature +// off but breaks under feature unification when std is turned on by an +// unrelated crate. +#[cfg(not(feature = "std"))] +mod placeholder { + use super::{AsDisplay, Sealed}; + use core::fmt::{self, Display}; + + pub struct Placeholder; + + impl<'a> AsDisplay<'a> for Placeholder { + type Target = Self; + + #[inline] + fn as_display(&'a self) -> Self::Target { + Placeholder + } + } + + impl Display for Placeholder { + fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result { + unreachable!() + } + } + + impl Sealed for Placeholder {} +} diff --git a/src/lib.rs b/src/lib.rs index 17975e89..fec22cdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -258,6 +258,7 @@ //! //! [`anyhow`]: https://github.com/dtolnay/anyhow +#![no_std] #![doc(html_root_url = "https://docs.rs/thiserror/1.0.68")] #![allow( clippy::module_name_repetitions, @@ -270,6 +271,11 @@ #[cfg(all(thiserror_nightly_testing, not(error_generic_member_access)))] compile_error!("Build script probe failed to compile."); +#[cfg(feature = "std")] +extern crate std; +#[cfg(feature = "std")] +extern crate std as core; + mod aserror; mod display; #[cfg(error_generic_member_access)] @@ -287,9 +293,9 @@ pub mod __private { #[cfg(error_generic_member_access)] #[doc(hidden)] pub use crate::provide::ThiserrorProvide; - #[cfg(not(thiserror_no_backtrace_type))] #[doc(hidden)] - pub use std::backtrace::Backtrace; + pub use core::error::Error; + #[cfg(all(feature = "std", not(thiserror_no_backtrace_type)))] #[doc(hidden)] - pub use std::error::Error; + pub use std::backtrace::Backtrace; } diff --git a/src/provide.rs b/src/provide.rs index 7b4e9223..4b2f06a9 100644 --- a/src/provide.rs +++ b/src/provide.rs @@ -1,4 +1,4 @@ -use std::error::{Error, Request}; +use core::error::{Error, Request}; #[doc(hidden)] pub trait ThiserrorProvide: Sealed { diff --git a/tests/crate/Cargo.toml b/tests/crate/Cargo.toml new file mode 100644 index 00000000..14d36899 --- /dev/null +++ b/tests/crate/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "thiserror_test" +version = "0.0.0" +authors = ["David Tolnay "] +edition = "2021" +publish = false + +[lib] +path = "test.rs" + +[dependencies] +thiserror = { path = "../..", default-features = false } + +[features] +default = ["std"] +std = ["thiserror/std"] diff --git a/tests/crate/test.rs b/tests/crate/test.rs new file mode 100644 index 00000000..f6dd34ee --- /dev/null +++ b/tests/crate/test.rs @@ -0,0 +1,58 @@ +#![no_std] + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Error::E")] + E(#[from] SourceError), +} + +#[derive(Error, Debug)] +#[error("SourceError {field}")] +pub struct SourceError { + pub field: i32, +} + +#[cfg(test)] +mod tests { + use crate::{Error, SourceError}; + use core::error::Error as _; + use core::fmt::{self, Write}; + use core::mem; + + struct Buf<'a>(&'a mut [u8]); + + impl<'a> Write for Buf<'a> { + fn write_str(&mut self, s: &str) -> fmt::Result { + if s.len() <= self.0.len() { + let (out, rest) = mem::take(&mut self.0).split_at_mut(s.len()); + out.copy_from_slice(s.as_bytes()); + self.0 = rest; + Ok(()) + } else { + Err(fmt::Error) + } + } + } + + #[test] + fn test() { + let source = SourceError { field: -1 }; + let error = Error::from(source); + + let source = error + .source() + .unwrap() + .downcast_ref::() + .unwrap(); + + let mut msg = [b'~'; 17]; + write!(Buf(&mut msg), "{}", error).unwrap(); + assert_eq!(msg, *b"Error::E~~~~~~~~~"); + + let mut msg = [b'~'; 17]; + write!(Buf(&mut msg), "{}", source).unwrap(); + assert_eq!(msg, *b"SourceError -1~~~"); + } +}