diff --git a/CHANGELOG.md b/CHANGELOG.md index 4110939..f05e809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [0.24.11] - 2025-10-27 + +### Fixed +- Detect `std::error::Request` support at build time and gate the internal + `provide` shim accordingly so `cargo +msrv package --locked` succeeds on the + documented MSRV toolchain. + ## [0.24.10] - 2025-10-26 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 310dddb..6c9768c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1804,7 +1804,7 @@ dependencies = [ [[package]] name = "masterror" -version = "0.24.10" +version = "0.24.11" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 030778d..445a3ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "masterror" -version = "0.24.10" +version = "0.24.11" rust-version = "1.90" edition = "2024" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 14dc6b8..be3994e 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,9 @@ The build script keeps the full feature snippet below in sync with ~~~toml [dependencies] -masterror = { version = "0.24.10", default-features = false } +masterror = { version = "0.24.11", default-features = false } # or with features: -# masterror = { version = "0.24.10", features = [ +# masterror = { version = "0.24.11", features = [ # "std", "axum", "actix", "openapi", # "serde_json", "tracing", "metrics", "backtrace", # "sqlx", "sqlx-migrate", "reqwest", "redis", @@ -446,4 +446,3 @@ assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED"); --- MSRV: **1.90** · License: **MIT OR Apache-2.0** · No `unsafe` - diff --git a/build.rs b/build.rs index 92a32ca..ea7b1ae 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,8 @@ use std::{ - env, + env, fs, path::{Path, PathBuf}, - process + process, + process::{Command, Stdio} }; use crate::readme::{sync_readme, verify_readme_relaxed}; @@ -17,10 +18,12 @@ fn main() { } fn run() -> Result<(), Box> { - println!("cargo:rustc-check-cfg=cfg(error_generic_member_access)"); + println!("cargo:rustc-check-cfg=cfg(masterror_has_error_generic_member_access)"); + println!("cargo:rustc-check-cfg=cfg(masterror_requires_error_generic_feature)"); println!("cargo:rerun-if-changed=Cargo.toml"); println!("cargo:rerun-if-changed=README.template.md"); println!("cargo:rerun-if-changed=build/readme.rs"); + println!("cargo:rerun-if-env-changed=MASTERROR_DISABLE_ERROR_GENERIC_MEMBER_ACCESS"); let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); @@ -41,9 +44,21 @@ fn run() -> Result<(), Box> { // В нормальном git-рабочем дереве — синхронизируем (жёсткий режим). sync_readme(&manifest_dir)?; + + if let Some(support) = detect_error_generic_member_access()? { + if support.requires_feature_attr { + println!("cargo:rustc-cfg=masterror_requires_error_generic_feature"); + } + println!("cargo:rustc-cfg=masterror_has_error_generic_member_access"); + } + Ok(()) } +struct ErrorGenericSupport { + requires_feature_attr: bool +} + // Твоя прежняя эвристика: target/package/... => packaged fn is_packaged_manifest(manifest_dir: &Path) -> bool { let mut seen_target = false; @@ -84,3 +99,66 @@ fn allow_readme_drift() -> bool { fn has_env(name: &str) -> bool { env::var_os(name).map(|v| !v.is_empty()).unwrap_or(false) } + +fn detect_error_generic_member_access() +-> Result, Box> { + if has_env("MASTERROR_DISABLE_ERROR_GENERIC_MEMBER_ACCESS") { + return Ok(None); + } + + let out_dir = PathBuf::from(env::var("OUT_DIR")?); + fs::create_dir_all(&out_dir)?; + + let stable_check = out_dir.join("check_error_generic_stable.rs"); + fs::write(&stable_check, STABLE_SNIPPET)?; + if compile_probe(&stable_check, &out_dir)?.success() { + return Ok(Some(ErrorGenericSupport { + requires_feature_attr: false + })); + } + + let nightly_check = out_dir.join("check_error_generic_nightly.rs"); + fs::write(&nightly_check, NIGHTLY_SNIPPET)?; + if compile_probe(&nightly_check, &out_dir)?.success() { + return Ok(Some(ErrorGenericSupport { + requires_feature_attr: true + })); + } + + Ok(None) +} + +fn compile_probe( + source: &Path, + out_dir: &Path +) -> Result> { + let rustc = env::var("RUSTC")?; + let mut cmd = Command::new(rustc); + cmd.arg("--crate-type").arg("lib"); + cmd.arg("--emit").arg("metadata"); + cmd.arg(source); + cmd.arg("-o"); + cmd.arg(out_dir.join("check_error_generic.rmeta")); + cmd.stdout(Stdio::null()); + cmd.stderr(Stdio::null()); + Ok(cmd.status()?) +} + +const STABLE_SNIPPET: &str = r#"use std::error::{Error, Request}; + +pub fn probe(request: &mut Request<'_>, error: &(dyn Error + 'static)) { + let _ = request; + let _ = error; +} +"#; + +const NIGHTLY_SNIPPET: &str = r#"#![feature(error_generic_member_access)] + +use std::error::{Error, Request}; + +pub fn probe(request: &mut Request<'_>, error: &(dyn Error + 'static)) { + request.provide_ref::<&'static str>(&"marker"); + request.provide_value::(0); + let _ = error; +} +"#; diff --git a/masterror-derive/src/error_trait.rs b/masterror-derive/src/error_trait.rs index 0b5f7ad..9d84d40 100644 --- a/masterror-derive/src/error_trait.rs +++ b/masterror-derive/src/error_trait.rs @@ -208,7 +208,7 @@ fn struct_backtrace_method(fields: &Fields) -> Option { let member = &field.member; let body = field_backtrace_expr(quote!(self.#member), quote!(&self.#member), field); Some(quote! { - #[cfg(error_generic_member_access)] + #[cfg(masterror_has_error_generic_member_access)] fn backtrace(&self) -> Option<&std::backtrace::Backtrace> { #body } @@ -227,7 +227,7 @@ fn enum_backtrace_method(variants: &[VariantData]) -> Option { if has_backtrace { Some(quote! { - #[cfg(error_generic_member_access)] + #[cfg(masterror_has_error_generic_member_access)] fn backtrace(&self) -> Option<&std::backtrace::Backtrace> { match self { #(#arms),* @@ -369,7 +369,7 @@ fn struct_provide_method(fields: &Fields) -> Option { }; Some(quote! { - #[cfg(error_generic_member_access)] + #[cfg(masterror_has_error_generic_member_access)] fn provide<'a>(&'a self, #request: &mut core::error::Request<'a>) { #trait_import #(#statements)* @@ -413,7 +413,7 @@ fn enum_provide_method(variants: &[VariantData]) -> Option { }; Some(quote! { - #[cfg(error_generic_member_access)] + #[cfg(masterror_has_error_generic_member_access)] fn provide<'a>(&'a self, #request: &mut core::error::Request<'a>) { #trait_import #[allow(deprecated)] diff --git a/src/lib.rs b/src/lib.rs index b8a2730..178383d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,10 @@ clippy::all )] #![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr( + masterror_requires_error_generic_feature, + feature(error_generic_member_access) +)] //! Framework-agnostic application error types for backend services. //! @@ -343,7 +347,7 @@ mod convert; pub mod error; mod kind; mod macros; -#[cfg(error_generic_member_access)] +#[cfg(masterror_has_error_generic_member_access)] #[doc(hidden)] pub mod provide; mod response; diff --git a/tests/error_derive.rs b/tests/error_derive.rs index 1b9858f..206272a 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -1,6 +1,6 @@ #![allow(unused_variables, non_shorthand_field_patterns)] -#[cfg(error_generic_member_access)] +#[cfg(masterror_has_error_generic_member_access)] use std::ptr; use std::{error::Error as StdError, fmt}; @@ -150,14 +150,14 @@ enum EnumWithBacktrace { Unit } -#[cfg_attr(not(error_generic_member_access), allow(dead_code))] +#[cfg_attr(not(masterror_has_error_generic_member_access), allow(dead_code))] #[derive(Clone, Debug, PartialEq, Eq)] struct TelemetrySnapshot { name: &'static str, value: u64 } -#[cfg_attr(not(error_generic_member_access), allow(dead_code))] +#[cfg_attr(not(masterror_has_error_generic_member_access), allow(dead_code))] #[derive(Debug, Error)] #[error("structured telemetry {snapshot:?}")] struct StructuredTelemetryError { @@ -165,7 +165,7 @@ struct StructuredTelemetryError { snapshot: TelemetrySnapshot } -#[cfg_attr(not(error_generic_member_access), allow(dead_code))] +#[cfg_attr(not(masterror_has_error_generic_member_access), allow(dead_code))] #[derive(Debug, Error)] #[error("optional telemetry {telemetry:?}")] struct OptionalTelemetryError { @@ -173,7 +173,7 @@ struct OptionalTelemetryError { telemetry: Option } -#[cfg_attr(not(error_generic_member_access), allow(dead_code))] +#[cfg_attr(not(masterror_has_error_generic_member_access), allow(dead_code))] #[derive(Debug, Error)] #[error("optional owned telemetry {telemetry:?}")] struct OptionalOwnedTelemetryError { @@ -181,7 +181,7 @@ struct OptionalOwnedTelemetryError { telemetry: Option } -#[cfg_attr(not(error_generic_member_access), allow(dead_code))] +#[cfg_attr(not(masterror_has_error_generic_member_access), allow(dead_code))] #[derive(Debug, Error)] enum EnumTelemetryError { #[error("named {label}")] @@ -442,7 +442,7 @@ struct DisplayDynamicPrecisionError { precision: usize } -#[cfg(error_generic_member_access)] +#[cfg(masterror_has_error_generic_member_access)] fn assert_backtrace_interfaces(error: &E, expected: &std::backtrace::Backtrace) where E: StdError + ?Sized @@ -454,14 +454,14 @@ where assert!(ptr::eq(reported, provided)); } -#[cfg(not(error_generic_member_access))] +#[cfg(not(masterror_has_error_generic_member_access))] fn assert_backtrace_interfaces(_error: &E, _expected: &std::backtrace::Backtrace) where E: StdError + ?Sized { } -#[cfg(error_generic_member_access)] +#[cfg(masterror_has_error_generic_member_access)] #[test] fn struct_provides_custom_telemetry() { let telemetry = TelemetrySnapshot { @@ -481,7 +481,7 @@ fn struct_provides_custom_telemetry() { assert_eq!(provided_value, telemetry); } -#[cfg(error_generic_member_access)] +#[cfg(masterror_has_error_generic_member_access)] #[test] fn option_telemetry_only_provided_when_present() { let snapshot = TelemetrySnapshot { @@ -515,7 +515,7 @@ fn option_telemetry_only_provided_when_present() { assert!(std::error::request_value::(&owned_none).is_none()); } -#[cfg(error_generic_member_access)] +#[cfg(masterror_has_error_generic_member_access)] #[test] fn enum_variants_provide_custom_telemetry() { let named_snapshot = TelemetrySnapshot { @@ -796,7 +796,7 @@ fn optional_source_backtrace_absent_when_none() { source: None }; assert!(StdError::source(&err).is_none()); - #[cfg(error_generic_member_access)] + #[cfg(masterror_has_error_generic_member_access)] { assert!(std::error::Error::backtrace(&err).is_none()); assert!(std::error::request_ref::(&err).is_none()); @@ -822,7 +822,7 @@ fn enum_backtrace_field_is_returned() { } let unit = EnumWithBacktrace::Unit; - #[cfg(error_generic_member_access)] + #[cfg(masterror_has_error_generic_member_access)] { assert!(std::error::Error::backtrace(&unit).is_none()); } @@ -952,7 +952,7 @@ fn enum_backtrace_is_inferred_without_attribute() { } assert!(StdError::source(&tuple).is_none()); - #[cfg(error_generic_member_access)] + #[cfg(masterror_has_error_generic_member_access)] { let none = AutoBacktraceEnum::Tuple(None); assert!(std::error::Error::backtrace(&none).is_none());