Repository: dtolnay/anyhow (rust)

Issue:
Nightly time bomb
Right now, the crate is checking in build.rs if nightly is present, and if it is, enables the backtrace cfg *without asking the user*, enabling the backtrace nightly feature: https://github.com/dtolnay/anyhow/blob/master/src/lib.rs#L174

Unstable Rust features can and often do change before stabilization though. This means that this crate is a time bomb for nightly users in the event of changes of the backtrace feature. Instead, this crate should make its backtrace support entirely opt-in for the user.

Pull request:
Replace version check with probing Backtrace api
Fixes #23.

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 7e761f0..12e7b82 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,4 +14,5 @@ default = ["std"]
 std = []
 
 [dev-dependencies]
+rustversion = "1.0"
 thiserror = "1.0"
diff --git a/build.rs b/build.rs
index 7f59c64..862fb7a 100644
--- a/build.rs
+++ b/build.rs
@@ -1,35 +1,59 @@
 use std::env;
-use std::process::Command;
-use std::str::{self, FromStr};
+use std::fs;
+use std::path::Path;
+use std::process::{Command, ExitStatus};
 
-fn main() {
-    let compiler = match rustc_version() {
-        Some(compiler) => compiler,
-        None => return,
-    };
+// This code exercises the surface area that we expect of the std Backtrace
+// type. If the current toolchain is able to compile it, we go ahead and use
+// backtrace in anyhow.
+const PROBE: &str = r#"
+    #![feature(backtrace)]
+    #![allow(dead_code)]
 
-    if compiler.minor >= 40 && compiler.nightly {
-        println!("cargo:rustc-cfg=backtrace");
-    }
-}
+    use std::backtrace::{Backtrace, BacktraceStatus};
+    use std::error::Error;
+    use std::fmt::{self, Display};
 
-struct Compiler {
-    minor: u32,
-    nightly: bool,
-}
+    #[derive(Debug)]
+    struct E;
 
-fn rustc_version() -> Option<Compiler> {
-    let rustc = env::var_os("RUSTC")?;
-    let output = Command::new(rustc).arg("--version").output().ok()?;
-    let version = str::from_utf8(&output.stdout).ok()?;
+    impl Display for E {
+        fn fmt(&self, _formatter: &mut fmt::Formatter) -> fmt::Result {
+            unimplemented!()
+        }
+    }
 
-    let mut pieces = version.split('.');
-    if pieces.next() != Some("rustc 1") {
-        return None;
+    impl Error for E {
+        fn backtrace(&self) -> Option<&Backtrace> {
+            let backtrace = Backtrace::capture();
+            match backtrace.status() {
+                BacktraceStatus::Captured | BacktraceStatus::Disabled | _ => {}
+            }
+            unimplemented!()
+        }
     }
+"#;
 
-    let next = pieces.next()?;
-    let minor = u32::from_str(next).ok()?;
-    let nightly = version.contains("nightly") || version.contains("dev");
-    Some(Compiler { minor, nightly })
+fn main() {
+    match compile_probe() {
+        Some(status) if status.success() => println!("cargo:rustc-cfg=backtrace"),
+        _ => {}
+    }
+}
+
+fn compile_probe() -> Option<ExitStatus> {
+    let rustc = env::var_os("RUSTC")?;
+    let out_dir = env::var_os("OUT_DIR")?;
+    let probefile = Path::new(&out_dir).join("lib.rs");
+    fs::write(&probefile, PROBE).ok()?;
+    Command::new(rustc)
+        .arg("--edition=2018")
+        .arg("--crate-name=anyhow_build")
+        .arg("--crate-type=lib")
+        .arg("--emit=metadata")
+        .arg("--out-dir")
+        .arg(out_dir)
+        .arg(probefile)
+        .status()
+        .ok()
 }


Test patch:
diff --git a/tests/test_backtrace.rs b/tests/test_backtrace.rs
new file mode 100644
index 0000000..ce385f5
--- /dev/null
+++ b/tests/test_backtrace.rs
@@ -0,0 +1,13 @@
+#[rustversion::not(nightly)]
+#[ignore]
+#[test]
+fn test_backtrace() {}
+
+#[rustversion::nightly]
+#[test]
+fn test_backtrace() {
+    use anyhow::anyhow;
+
+    let error = anyhow!("oh no!");
+    let _ = error.backtrace();
+}