Repository: dtolnay/thiserror
Pull request #373 (2024-11-06 02:47:44)

Allow disabling std dependency on 1.81+


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a2b6200..6f4f001 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
@@ -38,7 +38,9 @@ jobs:
       - name: Enable nightly-only tests
         run: echo RUSTFLAGS=${RUSTFLAGS}\ --cfg=thiserror_nightly_testing >> $GITHUB_ENV
         if: matrix.rust == 'nightly'
-      - run: cargo test --all
+      - run: cargo test --workspace --exclude thiserror_no_std_test
+      - run: cargo test --manifest-path tests/no-std/Cargo.toml
+        if: matrix.rust != '1.70.0'
       - uses: actions/upload-artifact@v4
         if: matrix.rust == 'nightly' && always()
         with:
diff --git a/Cargo.toml b/Cargo.toml
index 23f3609..dd8f11c 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/no-std"]
 
 [package.metadata.docs.rs]
 targets = ["x86_64-unknown-linux-gnu"]
diff --git a/build.rs b/build.rs
index 8495ef3..5d795e4 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 5fbadcb..ee126d4 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 1bced57..d66463a 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 7a62ae2..7ccdd81 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<T: Display> 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 17975e8..fec22cd 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 7b4e922..4b2f06a 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 {


Test patch:
diff --git a/tests/no-std/Cargo.toml b/tests/no-std/Cargo.toml
new file mode 100644
index 0000000..8b03d2f
--- /dev/null
+++ b/tests/no-std/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "thiserror_no_std_test"
+version = "0.0.0"
+authors = ["David Tolnay <dtolnay@gmail.com>"]
+edition = "2021"
+publish = false
+
+[lib]
+path = "test.rs"
+
+[dependencies]
+thiserror = { path = "../..", default-features = false }
diff --git a/tests/no-std/test.rs b/tests/no-std/test.rs
new file mode 100644
index 0000000..da7899c
--- /dev/null
+++ b/tests/no-std/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 Write for Buf<'_> {
+        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::<SourceError>()
+            .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~~~");
+    }
+}