#![expect(clippy::test_attr_in_doctest)]
use std::{
fmt::Debug,
path::Path,
process::{Command, Output},
};
pub use autocfg::AutoCfg;
use derive_more::Display;
use crate::{BuildError, Result, get_var};
use probes::{has, make_probe, unstable};
#[allow(non_camel_case_types, reason = "shadowing feature naming")]
#[derive(Debug, Clone, PartialEq, Eq, Display)]
pub enum UnstableFeature {
adt_const_params,
assert_matches,
bool_to_result,
can_vector,
doc_notable_trait,
iterator_try_collect,
never_type,
proc_macro_diagnostic,
strip_circumfix,
try_trait_v2,
try_trait_v2_residual,
unsized_const_params,
write_all_vectored,
OtherFeature(String),
}
impl UnstableFeature {
fn from(feature: &str) -> Self {
match feature {
"adt_const_params" => Self::adt_const_params,
"assert_matches" => Self::assert_matches,
"bool_to_result" => Self::bool_to_result,
"can_vector" => Self::can_vector,
"doc_notable_trait" => Self::doc_notable_trait,
"iterator_try_collect" => Self::iterator_try_collect,
"never_type" => Self::never_type,
"proc_macro_diagnostic" => Self::proc_macro_diagnostic,
"strip_circumfix" => Self::strip_circumfix,
"try_trait_v2" => Self::try_trait_v2,
"try_trait_v2_residual" => Self::try_trait_v2_residual,
"unsized_const_params" => Self::unsized_const_params,
"write_all_vectored" => Self::write_all_vectored,
_ => Self::OtherFeature(feature.to_string()),
}
}
}
mod probes {
use super::{AutoCfg, UnstableFeature};
pub fn make_probe(feature: &UnstableFeature, allowed: bool, probe: &str) -> String {
let mut _probe = String::with_capacity(256);
if allowed {
_probe.push('\n');
_probe.push_str("#![allow(stable_features)]");
_probe.push('\n');
_probe.push_str("#![feature(");
_probe.push_str(&feature.to_string());
_probe.push_str(")]");
_probe.push('\n');
};
_probe.push_str("#![allow(unused)]");
_probe.push('\n');
_probe.push_str(probe);
_probe
}
pub fn has(ac: &AutoCfg, feature: &UnstableFeature, allowed: bool, probe: &str) -> bool {
let cfg = format!("has_{feature}");
autocfg::emit_possibility(&cfg);
let code = make_probe(feature, allowed, probe);
if ac.probe_raw(&code).is_ok() {
autocfg::emit(&cfg);
true
} else {
false
}
}
pub fn unstable(
ac: &AutoCfg,
feature: &UnstableFeature,
allowed: bool,
extra_lines: Option<&str>,
) -> bool {
let cfg = format!("unstable_{feature}");
autocfg::emit_possibility(&cfg);
let mut code = format!(
r#"
#![deny(stable_features)]
#![feature({feature})]
#![allow(unused)]
"#
);
if let Some(extra_lines) = extra_lines {
code.push('\n');
code.push_str(extra_lines);
code.push('\n');
}
if allowed && ac.probe_raw(&code).is_ok() {
autocfg::emit(&cfg);
true
} else {
false
}
}
pub mod adt_const_params {
pub const AVAILABLE: &str = r#"
use std::marker::ConstParamTy;
#[derive(ConstParamTy, PartialEq, Eq)]
struct Increment(i32);
struct Counter<const INC: Increment>(i32);
"#;
}
pub mod assert_matches {
pub const AVAILABLE: &str = r#"
use std::assert_matches;
"#;
pub const ROOT: &str = r#"
use std::assert_matches;
fn main() {
assert_matches!(Some(4), Some(_));
}
"#;
pub const MODULE: &str = r#"
#![allow(stable_features)]
#![feature(assert_matches)]
use std::assert_matches::assert_matches;
fn main() {
assert_matches!(Some(4), Some(_));
}
"#;
}
pub mod bool_to_result {
pub const AVAILABLE: &str = r#"
fn main() {
let _ = true.ok_or(());
}
"#;
}
pub mod can_vector {
pub const AVAILABLE: &str = r#"
use std::io::Read;
fn main() {
std::io::empty().is_read_vectored();
}
"#;
}
pub mod doc_notable_trait {
pub const AVAILABLE: &str = r#"
#[doc(notable_trait)]
trait Foo {}
"#;
}
pub mod iterator_try_collect {
pub const AVAILABLE: &str = r#"
fn try_collect() {
let _: Option<Vec<_>> = std::iter::Iterator::try_collect(&mut vec![Some(1)].into_iter());
}
"#;
}
pub mod never_type {
pub const AVAILABLE: &str = r#"
type Bang = !;
"#;
}
pub mod proc_macro_diagnostic {
pub const AVAILABLE: &str = r#"
extern crate proc_macro;
use proc_macro::Diagnostic;
"#;
}
pub mod strip_circumfix {
pub const AVAILABLE: &str = r#"
fn main() {
let s = "foo";
let _ = s.strip_circumfix("f", "o");
}
"#;
}
pub mod try_trait_v2 {
pub const AVAILABLE: &str = r#"
use std::ops::Try;
"#;
}
pub mod try_trait_v2_residual {
pub const AVAILABLE: &str = r#"
use std::ops::Residual;
"#;
}
pub mod unsized_const_params {
pub const AVAILABLE: &str = r#"
#![allow(clippy::duplicated_attributes)]
#![allow(stable_features)]
#![allow(incomplete_features)]
#![feature(adt_const_params)]
struct Foo<const N: &'static str>;
"#;
}
pub mod write_all_vectored {
pub const AVAILABLE: &str = r#"
use std::io::{empty, Write, IoSlice};
fn main() {
let buf: [u8;_] = [0];
let slice = IoSlice::new(&buf);
empty().write_all_vectored(&mut [slice]);
}
"#;
}
}
pub trait Nightly {
fn emit_unstable_feature(
&self,
feature: UnstableFeature,
allowed_features: &AllowedFeatures,
) -> bool;
fn emit_unstable_feature_bundle<F: IntoIterator<Item = UnstableFeature>>(
&self,
features: F,
allowed_features: &AllowedFeatures,
bundle_name: &str,
) -> bool;
}
impl Nightly for AutoCfg {
fn emit_unstable_feature(
&self,
feature: UnstableFeature,
allowed_features: &AllowedFeatures,
) -> bool {
dbg!(&feature);
let ac = self;
let allowed = allowed_features.includes(&feature);
match feature {
UnstableFeature::adt_const_params => {
unstable(ac, &feature, allowed, None);
has(ac, &feature, allowed, probes::adt_const_params::AVAILABLE)
}
UnstableFeature::assert_matches => {
unstable(self, &feature, allowed, None);
autocfg::emit_possibility("assert_matches_location, values(\"root\", \"module\")");
if self
.probe_raw(&make_probe(&feature, allowed, probes::assert_matches::ROOT))
.is_ok()
{
autocfg::emit("assert_matches_location=\"root\"");
} else if allowed && self.probe_raw(probes::assert_matches::MODULE).is_ok() {
autocfg::emit("assert_matches_location=\"module\"");
}
has(ac, &feature, allowed, probes::assert_matches::AVAILABLE)
}
UnstableFeature::bool_to_result => {
unstable(ac, &feature, allowed, None);
has(ac, &feature, allowed, probes::bool_to_result::AVAILABLE)
}
UnstableFeature::can_vector => {
unstable(ac, &feature, allowed, None);
has(ac, &feature, allowed, probes::can_vector::AVAILABLE)
}
UnstableFeature::doc_notable_trait => {
unstable(ac, &feature, allowed, None);
has(ac, &feature, allowed, probes::doc_notable_trait::AVAILABLE)
}
UnstableFeature::iterator_try_collect => {
unstable(self, &feature, allowed, None);
has(
ac,
&feature,
allowed,
probes::iterator_try_collect::AVAILABLE,
)
}
UnstableFeature::never_type => {
unstable(self, &feature, allowed, None);
has(ac, &feature, allowed, probes::never_type::AVAILABLE)
}
UnstableFeature::proc_macro_diagnostic => {
unstable(ac, &feature, allowed, Some("extern crate proc_macro;"));
has(
ac,
&feature,
allowed,
probes::proc_macro_diagnostic::AVAILABLE,
)
}
UnstableFeature::strip_circumfix => {
unstable(ac, &feature, allowed, None);
has(ac, &feature, allowed, probes::strip_circumfix::AVAILABLE)
}
UnstableFeature::try_trait_v2 => {
unstable(self, &feature, allowed, None);
has(ac, &feature, allowed, probes::try_trait_v2::AVAILABLE)
}
UnstableFeature::try_trait_v2_residual => {
unstable(self, &feature, allowed, None);
has(
ac,
&feature,
allowed,
probes::try_trait_v2_residual::AVAILABLE,
)
}
UnstableFeature::unsized_const_params => {
let extra_lines = if unstable(
ac,
&UnstableFeature::adt_const_params,
allowed_features.includes(&UnstableFeature::adt_const_params),
None,
) {
Some("#![feature(adt_const_params)]\n#![allow(incomplete_features)]")
} else {
None
};
unstable(ac, &feature, allowed, extra_lines);
has(
ac,
&feature,
allowed,
probes::unsized_const_params::AVAILABLE,
)
}
UnstableFeature::write_all_vectored => {
unstable(ac, &feature, allowed, None);
has(ac, &feature, allowed, probes::write_all_vectored::AVAILABLE)
}
UnstableFeature::OtherFeature(_) => {
unstable(self, &feature, allowed, None);
false
}
}
}
fn emit_unstable_feature_bundle<F: IntoIterator<Item = UnstableFeature>>(
&self,
features: F,
allowed_features: &AllowedFeatures,
bundle_name: &str,
) -> bool {
let cfg = format!("has_{bundle_name}");
autocfg::emit_possibility(&cfg);
let mut has = true;
for feature in features.into_iter() {
has = self.emit_unstable_feature(feature, allowed_features) && has;
}
if has {
autocfg::emit(&cfg);
}
has
}
}
pub fn cargo_unstable() -> Result<bool> {
let mut cmd = Command::new(get_var("CARGO")?);
cmd.args([
"-Zunstable-options",
"--config",
"unstable.allow-features=[\"unstable-options\"]",
"help",
]);
let output = cmd
.output()
.map_err(|err| BuildError::Other(err.to_string()))?;
Ok(output.status.success())
}
fn cargo_config<P: AsRef<Path>>(
current_dir: &Option<P>,
added_unstable_options: bool,
) -> Result<Output> {
let mut cargo_config_get = Command::new(get_var("CARGO")?);
if let Some(dir) = ¤t_dir {
cargo_config_get.current_dir(dir);
}
cargo_config_get.arg("-Zunstable-options");
if added_unstable_options {
cargo_config_get.args(["--config", "unstable.allow-features=[\"unstable-options\"]"]);
}
cargo_config_get.args(["config", "get"]);
dbg!(&cargo_config_get);
cargo_config_get
.output()
.map_err(|err| BuildError::Other(err.to_string()))
}
pub fn cargo_allowed_features() -> Result<AllowedFeatures> {
println!("cargo::rerun-if-env-changed=BUILD_SAFELY_CARGO_CONFIG_DIR");
let cwd = std::env::var("BUILD_SAFELY_CARGO_CONFIG_DIR")
.or_else(|_| std::env::var("OUT_DIR"))
.ok();
_cargo_allowed_features(cwd)
}
fn _cargo_allowed_features<P: AsRef<Path> + Debug>(
current_dir: Option<P>,
) -> Result<AllowedFeatures> {
if !cargo_unstable()? {
dbg!("cargo won't accept `-Z` - so we're on a not-unstable toolchain");
let allowed_features = AllowedFeatures(_AllowedFeatures::None);
dbg!(&allowed_features);
return Ok(allowed_features);
}
let mut added_unstable_options = false;
let mut output = cargo_config(¤t_dir, added_unstable_options)?;
if !output.status.success() {
added_unstable_options = true;
output = cargo_config(¤t_dir, added_unstable_options)?;
if !output.status.success() {
return Err(BuildError::Other(format!(
"cargo config failed with error {code}: {stderr}",
code = output.status,
stderr = String::from_utf8_lossy(&output.stderr)
)));
}
};
let cargo_config = String::from_utf8_lossy(&output.stdout);
let allowed_features = match cargo_config
.lines()
.find(|line| line.starts_with("unstable.allow-features"))
{
None => AllowedFeatures(_AllowedFeatures::All),
Some(features) => {
let features: Vec<_> = features
.strip_prefix("unstable.allow-features = [")
.ok_or_else(|| {
BuildError::Other(format!(
"invalid cargo config output: {}",
String::from_utf8_lossy(&output.stdout)
))
})?
.strip_suffix("]")
.ok_or_else(|| {
BuildError::Other(format!(
"invalid cargo config output: {}",
String::from_utf8_lossy(&output.stdout)
))
})?
.replace("\"", "")
.split(",")
.map(str::trim)
.filter(|feature| !added_unstable_options || *feature != "unstable-options")
.map(UnstableFeature::from)
.collect();
if features.is_empty() {
AllowedFeatures(_AllowedFeatures::None)
} else {
AllowedFeatures(_AllowedFeatures::Some(features))
}
}
};
dbg!(&allowed_features);
Ok(allowed_features)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllowedFeatures(_AllowedFeatures);
impl AllowedFeatures {
fn includes(&self, feature: &UnstableFeature) -> bool {
match &self.0 {
_AllowedFeatures::None => false,
_AllowedFeatures::All => true,
_AllowedFeatures::Some(features) => features.iter().find(|f| *f == feature).is_some(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum _AllowedFeatures {
None,
All,
Some(Vec<UnstableFeature>),
}
#[cfg(test)]
mod tests {
use std::{
assert_matches,
fs::{self, File},
io::Write,
};
use tempfile::TempDir;
use super::UnstableFeature::*;
use super::*;
#[test]
fn no_config_toml() {
let tmp = TempDir::new().expect("tempdir");
let allowed = _cargo_allowed_features(Some(&tmp));
if cargo_unstable().expect("cargo_unstable") {
assert_matches!(allowed, Ok(AllowedFeatures(_AllowedFeatures::All)));
assert!(allowed.unwrap().includes(&try_trait_v2));
} else {
assert_matches!(allowed, Ok(AllowedFeatures(_AllowedFeatures::None)));
assert!(!allowed.unwrap().includes(&try_trait_v2));
}
}
#[test]
fn allowed_features() {
let tmp = TempDir::new().expect("tempdir");
let config_location = tmp.path().join(".cargo");
fs::create_dir(&config_location).expect(".cargo created");
dbg!(&config_location);
let mut config =
File::create_new(config_location.join("config.toml")).expect("create config.toml");
writeln!(
config,
"unstable.allow-features = [\"try_trait_v2\", \"unstable-options\"]"
)
.expect("added to config");
let allowed = _cargo_allowed_features(Some(&tmp)).unwrap();
if cargo_unstable().expect("cargo_unstable") {
assert_matches!(
allowed,
AllowedFeatures(_AllowedFeatures::Some(ref features))
if features == &vec![try_trait_v2, OtherFeature("unstable-options".to_string())]
);
assert!(allowed.includes(&try_trait_v2));
assert!(allowed.includes(&OtherFeature("unstable-options".to_string())));
} else {
assert_matches!(allowed, AllowedFeatures(_AllowedFeatures::None));
assert!(!allowed.includes(&try_trait_v2));
assert!(!allowed.includes(&OtherFeature("unstable-options".to_string())));
}
}
#[test]
fn allowed_features_no_unstable_options() {
let tmp = TempDir::new().expect("tempdir");
let config_location = tmp.path().join(".cargo");
fs::create_dir(&config_location).expect(".cargo created");
dbg!(&config_location);
let mut config =
File::create_new(config_location.join("config.toml")).expect("create config.toml");
writeln!(config, "unstable.allow-features = [\"try_trait_v2\"]").expect("added to config");
let allowed = _cargo_allowed_features(Some(&tmp)).unwrap();
if cargo_unstable().expect("cargo_unstable") {
assert_matches!(
allowed,
AllowedFeatures(_AllowedFeatures::Some(ref features))
if features == &vec![try_trait_v2]
);
assert!(allowed.includes(&try_trait_v2));
} else {
assert_matches!(allowed, AllowedFeatures(_AllowedFeatures::None));
assert!(!allowed.includes(&try_trait_v2));
}
}
#[test]
fn all_forbidden() {
let tmp = TempDir::new().expect("tempdir");
let config_location = tmp.path().join(".cargo");
fs::create_dir(&config_location).expect(".cargo created");
dbg!(&config_location);
let mut config =
File::create_new(config_location.join("config.toml")).expect("create config.toml");
writeln!(config, "unstable.allow-features = []").expect("added to config");
let allowed = _cargo_allowed_features(Some(&tmp));
assert_matches!(allowed, Ok(AllowedFeatures(_AllowedFeatures::None)));
}
#[test]
fn make_assert_matches_probe() {
let expected = r#"
#![allow(stable_features)]
#![feature(assert_matches)]
#![allow(unused)]
use std::assert_matches;
"#;
let probe = r#"
use std::assert_matches;
"#;
assert_eq!(probes::make_probe(&assert_matches, true, probe), expected);
}
#[test]
fn unstable_feature_display() {
assert_eq!(
"foo",
format!("{}", UnstableFeature::OtherFeature("foo".to_string()))
);
assert_eq!("try_trait_v2", format!("{}", UnstableFeature::try_trait_v2))
}
}