From 5c1158b4b66f0d3df140041d589d7aed6479c5bc Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Thu, 27 Aug 2026 04:56:46 +0000 Subject: [PATCH 1/4] rm ninja-build_rs --- ninja-build_rs/Cargo.toml | 22 - ninja-build_rs/LICENSE | 21 - ninja-build_rs/README.md | 65 -- ninja-build_rs/about.toml | 3 - .../examples/assert_matches/Cargo.toml | 12 - .../examples/assert_matches/build.rs | 9 - .../examples/assert_matches/src/lib.rs | 25 - .../examples/assert_matches/test.sh | 8 - ninja-build_rs/rust-toolchain.toml | 3 - ninja-build_rs/src/lib.rs | 188 ----- ninja-build_rs/src/nightly.rs | 690 ------------------ 11 files changed, 1046 deletions(-) delete mode 100644 ninja-build_rs/Cargo.toml delete mode 100644 ninja-build_rs/LICENSE delete mode 100644 ninja-build_rs/README.md delete mode 100644 ninja-build_rs/about.toml delete mode 100644 ninja-build_rs/examples/assert_matches/Cargo.toml delete mode 100644 ninja-build_rs/examples/assert_matches/build.rs delete mode 100644 ninja-build_rs/examples/assert_matches/src/lib.rs delete mode 100755 ninja-build_rs/examples/assert_matches/test.sh delete mode 100644 ninja-build_rs/rust-toolchain.toml delete mode 100644 ninja-build_rs/src/lib.rs delete mode 100644 ninja-build_rs/src/nightly.rs diff --git a/ninja-build_rs/Cargo.toml b/ninja-build_rs/Cargo.toml deleted file mode 100644 index 55bbdef..0000000 --- a/ninja-build_rs/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "ninja-build_rs" -version = "0.4.1" -edition = "2024" -readme = "README.md" -description = "build script helpers for working with nightly" -authors = ["Mike Foster "] -keywords = ["build", "autocfg", "nightly"] -homepage = "https://github.com/MusicalNinjaDad/rust" -repository = "https://github.com/MusicalNinjaDad/rust" -categories = ["development-tools::build-utils"] -license = "MIT" -rust-version = "1.85.1" - -[dependencies] -autocfg = "1.5.1" -derive_more.version = "2.1.1" -derive_more.features = ["display"] -indexmap = "2.14.0" - -[dev-dependencies] -tempfile = "3.27.0" diff --git a/ninja-build_rs/LICENSE b/ninja-build_rs/LICENSE deleted file mode 100644 index 8082144..0000000 --- a/ninja-build_rs/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 MusicalNinjaDad - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/ninja-build_rs/README.md b/ninja-build_rs/README.md deleted file mode 100644 index 758cf38..0000000 --- a/ninja-build_rs/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# ninja-build_rs - -Designed to help create good build scripts, with a focus on ease of use for you, -valuable output in cargo build -vv & no annoying surprises for anyone downstream. - -## Usage - -```rust -use ninja_build_rs::prelude::*; -// Result uses BuildError to give meaningful messages -fn main() -> Result<()> { - // get an environment variable and re-run build script if it changes. - let my_var: String = get_var("MY_VAR")?; - // get values from an environment variable, separated by the - // OS path separator and re-run build script if it changes. - let my_vals: IndexSet = split_var("MY_VALUES")?; - if my_vals.contains("some_value") { - unimplemented!("do something") - } - // get a new AutoCfg or provide a valuable error - // rather than panicing. - let ac = AutoCfg::new()?; - // check to see if the downstream crate has defined - // `unstable.allow-features` in `.cargo/config.toml`. - // It is mandatory to perform this check and pass the - // result to any calls to `emit_unstable_feature` - let allowed_features = cargo_allowed_features()?; - // We want to make use of `assert_matches` if it is available - ac.emit_unstable_feature(assert_matches, &allowed_features); - // ^^^^^^^^^^^^^^ - enum variant to avoid typos - Ok(()) -} - -``` - -## Prelude - -```rust -use ninja_build_rs::prelude::*; -``` - -provides: - -- A [`Result`] alias & [`BuildError`] type that gives meaningful output from `main() -> Result<()>`. -- [`get_var()`] & [`split_var()`] which automatically register `cargo::rerun-if-env-changed` - and include the variable name in any errors. -- [`emit_unstable_feature()`](nightly::Nightly::emit_unstable_feature), - [`cargo_allowed_features`](nightly::cargo_allowed_features) & - enum [`UnstableFeature`](nightly::UnstableFeature) to provide a safe way to identify the - availability of nightly features & handle the future stabilisation process without additional - effort on your part. All while respecting any `allow-feature` whitelists. - -## Note to downstream crates - -If you (transiently) depend on a crate which uses `ninja-build_rs` and have implemented a -whitelist of `allowed-features`. - -Due to limitations in the information provided by cargo: - -- This will obtain config.toml files based upon `OUT_DIR`. If this is not under the project - root, you can override by providing an alternative path via the environment variable - `NINJA_CARGO_CONFIG_DIR`. See cargo's documentation on config file hierarchical structure - for more details. -- This will not respect additional entries passed at the command line via - `cargo --config unstable.allow-features=[...]` diff --git a/ninja-build_rs/about.toml b/ninja-build_rs/about.toml deleted file mode 100644 index af9d4c7..0000000 --- a/ninja-build_rs/about.toml +++ /dev/null @@ -1,3 +0,0 @@ -accepted = [ - "MIT" -] diff --git a/ninja-build_rs/examples/assert_matches/Cargo.toml b/ninja-build_rs/examples/assert_matches/Cargo.toml deleted file mode 100644 index 6e68afe..0000000 --- a/ninja-build_rs/examples/assert_matches/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "ninja-build_rs-assert_matches_fixture" -edition = "2024" -version = "0.0.0" -publish = false -rust-version = "1.85.1" - -[lib] - -[build-dependencies] -autocfg = "1.5.1" -ninja-build_rs.path = "../.." diff --git a/ninja-build_rs/examples/assert_matches/build.rs b/ninja-build_rs/examples/assert_matches/build.rs deleted file mode 100644 index 04b38b9..0000000 --- a/ninja-build_rs/examples/assert_matches/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -use autocfg::AutoCfg; -use ninja_build_rs::prelude::*; - -fn main() -> Result<()> { - let ac = AutoCfg::new()?; - let allowed_features = cargo_allowed_features()?; - ac.emit_unstable_feature(assert_matches, &allowed_features); - Ok(()) -} diff --git a/ninja-build_rs/examples/assert_matches/src/lib.rs b/ninja-build_rs/examples/assert_matches/src/lib.rs deleted file mode 100644 index c33ce2a..0000000 --- a/ninja-build_rs/examples/assert_matches/src/lib.rs +++ /dev/null @@ -1,25 +0,0 @@ -#![cfg_attr(unstable_assert_matches, feature(assert_matches))] - -#[cfg(test)] -#[cfg(has_assert_matches)] -mod tests { - #[cfg(assert_matches_location = "root")] - use std::assert_matches; - - #[cfg(assert_matches_location = "module")] - use std::assert_matches::assert_matches; - - #[test] - fn has() { - assert_matches!(Some(5), Some(n) if n == 5); - } -} - -#[cfg(test)] -#[cfg(not(has_assert_matches))] -mod tests { - #[test] - fn has_not() { - assert_eq!(Some(5), Some(5)); - } -} diff --git a/ninja-build_rs/examples/assert_matches/test.sh b/ninja-build_rs/examples/assert_matches/test.sh deleted file mode 100755 index e28ec2c..0000000 --- a/ninja-build_rs/examples/assert_matches/test.sh +++ /dev/null @@ -1,8 +0,0 @@ -#! /bin/bash - -set -euxo pipefail - -RUSTC_BOOTSTRAP=0 cargo +stable test -cargo +nightly test -cargo +nightly-2026-01-01 test -RUSTC_BOOTSTRAP=0 cargo +1.85.1 test diff --git a/ninja-build_rs/rust-toolchain.toml b/ninja-build_rs/rust-toolchain.toml deleted file mode 100644 index 02cb8fc..0000000 --- a/ninja-build_rs/rust-toolchain.toml +++ /dev/null @@ -1,3 +0,0 @@ -[toolchain] -channel = "stable" -profile = "default" diff --git a/ninja-build_rs/src/lib.rs b/ninja-build_rs/src/lib.rs deleted file mode 100644 index 85607c5..0000000 --- a/ninja-build_rs/src/lib.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! Designed to help create good build scripts, with a focus on ease of use for you, -//! valuable output in cargo build -vv & no annoying surprises for anyone downstream. -//! -//! # Usage -//! -//! ```rust, no_run -//! # use indexmap::IndexSet; -//! use ninja_build_rs::prelude::*; -//! -//! // Result uses BuildError to give meaningful messages -//! fn main() -> Result<()> { -//! -//! // get an environment variable and re-run build script if it changes. -//! let my_var: String = get_var("MY_VAR")?; -//! -//! // get values from an environment variable, separated by the -//! // OS path separator and re-run build script if it changes. -//! let my_vals: IndexSet = split_var("MY_VALUES")?; -//! if my_vals.contains("some_value") { -//! unimplemented!("do something") -//! } -//! -//! // get a new AutoCfg or provide a valuable error -//! // rather than panicing. -//! let ac = AutoCfg::new()?; -//! -//! // check to see if the downstream crate has defined -//! // `unstable.allow-features` in `.cargo/config.toml`. -//! // It is mandatory to perform this check and pass the -//! // result to any calls to `emit_unstable_feature` -//! let allowed_features = cargo_allowed_features()?; -//! -//! // We want to make use of `assert_matches` if it is available -//! ac.emit_unstable_feature(assert_matches, &allowed_features); -//! // ^^^^^^^^^^^^^^ - enum variant to avoid typos -//! -//! Ok(()) -//! } -//! ``` -//! -//! # Prelude -//! -//! ```rust -//! use ninja_build_rs::prelude::*; -//! ``` -//! -//! provides: -//! -//! - A [`Result`] alias & [`BuildError`] type that gives meaningful output from `main() -> Result<()>`. -//! - [`get_var()`] & [`split_var()`] which automatically register `cargo::rerun-if-env-changed` -//! and include the variable name in any errors. -//! - [`emit_unstable_feature()`](nightly::Nightly::emit_unstable_feature), -//! [`cargo_allowed_features`](nightly::cargo_allowed_features) & -//! enum [`UnstableFeature`](nightly::UnstableFeature) to provide a safe way to identify the -//! availability of nightly features & handle the future stabilisation process without additional -//! effort on your part. All while respecting any `allow-feature` whitelists. -//! - -use std::{env::VarError, ffi::OsString}; - -use indexmap::IndexSet; - -/// Recommended prelude: `use ninja-build_rs::prelude::*` -/// -/// - A [`Result`] alias & [`BuildError`] type that gives meaningful output from `main() -> Result<()>`. -/// - [`get_var()`] & [`split_var()`] which automatically register `cargo::rerun-if-env-changed` -/// and include the variable name in any errors. -/// - [`emit_unstable_feature()`](nightly::Nightly::emit_unstable_feature), -/// [`cargo_allowed_features`](nightly::cargo_allowed_features) & -/// enum [`UnstableFeature`](nightly::UnstableFeature) to provide a safe way to identify the -/// availability of nightly features & handle the future stabilisation process without additional -/// effort on your part. All while respecting any `allow-feature` whitelists. -pub mod prelude { - pub use crate::nightly::{AutoCfg, Nightly, UnstableFeature::*, cargo_allowed_features}; - pub use crate::{Result, get_var, split_var}; -} - -pub mod nightly; - -/// Attempt to get an environment variable, re-run build if it changes or provide a meaningful -/// error if missing. -/// -/// - Emits `cargo::rerun-if-env-changed=key` to ensure changes trigger a rebuild. -/// - If not found the error returned will include the key name in the debug representation. -pub fn get_var(key: &str) -> Result { - println!("cargo::rerun-if-env-changed={key}"); - std::env::var(key).map_err(|err| BuildError::from_var_error(key, err)) -} - -/// Attempt to get an environment variable and split the values using the OS path separator, -/// re-run build if it changes or provide a meaningful error if missing. -/// -/// - Emits `cargo::rerun-if-env-changed=key` to ensure changes trigger a rebuild. -/// - If not found the error returned will include the key name in the debug representation. -/// - Returns an [IndexSet] which implements `.contains()` AND retains ordering -pub fn split_var(key: &str) -> Result> { - Ok(std::env::split_paths(&get_var(key)?) - .map(|p| p.to_string_lossy().to_string()) - .collect()) -} - -/// Result type wrapping [BuildError]. Using `main() -> Result<()>` in `build.rs` will -/// provide useful information in the debug representation sent to stderr on failure. -pub type Result = std::result::Result; - -#[derive(Debug)] -/// An error designed to have nice debug representations for common errors encountered -/// in build.rs -pub enum BuildError { - /// If an environment variable was requested but not set - /// - /// outputs `VarNotSet("KEY")` to stderr - VarNotSet(OsString), - /// If an environment variable contains non-unicode characters - /// - /// outputs `VarInvalid("KEY", "contents")` to stderr - VarInvalid(OsString, OsString), - /// An IO Error occurred - /// - /// outputs `IOError(error details)` to stderr - IOError(std::io::Error), - /// An error when creating or using [autocfg] - /// - /// outputs `AutoCfgError(error details)` - AutoCfgError(autocfg::Error), - /// Catch-all for any other error - /// - /// outputs `Other(some text)` to stderr - Other(String), -} - -impl BuildError { - /// Create a `BuildError` from a `VarError` for a given key. - /// You probably won't need this often and can use [get_var] for most cases. - pub fn from_var_error(key: &str, err: VarError) -> BuildError { - match err { - VarError::NotPresent => BuildError::VarNotSet(key.into()), - VarError::NotUnicode(contents) => BuildError::VarInvalid(key.into(), contents), - } - } -} - -impl From for BuildError { - fn from(e: autocfg::Error) -> Self { - BuildError::AutoCfgError(e) - } -} - -impl From for BuildError { - fn from(e: std::io::Error) -> Self { - BuildError::IOError(e) - } -} - -/// Generate your own with `Err("some text")` -impl From<&str> for BuildError { - fn from(msg: &str) -> Self { - msg.to_string().into() - } -} - -/// Generate your own with `Err(String)` -impl From for BuildError { - fn from(msg: String) -> Self { - BuildError::Other(msg) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn err_from_str() { - let err = BuildError::from("oops"); - let msg = r#"Other("oops")"#; - assert!(format!("{err:?}").contains(msg)); - } - - #[test] - fn missing_env_var() { - let random_key = "019de8d0-bb66-769d-9d4d-fec48aebdd49"; - let err = get_var(random_key); - dbg!(&err); - assert!(err.is_err()); - assert!(format!("{err:?}").contains(random_key)); - } -} diff --git a/ninja-build_rs/src/nightly.rs b/ninja-build_rs/src/nightly.rs deleted file mode 100644 index 2bf92ee..0000000 --- a/ninja-build_rs/src/nightly.rs +++ /dev/null @@ -1,690 +0,0 @@ -#![expect(clippy::test_attr_in_doctest)] -//! Checking for experimental or stabilised features is prone to subtle errors which create issues -//! for downstream users and verbose when done properly. This provides extensions to the amazing -//! [autocfg::AutoCfg] (re-exported via our prelude to make your life easier) to safely identify the -//! availability of nightly features & handle the future stabilisation process without additional -//! effort on your part. All while respecting any `allow-feature` whitelists. -//! -//! For a list of known features with dedicated probes see [UnstableFeature] -//! -//! # Usage -//! -//! ## In `build.rs` -//! -//! ```rust, no_run -//! use ninja_build_rs::prelude::*; -//! -//! fn main() -> Result<()> { -//! // get a new AutoCfg or provide a valuable error -//! // rather than panicing -//! let ac = AutoCfg::new()?; -//! -//! // check to see if the downstream crate has defined -//! // `unstable.allow-features` in `.cargo/config.toml`. -//! // It is mandatory to perform this check and pass the -//! // result to any calls to `emit_unstable_feature` -//! let allowed_features = cargo_allowed_features()?; -//! -//! // We want to make use of `assert_matches` if it is available -//! ac.emit_unstable_feature(assert_matches, &allowed_features); -//! // ^^^^^^^^^^^^^^ - enum variant to avoid typos -//! -//! Ok(()) -//! } -//! ``` -//! -//! ## In `lib.rs` / `main.rs` -//! -//! ```rust -//! // only enable unstable feature if it is available and has not yet been stabilised -//! #![cfg_attr(unstable_assert_matches, feature(assert_matches))] -//! -//! #[cfg(test)] -//! // Do these tests if `assert_matches` is available -//! #[cfg(has_assert_matches)] -//! mod tests { -//! // `assert_matches` was moved in early 2026 before stabilisation -//! #[cfg(assert_matches_location = "root")] -//! use std::assert_matches; -//! -//! // in earlier nightly compilers `assert_matches` was in a separate module -//! #[cfg(assert_matches_location = "module")] -//! use std::assert_matches::assert_matches; -//! -//! #[test] -//! fn has() { -//! assert_matches!(Some(5), Some(n) if n == 5); -//! } -//! } -//! -//! #[cfg(test)] -//! // Do these tests if `assert_matches` is not available -//! #[cfg(not(has_assert_matches))] -//! mod tests { -//! #[test] -//! fn has_not() { -//! assert_eq!(Some(5), Some(5)); -//! } -//! } -//! ``` -//! -//! # Note to downstream crates -//! -//! If you (transiently) depend on a crate which uses `ninja-build_rs` and have implemented a -//! whitelist of `allowed-features`. -//! -//! Due to limitations in the information provided by cargo: -//! -//! - This will obtain config.toml files based upon `OUT_DIR`. If this is not under the project -//! root, you can override by providing an alternative path via the environment variable -//! `NINJA_CARGO_CONFIG_DIR`. See cargo's documentation on config file hierarchical structure -//! for more details. -//! - This will not respect additional entries passed at the command line via -//! `cargo --config unstable.allow-features=[...]` - -use std::{ - fmt::Debug, - path::Path, - process::{Command, Output}, -}; - -/// re-exported from autocfg -/// -pub use autocfg::AutoCfg; -use derive_more::Display; - -use crate::{BuildError, Result, get_var}; -use probes::{has, make_probe, unstable}; - -/// Known features with `unstable_...` & dedicated probes for `has_...`. -/// -/// If the feature you want is not in this list you can use `Other` to get `unstable_...` -/// but please also raise a PR (or open an issue) to add a custom probe for `has_...`. -#[allow(non_camel_case_types, reason = "shadowing feature naming")] -#[derive(Debug, Clone, PartialEq, Eq, Display)] -pub enum UnstableFeature { - /// ## Provides cfg flags: - /// - `#![cfg_attr(unstable_assert_matches, feature(assert_matches))]` - /// - `#[cfg(has_assert_matches)]` - /// - ```rust, ignore - /// #[cfg(assert_matches_location = "root")] - /// use std::assert_matches; - /// ``` - /// - ```rust, ignore - /// #[cfg(assert_matches_location = "module")] - /// use std::assert_matches::assert_matches; - /// ``` - assert_matches, - /// ## Provides cfg flags for feature [`can_vector`](https://github.com/rust-lang/rust/issues/69941) - /// - `#![cfg_attr(unstable_can_vector, feature(can_vector))]` - /// - `#[cfg(has_can_vector)]` - /// - this gates [`std::io::Read::is_read_vectored`] & [`std::io::Write::is_write_vectored`] - can_vector, - /// ## Provides cfg flags: - /// - `#![cfg_attr(unstable_iterator_try_collect, feature(iterator_try_collect))]` - /// - `#[cfg(has_iterator_try_collect)]` - iterator_try_collect, - /// ## Provides cfg flags: - /// - `#![cfg_attr(unstable_never_type, feature(never_type))]` - /// - `#[cfg(has_never_type)]` - never_type, - /// ## Provides cfg flags: - /// - `#![cfg_attr(unstable_proc_macro_diagnostic, feature(proc_macro_diagnostic))]` - /// - `#[cfg(has_proc_macro_diagnostic)]` - proc_macro_diagnostic, - /// ## Provides cfg flags: - /// - `#![cfg_attr(unstable_try_trait_v2, feature(try_trait_v2))]` - /// - `#[cfg(has_try_trait_v2)]` - try_trait_v2, - /// ## Provides cfg flags: - /// - `#![cfg_attr(unstable_try_trait_v2_residual, feature(try_trait_v2_residual))]` - /// - `#[cfg(has_try_trait_v2_residual)]` - try_trait_v2_residual, - /// ## Provides cfg flags for feature [`write_all_vectored`](https://github.com/rust-lang/rust/issues/70436) - /// - `#![cfg_attr(unstable_write_all_vectored, feature(write_all_vectored))]` - /// - `#[cfg(has_write_all_vectored)]` - /// - this gates [`std::io::Write::write_all_vectored`] - write_all_vectored, - /// only provides `unstable_...` - please raise a PR to add a custom probe for `has_...` - OtherFeature(String), -} - -impl UnstableFeature { - // This is not pub or trait From to avoid risk of typos - fn from(feature: &str) -> Self { - match feature { - "assert_matches" => Self::assert_matches, - "can_vector" => Self::can_vector, - "iterator_try_collect" => Self::iterator_try_collect, - "never_type" => Self::never_type, - "proc_macro_diagnostic" => Self::proc_macro_diagnostic, - "try_trait_v2" => Self::try_trait_v2, - "try_trait_v2_residual" => Self::try_trait_v2_residual, - "write_all_vectored" => Self::write_all_vectored, - _ => Self::OtherFeature(feature.to_string()), - } - } -} - -mod probes { - use super::{AutoCfg, UnstableFeature}; - - /// Prefix with: - /// - #![allow(stable_features)] (only if allowed) - /// - #![feature()] (only if allowed) - /// - #![allow(unused)] (always) - 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 - } - - /// Register `#[cfg(has_feature)]` & set based on the probe - pub fn has(ac: &AutoCfg, feature: &UnstableFeature, allowed: bool, probe: &str) { - 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); - } - } - - /// Register `#[cfg(has_feature)]` & run a default probe - pub fn unstable(ac: &AutoCfg, feature: &UnstableFeature, allowed: bool) { - let cfg = format!("unstable_{feature}"); - autocfg::emit_possibility(&cfg); - - if allowed { - let code = format!( - r#" -#![deny(stable_features)] -#![feature({feature})] -#![allow(unused)] -"# - ); - - if ac.probe_raw(&code).is_ok() { - autocfg::emit(&cfg); - } - } - } - - 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(_)); -} -"#; - // was stabilised in root - so no need to remove feature from this probe - 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 can_vector { - pub const AVAILABLE: &str = r#" -use std::io::Read; -fn main() { - std::io::empty().is_read_vectored(); -} -"#; - } - - pub mod iterator_try_collect { - // vec! not array: https://internals.rust-lang.org/t/code-compiles-on-playground-but-fails-when-passed-via-stdin-to-rustc/24393 - pub const AVAILABLE: &str = r#" -fn try_collect() { - let _: Option> = 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 { - /// Special probe as feature only available in proc_macro context - pub const UNSTABLE: &str = r#" -#![deny(stable_features)] -#![feature(proc_macro_diagnostic)] -#![allow(unused)] -extern crate proc_macro; -"#; - pub const AVAILABLE: &str = r#" -extern crate proc_macro; -use proc_macro::Diagnostic; -"#; - } - - 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 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]); -} -"#; - } -} - -/// Adds [`AutoCfg::emit_unstable_feature`](Nightly::emit_unstable_feature) -pub trait Nightly { - /// Offers at least 2 cfg flags for all [known features](UnstableFeature) - /// - /// # Feature enablement: `cfg(unstable_...)` - /// - /// - To be used at top-level crate via `#![cfg_attr(unstable_foo, feature(foo))]` - /// - /// # Cfg-gating: `cfg(has_...)` - /// - /// - **Do not rely on `#[cfg(not(unstable_foo))]` to suggest that `feature(foo)` is stable!** - /// - There are 3 reasons that `#[cfg(unstable_foo)]` could be `false`: - /// 1. The build is using `stable`/`beta` or the feature is not on the `allow-features` whitelist - /// 2. The feature has been stabilised - /// 3. The compiler is from before the feature was implemented - /// - All [known features](UnstableFeature) have a `#[cfg(has_...)]` for this purpose. - /// - /// # Note - /// - /// - You must pass a set of [AllowedFeatures], created by calling [cargo_allowed_features] - /// - If you need to test that a feature is available in order to cfg-gate your code and it is not - /// on the list of [known features](UnstableFeature), please raise a PR with a suggested probe. - fn emit_unstable_feature(&self, feature: UnstableFeature, allowed_features: &AllowedFeatures); -} - -impl Nightly for AutoCfg { - fn emit_unstable_feature(&self, feature: UnstableFeature, allowed_features: &AllowedFeatures) { - // show in `cargo build -vv` - dbg!(&feature); - - let ac = self; - let allowed = allowed_features.includes(&feature); - match feature { - UnstableFeature::assert_matches => { - unstable(self, &feature, allowed); - has(ac, &feature, allowed, probes::assert_matches::AVAILABLE); - 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() { - // ^^^^^^^ assert_matches was stabilised in root - autocfg::emit("assert_matches_location=\"module\""); - } - } - UnstableFeature::can_vector => { - unstable(ac, &feature, allowed); - has(ac, &feature, allowed, probes::can_vector::AVAILABLE); - } - UnstableFeature::iterator_try_collect => { - unstable(self, &feature, allowed); - has( - ac, - &feature, - allowed, - probes::iterator_try_collect::AVAILABLE, - ); - } - UnstableFeature::never_type => { - unstable(self, &feature, allowed); - has(ac, &feature, allowed, probes::never_type::AVAILABLE); - } - UnstableFeature::proc_macro_diagnostic => { - autocfg::emit_possibility("unstable_proc_macro_diagnostic"); - if allowed - && self - .probe_raw(probes::proc_macro_diagnostic::UNSTABLE) - .is_ok() - { - autocfg::emit("unstable_proc_macro_diagnostic"); - } - has( - ac, - &feature, - allowed, - probes::proc_macro_diagnostic::AVAILABLE, - ); - } - UnstableFeature::try_trait_v2 => { - unstable(self, &feature, allowed); - has(ac, &feature, allowed, probes::try_trait_v2::AVAILABLE); - } - UnstableFeature::try_trait_v2_residual => { - unstable(self, &feature, allowed); - has( - ac, - &feature, - allowed, - probes::try_trait_v2_residual::AVAILABLE, - ); - } - UnstableFeature::write_all_vectored => { - unstable(ac, &feature, allowed); - has(ac, &feature, allowed, probes::write_all_vectored::AVAILABLE); - } - UnstableFeature::OtherFeature(_) => unstable(self, &feature, allowed), - } - } -} - -/// Check whether cargo will accept unstable flags. You probably never need to run this -/// yourself and should prefer to simply call [`cargo_allowed_features`]. -pub fn cargo_unstable() -> Result { - Ok(Command::new(get_var("CARGO")?) - .args([ - "-Zunstable-options", - "--config", - "unstable.allow-features=[\"unstable-options\"]", - "help", - ]) - .output() - .map_err(|err| BuildError::Other(err.to_string()))? - .status - .success()) -} - -fn cargo_config>( - current_dir: &Option

, - added_unstable_options: bool, -) -> Result { - 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"]); - - // show in `cargo build -vv` - dbg!(&cargo_config_get); - - cargo_config_get - .output() - .map_err(|err| BuildError::Other(err.to_string())) -} - -/// Identify which experimental features are allowed for this build. -/// -/// This works fine on any channel and respects whitelists (`unstable.allowed-features`) in all -/// relevant cargo config.toml files. -/// -/// ## Note to downstream crates -/// -/// Due to limitations in the information provided by cargo: -/// -/// - This will obtain config.toml files based upon `OUT_DIR`. If this is not under the project -/// root, you can override by providing an alternative path via the environment variable -/// `NINJA_CARGO_CONFIG_DIR`. See cargo's documentation on config file hierarchical structure -/// for more details. -/// - This will not respect additional entries passed at the command line via -/// `cargo --config unstable.allow-features=[...]` -pub fn cargo_allowed_features() -> Result { - println!("cargo::rerun-if-env-changed=NINJA_CARGO_CONFIG_DIR"); - let cwd = std::env::var("NINJA_CARGO_CONFIG_DIR") - .or_else(|_| std::env::var("OUT_DIR")) - .ok(); - _cargo_allowed_features(cwd) -} - -fn _cargo_allowed_features>(current_dir: Option

) -> Result { - if !cargo_unstable()? { - // show in `cargo build -vv` - dbg!("cargo won't accept `-Z` - so we're on a not-unstable toolchain"); - - let allowed_features = AllowedFeatures(_AllowedFeatures::None); - - // show in `cargo build -vv` - 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() { - // Maybe there is a restricted list which doesn't include unstable-options - added_unstable_options = true; - output = cargo_config(¤t_dir, added_unstable_options)?; - - if !output.status.success() { - // Nope something else went wrong! - 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) => { - // default output format is toml - 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)) - } - } - }; - - // show in `cargo build -vv` - dbg!(&allowed_features); - - Ok(allowed_features) -} - -/// The set of allowed experimental features for the current build. The only way to create this -/// is via a call to [cargo_allowed_features] - this is deliberate, to ensure that people who have -/// decided to restrict the experimental features they use to a whitelist are respected. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AllowedFeatures(_AllowedFeatures); - -impl AllowedFeatures { - /// Not public as this doesn't consider any restrictions made via `RUSTFLAGS`, those - /// features will be disabled for all calls to rustc when running probes. - 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)] -/// private to make it impossible to manually construct an [AllowedFeatures] from outside this crate -enum _AllowedFeatures { - None, - All, - Some(Vec), -} - -#[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)) - } -} From 2e8033491db06a0cd4f1521871e0c16177f49fa3 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Thu, 27 Aug 2026 04:58:59 +0000 Subject: [PATCH 2/4] clean up CI: rust --- .github/workflows/rust.yml | 63 -------------------------------------- 1 file changed, 63 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 2b75d04..b4febb1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -24,54 +24,21 @@ jobs: with: concurrent_skipping: 'same_content_newer' - # indirection needed as fromJson will not read env context in matrix strategy - crates: - needs: skip_check - if: needs.skip_check.outputs.should_skip != 'true' - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.setup-matrix.outputs.matrix }} - steps: - - id: setup-matrix - uses: druzsan/setup-matrix@e52eed47b195a3c2157f91e1baceaa82df2276f2 #v2 - with: - matrix: | - crate: ["ninja-xtask", "ninja-build_rs"] - test: needs: - skip_check - - crates if: needs.skip_check.outputs.should_skip != 'true' - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.crates.outputs.matrix) }} - defaults: - run: - working-directory: ${{ matrix.crate }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Run tests run: | cargo test - cargo +nightly test - - name: Test fixture - if: ${{ matrix.crate == 'ninja-build_rs' }} - working-directory: ${{ matrix.crate }}/examples/assert_matches - run: ./test.sh lint: needs: - skip_check - - crates if: needs.skip_check.outputs.should_skip != 'true' - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.crates.outputs.matrix) }} - defaults: - run: - working-directory: ${{ matrix.crate }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -81,39 +48,9 @@ jobs: fmt: needs: - skip_check - - crates if: needs.skip_check.outputs.should_skip != 'true' - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.crates.outputs.matrix) }} - defaults: - run: - working-directory: ${{ matrix.crate }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: fmt run: cargo fmt --check - - # msrv: - # needs: - # - skip_check - # - crates - # if: needs.skip_check.outputs.should_skip != 'true' - # strategy: - # fail-fast: false - # matrix: ${{ fromJson(needs.crates.outputs.matrix) }} - # defaults: - # run: - # working-directory: ${{ matrix.crate }} - # runs-on: ubuntu-latest - # env: - # RUSTC_BOOTSTRAP: 1 - # steps: - # - uses: actions/checkout@v7 - # - name: install cargo-msrv - # uses: taiki-e/install-action@v2.75.27 - # with: - # tool: cargo-msrv - # - name: check MSRV - # run: cargo msrv verify From fb82894f4b97d83cc192272e54b8670b12908caf Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Thu, 27 Aug 2026 05:00:23 +0000 Subject: [PATCH 3/4] clean up CI: rust-stability --- .github/workflows/rust-stability.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/rust-stability.yml b/.github/workflows/rust-stability.yml index 5c4e918..c9fc7d8 100644 --- a/.github/workflows/rust-stability.yml +++ b/.github/workflows/rust-stability.yml @@ -30,16 +30,12 @@ jobs: with: matrix: | channel: ["stable", "beta", "nightly"] - crate: ["ninja-xtask", "ninja-build_rs"] test: needs: crates strategy: fail-fast: false matrix: ${{ fromJson(needs.crates.outputs.matrix) }} - defaults: - run: - working-directory: ${{ matrix.crate }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -55,9 +51,6 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJson(needs.crates.outputs.matrix) }} - defaults: - run: - working-directory: ${{ matrix.crate }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 From cb12f9fbb3e989010c7eaa29a976295de8fcf669 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Thu, 27 Aug 2026 05:08:38 +0000 Subject: [PATCH 4/4] run tests in correct working dir --- .github/workflows/rust-stability.yml | 6 ++++++ .github/workflows/rust.yml | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/.github/workflows/rust-stability.yml b/.github/workflows/rust-stability.yml index c9fc7d8..527817e 100644 --- a/.github/workflows/rust-stability.yml +++ b/.github/workflows/rust-stability.yml @@ -37,6 +37,9 @@ jobs: fail-fast: false matrix: ${{ fromJson(needs.crates.outputs.matrix) }} runs-on: ubuntu-latest + defaults: + run: + working-directory: ninja-xtask steps: - uses: actions/checkout@v7 - name: update rust @@ -52,6 +55,9 @@ jobs: fail-fast: false matrix: ${{ fromJson(needs.crates.outputs.matrix) }} runs-on: ubuntu-latest + defaults: + run: + working-directory: ninja-xtask steps: - uses: actions/checkout@v7 - name: update rust diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b4febb1..c8b6bb1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -28,6 +28,9 @@ jobs: needs: - skip_check if: needs.skip_check.outputs.should_skip != 'true' + defaults: + run: + working-directory: ninja-xtask runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -39,6 +42,9 @@ jobs: needs: - skip_check if: needs.skip_check.outputs.should_skip != 'true' + defaults: + run: + working-directory: ninja-xtask runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -49,6 +55,9 @@ jobs: needs: - skip_check if: needs.skip_check.outputs.should_skip != 'true' + defaults: + run: + working-directory: ninja-xtask runs-on: ubuntu-latest steps: - uses: actions/checkout@v7