Skip to main content

cargo/workspace/
features.rs

1//! Support for nightly features in Cargo itself.
2//!
3//! This file is the version of `feature_gate.rs` in upstream Rust for Cargo
4//! itself and is intended to be the avenue for which new features in Cargo are
5//! gated by default and then eventually stabilized. All known stable and
6//! unstable features are tracked in this file.
7//!
8//! If you're reading this then you're likely interested in adding a feature to
9//! Cargo, and the good news is that it shouldn't be too hard! First determine
10//! how the feature should be gated:
11//!
12//! * Error when the feature is used without the gate
13//!   * Required if ignoring the feature violates the users intent in non-superficial ways
14//!   * A low-effort / safe way to protect the user from being broken if the format of the feature changes in
15//!     incompatible was (can be worked around)
16//!   * Good for: CLI (gate: `-Zunstable-options` or `-Z` if combined with other changes), `Cargo.toml` (gate: `cargo-features`)
17//! * Warn that the feature is ignored due to lack of the gate
18//!   * For if you could opt-in to the unimplemented feature on Cargo today and Cargo would
19//!     operate just fine
20//!   * If gate is not enabled, prefer to warn if the format of the feature is incompatible
21//!     (instead of error or ignore)
22//!   * Good for: `Cargo.toml`, `.cargo/config.toml`, `config.json` index file (gate: `-Z`)
23//! * Ignore the feature that is used without a gate
24//!   * For when ignoring the feature has so little impact that annoying the user is not worth it
25//!     (e.g. a config field that changes Cargo's terminal output)
26//!   * For behavior changes without an interface (e.g. the resolver)
27//!   * Good for: `.cargo/config.toml`, `config.json` index file (gate: `-Z`)
28//!
29//! For features that touch multiple parts of Cargo, multiple feature gating strategies (error,
30//! warn, ignore) and mechanisms (`-Z`, `cargo-features`) may be used.
31//!
32//! When adding new tests for your feature, usually the tests should go into a
33//! new module of the testsuite named after the feature. See
34//! <https://doc.crates.io/contrib/tests/writing.html> for more information on
35//! writing tests. Particularly, check out the "Testing Nightly Features"
36//! section for testing unstable features. Be sure to test the feature gate itself.
37//!
38//! After you have added your feature, be sure to update the unstable
39//! documentation at `doc/book/src/reference/unstable.md` to include a short
40//! description of how to use your new feature.
41//!
42//! And hopefully that's it!
43//!
44//! ## `cargo-features`
45//!
46//! The steps for adding new Cargo.toml syntax are:
47//!
48//! 1. Add the cargo-features unstable gate. Search the code below for "look here" to
49//!    find the [`features!`] macro invocation and add your feature to the list.
50//!
51//! 2. Update the Cargo.toml parsing code to handle your new feature.
52//!
53//! 3. Wherever you added the new parsing code, call
54//!    `features.require(Feature::my_feature_name())?` if the new syntax is
55//!    used. This will return an error if the user hasn't listed the feature
56//!    in `cargo-features` or this is not the nightly channel.
57//!
58//! ## `-Z unstable-options`
59//!
60//! `-Z unstable-options` is intended to force the user to opt-in to new CLI
61//! flags, options, and new subcommands.
62//!
63//! The steps to add a new command-line option are:
64//!
65//! 1. Add the option to the CLI parsing code. In the help text, be sure to
66//!    include `(unstable)` to note that this is an unstable option.
67//! 2. Where the CLI option is loaded, be sure to call
68//!    [`CliUnstable::fail_if_stable_opt`]. This will return an error if `-Z
69//!    unstable options` was not passed.
70//!
71//! ## `-Z` options
72//!
73//! New `-Z` options cover all other functionality that isn't covered with
74//! `cargo-features` or `-Z unstable-options`.
75//!
76//! The steps to add a new `-Z` option are:
77//!
78//! 1. Add the option to the [`CliUnstable`] struct in the macro invocation of
79//!    [`unstable_cli_options!`]. Flags can take an optional value if you want.
80//! 2. Update the [`CliUnstable::add`] function to parse the flag.
81//! 3. Wherever the new functionality is implemented, call
82//!    [`GlobalContext::cli_unstable`] to get an instance of [`CliUnstable`]
83//!    and check if the option has been enabled on the [`CliUnstable`] instance.
84//!    Nightly gating is already handled, so no need to worry about that.
85//!    If warning when feature is used without the gate, be sure to gracefully degrade (with a
86//!    warning) when the `Cargo.toml` / `.cargo/config.toml` field usage doesn't match the
87//!    schema.
88//! 4. For any `Cargo.toml` fields, strip them in [`prepare_for_publish`] if the gate isn't set
89//!
90//! ## Stabilization
91//!
92//! For the stabilization process, see
93//! <https://doc.crates.io/contrib/process/unstable.html#stabilization>.
94//!
95//! The steps for stabilizing are roughly:
96//!
97//! 1. Update the feature to be stable, based on the kind of feature:
98//!   1. `cargo-features`: Change the feature to `stable` in the [`features!`]
99//!      macro invocation below, and include the version and a URL for the
100//!      documentation.
101//!   2. `-Z unstable-options`: Find the call to [`fail_if_stable_opt`] and
102//!      remove it. Be sure to update the man pages if necessary.
103//!   3. `-Z` flag: Change the parsing code in [`CliUnstable::add`] to call
104//!      `stabilized_warn` or `stabilized_err` and remove the field from
105//!      [`CliUnstable`]. Remove the `(unstable)` note in the clap help text if
106//!      necessary.
107//! 2. Remove `masquerade_as_nightly_cargo` from any tests, and remove
108//!    `cargo-features` from `Cargo.toml` test files if any. You can
109//!     quickly find what needs to be removed by searching for the name
110//!     of the feature, e.g. `print_im_a_teapot`
111//! 3. Update the docs in unstable.md to move the section to the bottom
112//!    and summarize it similar to the other entries. Update the rest of the
113//!    documentation to add the new feature.
114//!
115//! [`GlobalContext::cli_unstable`]: crate::context::GlobalContext::cli_unstable
116//! [`fail_if_stable_opt`]: CliUnstable::fail_if_stable_opt
117//! [`features!`]: macro.features.html
118//! [`unstable_cli_options!`]: macro.unstable_cli_options.html
119//! [`prepare_for_publish`]: crate::workspace::parser::prepare_for_publish
120
121use std::collections::BTreeSet;
122use std::env;
123use std::fmt::{self, Write};
124use std::path::PathBuf;
125use std::str::FromStr;
126
127use anyhow::{Error, bail};
128use cargo_util::ProcessBuilder;
129use serde::{Deserialize, Serialize};
130use tracing::debug;
131
132use crate::GlobalContext;
133use crate::resolver::ResolveBehavior;
134use crate::util::errors::CargoResult;
135use crate::util::indented_lines;
136
137pub const SEE_CHANNELS: &str = "See https://doc.rust-lang.org/book/appendix-07-nightly-rust.html for more information \
138     about Rust release channels.";
139
140/// Value of [`allow-features`](CliUnstable::allow_features)
141pub type AllowFeatures = BTreeSet<String>;
142
143/// The edition of the compiler ([RFC 2052])
144///
145/// The following sections will guide you how to add and stabilize an edition.
146///
147/// ## Adding a new edition
148///
149/// - Add the next edition to the enum.
150/// - Update every match expression that now fails to compile.
151/// - Update the [`FromStr`] impl.
152/// - Update [`CLI_VALUES`] to include the new edition.
153/// - Set [`LATEST_UNSTABLE`] to Some with the new edition.
154/// - Update the shell completion files.
155/// - Update any failing tests (hopefully there are very few).
156///
157/// ## Stabilization instructions
158///
159/// - Set [`LATEST_UNSTABLE`] to None.
160/// - Set [`LATEST_STABLE`] to the new version.
161/// - Update [`is_stable`] to `true`.
162/// - Set [`first_version`] to the version it will be released.
163/// - Update any tests that are affected.
164/// - Update the man page for the `--edition` flag.
165/// - Update the documentation:
166///   - Update any features impacted by the edition.
167///   - Update manifest.md#the-edition-field.
168///   - Update the `--edition` flag (options-new.md).
169///   - Rebuild man pages.
170///
171/// [RFC 2052]: https://rust-lang.github.io/rfcs/2052-epochs.html
172/// [`FromStr`]: Edition::from_str
173/// [`CLI_VALUES`]: Edition::CLI_VALUES
174/// [`LATEST_UNSTABLE`]: Edition::LATEST_UNSTABLE
175/// [`LATEST_STABLE`]: Edition::LATEST_STABLE
176/// [`first_version`]: Edition::first_version
177/// [`is_stable`]: Edition::is_stable
178/// [`toml`]: crate::workspace::parser
179/// [`features!`]: macro.features.html
180#[derive(
181    Default, Clone, Copy, Debug, Hash, PartialOrd, Ord, Eq, PartialEq, Serialize, Deserialize,
182)]
183pub enum Edition {
184    /// The 2015 edition
185    #[default]
186    Edition2015,
187    /// The 2018 edition
188    Edition2018,
189    /// The 2021 edition
190    Edition2021,
191    /// The 2024 edition
192    Edition2024,
193    /// The future edition (permanently unstable)
194    EditionFuture,
195}
196
197impl Edition {
198    /// The latest edition that is unstable.
199    ///
200    /// This is `None` if there is no next unstable edition.
201    ///
202    /// Note that this does *not* include "future" since this is primarily
203    /// used for tests that need to step between stable and unstable.
204    pub const LATEST_UNSTABLE: Option<Edition> = None;
205    /// The latest stable edition.
206    pub const LATEST_STABLE: Edition = Edition::Edition2024;
207    pub const ALL: &'static [Edition] = &[
208        Self::Edition2015,
209        Self::Edition2018,
210        Self::Edition2021,
211        Self::Edition2024,
212        Self::EditionFuture,
213    ];
214    /// Possible values allowed for the `--edition` CLI flag.
215    ///
216    /// This requires a static value due to the way clap works, otherwise I
217    /// would have built this dynamically.
218    ///
219    /// This does not include `future` since we don't need to create new
220    /// packages with it.
221    pub const CLI_VALUES: [&'static str; 4] = ["2015", "2018", "2021", "2024"];
222
223    /// Returns the first version that a particular edition was released on
224    /// stable.
225    pub(crate) fn first_version(&self) -> Option<semver::Version> {
226        use Edition::*;
227        match self {
228            Edition2015 => None,
229            Edition2018 => Some(semver::Version::new(1, 31, 0)),
230            Edition2021 => Some(semver::Version::new(1, 56, 0)),
231            Edition2024 => Some(semver::Version::new(1, 85, 0)),
232            EditionFuture => None,
233        }
234    }
235
236    /// Returns `true` if this edition is stable in this release.
237    pub fn is_stable(&self) -> bool {
238        use Edition::*;
239        match self {
240            Edition2015 => true,
241            Edition2018 => true,
242            Edition2021 => true,
243            Edition2024 => true,
244            EditionFuture => false,
245        }
246    }
247
248    /// Returns the previous edition from this edition.
249    ///
250    /// Returns `None` for 2015.
251    pub fn previous(&self) -> Option<Edition> {
252        use Edition::*;
253        match self {
254            Edition2015 => None,
255            Edition2018 => Some(Edition2015),
256            Edition2021 => Some(Edition2018),
257            Edition2024 => Some(Edition2021),
258            EditionFuture => panic!("future does not have a previous edition"),
259        }
260    }
261
262    /// Returns the next edition from this edition, returning the last edition
263    /// if this is already the last one.
264    pub fn saturating_next(&self) -> Edition {
265        use Edition::*;
266        // Nothing should treat "future" as being next.
267        match self {
268            Edition2015 => Edition2018,
269            Edition2018 => Edition2021,
270            Edition2021 => Edition2024,
271            Edition2024 => Edition2024,
272            EditionFuture => EditionFuture,
273        }
274    }
275
276    /// Updates the given [`ProcessBuilder`] to include the appropriate flags
277    /// for setting the edition.
278    pub(crate) fn cmd_edition_arg(&self, cmd: &mut ProcessBuilder) {
279        cmd.arg(format!("--edition={}", self));
280        if !self.is_stable() {
281            cmd.arg("-Z").arg("unstable-options");
282        }
283    }
284
285    /// Adds the appropriate argument to generate warnings for this edition.
286    pub(crate) fn force_warn_arg(&self, cmd: &mut ProcessBuilder) {
287        use Edition::*;
288        match self {
289            Edition2015 => {}
290            EditionFuture => {
291                cmd.arg("--force-warn=edition_future_compatibility");
292            }
293            e => {
294                // Note that cargo always passes this even if the
295                // compatibility lint group does not exist. When a new edition
296                // is introduced, but there are no migration lints, rustc does
297                // not create the lint group. That's OK because rustc will
298                // just generate a warning about an unknown lint which will be
299                // suppressed due to cap-lints.
300                cmd.arg(format!("--force-warn=rust-{e}-compatibility"));
301            }
302        }
303    }
304
305    /// Whether or not this edition supports the `rust_*_idioms` lint.
306    ///
307    /// Ideally this would not be necessary...
308    pub(crate) fn supports_idiom_lint(&self) -> bool {
309        use Edition::*;
310        match self {
311            Edition2015 => false,
312            Edition2018 => true,
313            Edition2021 => false,
314            Edition2024 => false,
315            EditionFuture => false,
316        }
317    }
318
319    pub(crate) fn default_resolve_behavior(&self) -> ResolveBehavior {
320        if *self >= Edition::Edition2024 {
321            ResolveBehavior::V3
322        } else if *self >= Edition::Edition2021 {
323            ResolveBehavior::V2
324        } else {
325            ResolveBehavior::V1
326        }
327    }
328}
329
330impl fmt::Display for Edition {
331    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        match *self {
333            Edition::Edition2015 => f.write_str("2015"),
334            Edition::Edition2018 => f.write_str("2018"),
335            Edition::Edition2021 => f.write_str("2021"),
336            Edition::Edition2024 => f.write_str("2024"),
337            Edition::EditionFuture => f.write_str("future"),
338        }
339    }
340}
341
342impl FromStr for Edition {
343    type Err = Error;
344    fn from_str(s: &str) -> Result<Self, Error> {
345        match s {
346            "2015" => Ok(Edition::Edition2015),
347            "2018" => Ok(Edition::Edition2018),
348            "2021" => Ok(Edition::Edition2021),
349            "2024" => Ok(Edition::Edition2024),
350            "future" => Ok(Edition::EditionFuture),
351            s if s.parse().map_or(false, |y: u16| y > 2024 && y < 2050) => bail!(
352                "this version of Cargo is older than the `{}` edition, \
353                 and only supports `2015`, `2018`, `2021`, and `2024` editions.",
354                s
355            ),
356            s => bail!(
357                "supported edition values are `2015`, `2018`, `2021`, or `2024`, \
358                 but `{}` is unknown",
359                s
360            ),
361        }
362    }
363}
364
365/// The value for `-Zfix-edition`.
366#[derive(Debug, Deserialize)]
367pub enum FixEdition {
368    /// `-Zfix-edition=start=$INITIAL`
369    ///
370    /// This mode for `cargo fix` will just run `cargo check` if the current
371    /// edition is equal to this edition. If it is a different edition, then
372    /// it just exits with success. This is used for crater integration which
373    /// needs to set a baseline for the "before" toolchain.
374    Start(Edition),
375    /// `-Zfix-edition=end=$INITIAL,$NEXT`
376    ///
377    /// This mode for `cargo fix` will migrate to the `next` edition if the
378    /// current edition is `initial`. After migration, it will update
379    /// `Cargo.toml` and verify that that it works on the new edition. If the
380    /// current edition is not `initial`, then it immediately exits with
381    /// success since we just want to ignore those packages.
382    End { initial: Edition, next: Edition },
383}
384
385impl FromStr for FixEdition {
386    type Err = anyhow::Error;
387    fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
388        if let Some(start) = s.strip_prefix("start=") {
389            Ok(FixEdition::Start(start.parse()?))
390        } else if let Some(end) = s.strip_prefix("end=") {
391            let (initial, next) = end
392                .split_once(',')
393                .ok_or_else(|| anyhow::format_err!("expected `initial,next`"))?;
394            Ok(FixEdition::End {
395                initial: initial.parse()?,
396                next: next.parse()?,
397            })
398        } else {
399            bail!("invalid `-Zfix-edition, expected start= or end=, got `{s}`");
400        }
401    }
402}
403
404#[derive(Debug, PartialEq)]
405enum Status {
406    Stable,
407    Unstable,
408    Removed,
409}
410
411/// A listing of stable and unstable new syntax in Cargo.toml.
412///
413/// This generates definitions and impls for [`Features`] and [`Feature`]
414/// for each new syntax.
415///
416/// Note that all feature names in the macro invocation are valid Rust
417/// identifiers, but the `_` character is translated to `-` when specified in
418/// the `cargo-features` manifest entry in `Cargo.toml`.
419///
420/// See the [module-level documentation](self#new-cargotoml-syntax)
421/// for the process of adding a new syntax.
422macro_rules! features {
423    (
424        $(
425            $(#[$attr:meta])*
426            ($stab:ident, $feature:ident, $version:expr, $docs:expr),
427        )*
428    ) => (
429        /// Unstable feature context for querying if a new Cargo.toml syntax
430        /// is allowed to use.
431        ///
432        /// See the [module-level documentation](self#new-cargotoml-syntax) for the usage.
433        #[derive(Default, Clone, Debug)]
434        pub struct Features {
435            $($feature: bool,)*
436            /// The current activated features.
437            activated: Vec<String>,
438            /// Whether is allowed to use any unstable features.
439            nightly_features_allowed: bool,
440            /// Whether the source manifest is from a local package.
441            is_local: bool,
442        }
443
444        impl Feature {
445            $(
446                $(#[$attr])*
447                #[doc = concat!("\n\n\nSee <https://doc.rust-lang.org/nightly/cargo/", $docs, ">.")]
448                pub const fn $feature() -> &'static Feature {
449                    fn get(features: &Features) -> bool {
450                        stab!($stab) == Status::Stable || features.$feature
451                    }
452                    const FEAT: Feature = Feature {
453                        name: stringify!($feature),
454                        stability: stab!($stab),
455                        version: $version,
456                        docs: $docs,
457                        get,
458                    };
459                    &FEAT
460                }
461            )*
462
463            /// Whether this feature is allowed to use in the given [`Features`] context.
464            fn is_enabled(&self, features: &Features) -> bool {
465                (self.get)(features)
466            }
467
468            pub(crate) fn name(&self) -> &str {
469                self.name
470            }
471        }
472
473        impl Features {
474            fn status(&mut self, feature: &str) -> Option<(&mut bool, &'static Feature)> {
475                if feature.contains("_") {
476                    return None;
477                }
478                let feature = feature.replace("-", "_");
479                $(
480                    if feature == stringify!($feature) {
481                        return Some((&mut self.$feature, Feature::$feature()));
482                    }
483                )*
484                None
485            }
486        }
487    )
488}
489
490macro_rules! stab {
491    (stable) => {
492        Status::Stable
493    };
494    (unstable) => {
495        Status::Unstable
496    };
497    (removed) => {
498        Status::Removed
499    };
500}
501
502// "look here"
503features! {
504    /// A dummy feature that doesn't actually gate anything, but it's used in
505    /// testing to ensure that we can enable stable features.
506    (stable, test_dummy_stable, "1.0", ""),
507
508    /// A dummy feature that gates the usage of the `im-a-teapot` manifest
509    /// entry. This is basically just intended for tests.
510    (unstable, test_dummy_unstable, "", "reference/unstable.html"),
511
512    /// Downloading packages from alternative registry indexes.
513    (stable, alternative_registries, "1.34", "reference/registries.html"),
514
515    /// Using editions
516    (stable, edition, "1.31", "reference/manifest.html#the-edition-field"),
517
518    /// Renaming a package in the manifest via the `package` key.
519    (stable, rename_dependency, "1.31", "reference/specifying-dependencies.html#renaming-dependencies-in-cargotoml"),
520
521    /// Whether a lock file is published with this crate.
522    (removed, publish_lockfile, "1.37", "reference/unstable.html#publish-lockfile"),
523
524    /// Overriding profiles for dependencies.
525    (stable, profile_overrides, "1.41", "reference/profiles.html#overrides"),
526
527    /// "default-run" manifest option.
528    (stable, default_run, "1.37", "reference/manifest.html#the-default-run-field"),
529
530    /// Declarative build scripts.
531    (unstable, metabuild, "", "reference/unstable.html#metabuild"),
532
533    /// Specifying the 'public' attribute on dependencies.
534    (unstable, public_dependency, "", "reference/unstable.html#public-dependency"),
535
536    /// Allow to specify profiles other than 'dev', 'release', 'test', etc.
537    (stable, named_profiles, "1.57", "reference/profiles.html#custom-profiles"),
538
539    /// Opt-in new-resolver behavior.
540    (stable, resolver, "1.51", "reference/resolver.html#resolver-versions"),
541
542    /// Allow to specify whether binaries should be stripped.
543    (stable, strip, "1.58", "reference/profiles.html#strip-option"),
544
545    /// Specifying a minimal 'rust-version' attribute for crates.
546    (stable, rust_version, "1.56", "reference/manifest.html#the-rust-version-field"),
547
548    /// Support for 2021 edition.
549    (stable, edition2021, "1.56", "reference/manifest.html#the-edition-field"),
550
551    /// Allow to specify per-package targets (compile kinds).
552    (unstable, per_package_target, "", "reference/unstable.html#per-package-target"),
553
554    /// Allow to specify which codegen backend should be used.
555    (unstable, codegen_backend, "", "reference/unstable.html#codegen-backend"),
556
557    /// Allow specifying different binary name apart from the crate name.
558    (unstable, different_binary_name, "", "reference/unstable.html#different-binary-name"),
559
560    /// Allow specifying rustflags directly in a profile.
561    (unstable, profile_rustflags, "", "reference/unstable.html#profile-rustflags-option"),
562
563    /// Allow workspace members to inherit fields and dependencies from a workspace.
564    (stable, workspace_inheritance, "1.64", "reference/unstable.html#workspace-inheritance"),
565
566    /// Support for 2024 edition.
567    (stable, edition2024, "1.85", "reference/manifest.html#the-edition-field"),
568
569    /// Allow setting trim-paths in a profile to control the sanitisation of file paths in build outputs.
570    (unstable, trim_paths, "", "reference/unstable.html#profile-trim-paths-option"),
571
572    /// Allow multiple packages to participate in the same API namespace
573    (unstable, open_namespaces, "", "reference/unstable.html#open-namespaces"),
574
575    /// Allow paths that resolve relatively to a base specified in the config.
576    (unstable, path_bases, "", "reference/unstable.html#path-bases"),
577
578    /// Allows use of editions that are not yet stable.
579    (unstable, unstable_editions, "", "reference/unstable.html#unstable-editions"),
580
581    /// Allows use of multiple build scripts.
582    (unstable, multiple_build_scripts, "", "reference/unstable.html#multiple-build-scripts"),
583
584    /// Allows use of panic="immediate-abort".
585    (unstable, panic_immediate_abort, "", "reference/unstable.html#panic-immediate-abort"),
586
587    /// Allow to use a table for defining features.
588    (unstable, feature_metadata, "", "reference/unstable.html#feature_metadata"),
589}
590
591/// Status and metadata for a single unstable feature.
592#[derive(Debug)]
593pub struct Feature {
594    /// Feature name. This is valid Rust identifier so no dash only underscore.
595    name: &'static str,
596    stability: Status,
597    /// Version that this feature was stabilized or removed.
598    version: &'static str,
599    /// Link to the unstable documentation.
600    docs: &'static str,
601    get: fn(&Features) -> bool,
602}
603
604impl Features {
605    /// Creates a new unstable features context.
606    pub fn new(
607        features: &[String],
608        gctx: &GlobalContext,
609        warnings: &mut Vec<String>,
610        is_local: bool,
611    ) -> CargoResult<Features> {
612        let mut ret = Features::default();
613        ret.nightly_features_allowed = gctx.nightly_features_allowed;
614        ret.is_local = is_local;
615        for feature in features {
616            ret.add(feature, gctx, warnings)?;
617            ret.activated.push(feature.to_string());
618        }
619        Ok(ret)
620    }
621
622    fn add(
623        &mut self,
624        feature_name: &str,
625        gctx: &GlobalContext,
626        warnings: &mut Vec<String>,
627    ) -> CargoResult<()> {
628        let nightly_features_allowed = self.nightly_features_allowed;
629        let Some((slot, feature)) = self.status(feature_name) else {
630            let mut msg = format!("unknown Cargo.toml feature `{feature_name}`\n\n");
631            let mut append_see_docs = true;
632
633            if feature_name.contains('_') {
634                let _ = writeln!(msg, "Feature names must use '-' instead of '_'.");
635                append_see_docs = false;
636            } else {
637                let underscore_name = feature_name.replace('-', "_");
638                if CliUnstable::help()
639                    .iter()
640                    .any(|(option, _)| *option == underscore_name)
641                {
642                    let _ = writeln!(
643                        msg,
644                        "This feature can be enabled via -Z{feature_name} or the `[unstable]` section in config.toml."
645                    );
646                }
647            }
648
649            if append_see_docs {
650                let _ = writeln!(
651                    msg,
652                    "See https://doc.rust-lang.org/nightly/cargo/reference/unstable.html for more information."
653                );
654            }
655            bail!(msg)
656        };
657
658        if *slot {
659            bail!(
660                "the cargo feature `{}` has already been activated",
661                feature_name
662            );
663        }
664
665        let see_docs = || {
666            format!(
667                "See {} for more information about using this feature.",
668                cargo_docs_link(feature.docs)
669            )
670        };
671
672        match feature.stability {
673            Status::Stable => {
674                let warning = format!(
675                    "the cargo feature `{}` has been stabilized in the {} \
676                         release and is no longer necessary to be listed in the \
677                         manifest\n  {}",
678                    feature_name,
679                    feature.version,
680                    see_docs()
681                );
682                warnings.push(warning);
683            }
684            Status::Unstable if !nightly_features_allowed => bail!(
685                "the cargo feature `{}` requires a nightly version of \
686                 Cargo, but this is the `{}` channel\n\
687                 {}\n{}",
688                feature_name,
689                channel(),
690                SEE_CHANNELS,
691                see_docs()
692            ),
693            Status::Unstable => {
694                if let Some(allow) = &gctx.cli_unstable().allow_features {
695                    if !allow.contains(feature_name) {
696                        bail!(
697                            "the feature `{}` is not in the list of allowed features: [{}]",
698                            feature_name,
699                            itertools::join(allow, ", "),
700                        );
701                    }
702                }
703            }
704            Status::Removed => {
705                let mut msg = format!(
706                    "the cargo feature `{}` has been removed in the {} release\n\n",
707                    feature_name, feature.version
708                );
709                if self.is_local {
710                    let _ = writeln!(
711                        msg,
712                        "Remove the feature from Cargo.toml to remove this error."
713                    );
714                } else {
715                    let _ = writeln!(
716                        msg,
717                        "This package cannot be used with this version of Cargo, \
718                         as the unstable feature `{}` is no longer supported.",
719                        feature_name
720                    );
721                }
722                let _ = writeln!(msg, "{}", see_docs());
723                bail!(msg);
724            }
725        }
726
727        *slot = true;
728
729        Ok(())
730    }
731
732    /// Gets the current activated features.
733    pub fn activated(&self) -> &[String] {
734        &self.activated
735    }
736
737    /// Checks if the given feature is enabled.
738    pub fn require(&self, feature: &Feature) -> CargoResult<()> {
739        self.require_with_hint(feature, None)
740    }
741
742    /// Like [`require`][Self::require], but appends an optional help message
743    /// to the error, placed just before the documentation link.
744    ///
745    /// Use this when the call site has additional context (e.g. the package's
746    /// `rust-version`) that can make the error more actionable.
747    pub(crate) fn require_with_hint(
748        &self,
749        feature: &Feature,
750        hint: Option<&str>,
751    ) -> CargoResult<()> {
752        if feature.is_enabled(self) {
753            return Ok(());
754        }
755        let feature_name = feature.name.replace("_", "-");
756        let mut msg = format!(
757            "feature `{}` is required\n\
758             \n\
759             The package requires the Cargo feature called `{}`, but \
760             that feature is not stabilized in this version of Cargo ({}).\n\
761            ",
762            feature_name,
763            feature_name,
764            crate::version(),
765        );
766
767        if self.nightly_features_allowed {
768            if self.is_local {
769                let _ = writeln!(
770                    msg,
771                    "Consider adding `cargo-features = [\"{}\"]` \
772                     to the top of Cargo.toml (above the [package] table) \
773                     to tell Cargo you are opting in to use this unstable feature.",
774                    feature_name
775                );
776            } else {
777                let _ = writeln!(msg, "Consider trying a more recent nightly release.");
778            }
779        } else {
780            let _ = writeln!(
781                msg,
782                "Consider trying a newer version of Cargo \
783                 (this may require the nightly release)."
784            );
785        }
786        let _ = writeln!(
787            msg,
788            "See https://doc.rust-lang.org/nightly/cargo/{} for more information \
789             about the status of this feature.",
790            feature.docs
791        );
792        if let Some(hint) = hint {
793            let _ = writeln!(msg, "{hint}");
794        }
795
796        bail!("{}", msg);
797    }
798
799    /// Whether the given feature is allowed to use in this context.
800    pub fn is_enabled(&self, feature: &Feature) -> bool {
801        feature.is_enabled(self)
802    }
803}
804
805/// Generates `-Z` flags as fields of [`CliUnstable`].
806///
807/// See the [module-level documentation](self#-z-options) for details.
808macro_rules! unstable_cli_options {
809    (
810        $(
811            $(#[$meta:meta])?
812            $element: ident: $ty: ty$( = ($help:literal))?,
813        )*
814    ) => {
815        /// A parsed representation of all unstable flags that Cargo accepts.
816        ///
817        /// Cargo, like `rustc`, accepts a suite of `-Z` flags which are intended for
818        /// gating unstable functionality to Cargo. These flags are only available on
819        /// the nightly channel of Cargo.
820        #[derive(Debug, Deserialize)]
821        #[serde(default, rename_all = "kebab-case")]
822        pub struct CliUnstable {
823            $(
824                $(#[doc = $help])?
825                $(#[$meta])?
826                pub $element: $ty
827            ),*
828        }
829        impl CliUnstable {
830            /// Returns a list of `(<option-name>, <help-text>)`.
831            pub fn help() -> Vec<(&'static str, Option<&'static str>)> {
832                let fields = vec![$((stringify!($element), None$(.or(Some($help)))?)),*];
833                fields
834            }
835        }
836        impl Default for CliUnstable {
837            fn default() -> Self {
838                let mut unstable = Self {
839                    $(
840                        $element: Default::default()
841                    ),*
842                };
843
844                unstable.build_dir_new_layout = !is_new_build_dir_layout_opt_out();
845
846                return unstable;
847            }
848        }
849
850        #[cfg(test)]
851        mod test {
852            #[test]
853            fn ensure_sorted() {
854                // This will be printed out if the fields are not sorted.
855                let location = std::panic::Location::caller();
856                println!(
857                    "\nTo fix this test, sort the features inside the macro at {}:{}\n",
858                    location.file(),
859                    location.line()
860                );
861                let mut expected = vec![$(stringify!($element)),*];
862                // Skip permanently unstable fields
863                expected[3..].sort();
864                let expected = format!("{:#?}", expected);
865                let actual = format!("{:#?}", vec![$(stringify!($element)),*]);
866                snapbox::assert_data_eq!(actual, expected);
867            }
868        }
869    }
870}
871
872unstable_cli_options!(
873    // Permanently unstable features:
874    allow_features: Option<AllowFeatures> = ("Allow *only* the listed unstable features"),
875    embed_metadata: Option<bool> = ("Avoid embedding metadata in library artifacts"),
876    print_im_a_teapot: bool,
877
878    // All other unstable features.
879    // Please keep this list lexicographically ordered.
880    advanced_env: bool,
881    any_build_script_metadata: bool = ("Allow any build script to specify env vars via cargo::metadata=key=value"),
882    asymmetric_token: bool = ("Allows authenticating with asymmetric tokens"),
883    avoid_dev_deps: bool = ("Avoid installing dev-dependencies if possible"),
884    binary_dep_depinfo: bool = ("Track changes to dependency artifacts"),
885    bindeps: bool = ("Allow Cargo packages to depend on bin, cdylib, and staticlib crates, and use the artifacts built by those crates"),
886    build_analysis: bool = ("Record and persist build metrics across runs, with commands to query past builds."),
887    build_dir_new_layout: bool = ("Use the new build-dir filesystem layout"),
888    #[serde(deserialize_with = "deserialize_comma_separated_list")]
889    build_std: Option<Vec<String>>  = ("Enable Cargo to compile the standard library itself as part of a crate graph compilation"),
890    #[serde(deserialize_with = "deserialize_comma_separated_list")]
891    build_std_features: Option<Vec<String>>  = ("Configure features enabled for the standard library itself when building the standard library"),
892    checksum_freshness: bool = ("Use a checksum to determine if output is fresh rather than filesystem mtime"),
893    codegen_backend: bool = ("Enable the `codegen-backend` option in profiles in .cargo/config.toml file"),
894    direct_minimal_versions: bool = ("Resolve minimal dependency versions instead of maximum (direct dependencies only)"),
895    dual_proc_macros: bool = ("Build proc-macros for both the host and the target"),
896    feature_unification: bool = ("Enable new feature unification modes in workspaces"),
897    features: Option<Vec<String>>,
898    fine_grain_locking: bool = ("Use fine grain locking instead of locking the entire build cache"),
899    fix_edition: Option<FixEdition> = ("Permanently unstable edition migration helper"),
900    gc: bool = ("Track cache usage and \"garbage collect\" unused files"),
901    #[serde(deserialize_with = "deserialize_git_features")]
902    git: Option<GitFeatures> = ("Enable support for shallow git fetch operations"),
903    #[serde(deserialize_with = "deserialize_gitoxide_features")]
904    gitoxide: Option<GitoxideFeatures> = ("Use gitoxide for the given git interactions, or all of them if no argument is given"),
905    hint_msrv: bool = ("Enable passing `package.rust-version` to rustc for lints"),
906    host_config: bool = ("Enable the `[host]` section in the .cargo/config.toml file"),
907    json_target_spec: bool = ("Enable `.json` target spec files"),
908    minimal_versions: bool = ("Resolve minimal dependency versions instead of maximum"),
909    msrv_policy: bool = ("Enable rust-version aware policy within cargo"),
910    mtime_on_use: bool = ("Configure Cargo to update the mtime of used files"),
911    next_lockfile_bump: bool,
912    no_index_update: bool = ("Do not update the registry index even if the cache is outdated"),
913    panic_abort_tests: bool = ("Enable support to run tests with -Cpanic=abort"),
914    panic_immediate_abort: bool = ("Enable setting `panic = \"immediate-abort\"` in profiles"),
915    profile_hint_mostly_unused: bool = ("Enable the `hint-mostly-unused` setting in profiles to mark a crate as mostly unused."),
916    profile_rustflags: bool = ("Enable the `rustflags` option in profiles in .cargo/config.toml file"),
917    public_dependency: bool = ("Respect a dependency's `public` field in Cargo.toml to control public/private dependencies"),
918    publish_timeout: bool = ("Enable the `publish.timeout` key in .cargo/config.toml file"),
919    root_dir: Option<PathBuf> = ("Set the root directory relative to which paths are printed (defaults to workspace root)"),
920    rustc_unicode: bool = ("Enable `rustc`'s unicode error format in Cargo's error messages"),
921    rustdoc_depinfo: bool = ("Use dep-info files in rustdoc rebuild detection"),
922    rustdoc_map: bool = ("Allow passing external documentation mappings to rustdoc"),
923    rustdoc_mergeable_info: bool = ("Use rustdoc mergeable cross-crate-info files"),
924    rustdoc_scrape_examples: bool = ("Allows Rustdoc to scrape code examples from reverse-dependencies"),
925    sbom: bool = ("Enable the `sbom` option in build config in .cargo/config.toml file"),
926    script: bool = ("Enable support for single-file, `.rs` packages"),
927    section_timings: bool = ("Enable support for extended compilation sections in --timings output"),
928    separate_nightlies: bool,
929    skip_rustdoc_fingerprint: bool,
930    target_applies_to_host: bool = ("Enable the `target-applies-to-host` key in the .cargo/config.toml file"),
931    trim_paths: bool = ("Enable the `trim-paths` option in profiles"),
932    unstable_options: bool = ("Allow the usage of unstable options"),
933);
934
935const STABILIZED_COMPILE_PROGRESS: &str = "The progress bar is now always \
936    enabled when used on an interactive console.\n\
937    See https://doc.rust-lang.org/cargo/reference/config.html#termprogresswhen \
938    for information on controlling the progress bar.";
939
940const STABILIZED_OFFLINE: &str = "Offline mode is now available via the \
941    --offline CLI option";
942
943const STABILIZED_CACHE_MESSAGES: &str = "Message caching is now always enabled.";
944
945const STABILIZED_INSTALL_UPGRADE: &str = "Packages are now always upgraded if \
946    they appear out of date.\n\
947    See https://doc.rust-lang.org/cargo/commands/cargo-install.html for more \
948    information on how upgrading works.";
949
950const STABILIZED_CONFIG_PROFILE: &str = "See \
951    https://doc.rust-lang.org/cargo/reference/config.html#profile for more \
952    information about specifying profiles in config.";
953
954const STABILIZED_CRATE_VERSIONS: &str = "The crate version is now \
955    automatically added to the documentation.";
956
957const STABILIZED_PACKAGE_FEATURES: &str = "Enhanced feature flag behavior is now \
958    available in virtual workspaces, and `member/feature-name` syntax is also \
959    always available. Other extensions require setting `resolver = \"2\"` in \
960    Cargo.toml.\n\
961    See https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags \
962    for more information.";
963
964const STABILIZED_FEATURES: &str = "The new feature resolver is now available \
965    by specifying `resolver = \"2\"` in Cargo.toml.\n\
966    See https://doc.rust-lang.org/nightly/cargo/reference/features.html#feature-resolver-version-2 \
967    for more information.";
968
969const STABILIZED_EXTRA_LINK_ARG: &str = "Additional linker arguments are now \
970    supported without passing this flag.";
971
972const STABILIZED_CONFIGURABLE_ENV: &str = "The [env] section is now always enabled.";
973
974const STABILIZED_PATCH_IN_CONFIG: &str = "The patch-in-config feature is now always enabled.";
975
976const STABILIZED_NAMED_PROFILES: &str = "The named-profiles feature is now always enabled.\n\
977    See https://doc.rust-lang.org/nightly/cargo/reference/profiles.html#custom-profiles \
978    for more information";
979
980const STABILIZED_DOCTEST_IN_WORKSPACE: &str =
981    "The doctest-in-workspace feature is now always enabled.";
982
983const STABILIZED_FUTURE_INCOMPAT_REPORT: &str =
984    "The future-incompat-report feature is now always enabled.";
985
986const STABILIZED_WEAK_DEP_FEATURES: &str = "Weak dependency features are now always available.";
987
988const STABILISED_NAMESPACED_FEATURES: &str = "Namespaced features are now always available.";
989
990const STABILIZED_TIMINGS: &str = "The -Ztimings option has been stabilized as --timings.";
991
992const STABILISED_MULTITARGET: &str = "Multiple `--target` options are now always available.";
993
994const STABILIZED_TERMINAL_WIDTH: &str =
995    "The -Zterminal-width option is now always enabled for terminal output.";
996
997const STABILISED_SPARSE_REGISTRY: &str = "The sparse protocol is now the default for crates.io";
998
999const STABILIZED_CREDENTIAL_PROCESS: &str =
1000    "Authentication with a credential provider is always available.";
1001
1002const STABILIZED_REGISTRY_AUTH: &str =
1003    "Authenticated registries are available if a credential provider is configured.";
1004
1005const STABILIZED_LINTS: &str = "The `[lints]` table is now always available.";
1006
1007const STABILIZED_CARGO_LINTS: &str = "The `[lints.cargo]` table is now always available.";
1008
1009const STABILIZED_CHECK_CFG: &str =
1010    "Compile-time checking of conditional (a.k.a. `-Zcheck-cfg`) is now always enabled.";
1011
1012const STABILIZED_DOCTEST_XCOMPILE: &str = "Doctest cross-compiling is now always enabled.";
1013
1014const STABILIZED_PACKAGE_WORKSPACE: &str =
1015    "Workspace packaging and publishing (a.k.a. `-Zpackage-workspace`) is now always enabled.";
1016
1017const STABILIZED_BUILD_DIR: &str = "build.build-dir is now always enabled.";
1018
1019const STABILIZED_CONFIG_INCLUDE: &str = "The `include` config key is now always available";
1020
1021const STABILIZED_LOCKFILE_PATH: &str = "The `lockfile-path` config key is now always available";
1022
1023const STABILIZED_WARNINGS: &str = "The `build.warnings` config key is now always available";
1024
1025const STABILIZED_BUILD_DIR_NEW_LAYOUT: &str = "build.build-dir-new-layout is now always enabled.";
1026
1027const STABILIZED_MIN_PUBLISH_AGE: &str =
1028    "The `min-publish-age` configuration is now always available.";
1029
1030fn deserialize_comma_separated_list<'de, D>(
1031    deserializer: D,
1032) -> Result<Option<Vec<String>>, D::Error>
1033where
1034    D: serde::Deserializer<'de>,
1035{
1036    let Some(list) = <Option<Vec<String>>>::deserialize(deserializer)? else {
1037        return Ok(None);
1038    };
1039    let v = list
1040        .iter()
1041        .flat_map(|s| s.split(','))
1042        .filter(|s| !s.is_empty())
1043        .map(String::from)
1044        .collect();
1045    Ok(Some(v))
1046}
1047
1048#[derive(Debug, Copy, Clone, Default, Deserialize, Ord, PartialOrd, Eq, PartialEq)]
1049#[serde(default)]
1050pub struct GitFeatures {
1051    /// When cloning the index, perform a shallow clone. Maintain shallowness upon subsequent fetches.
1052    pub shallow_index: bool,
1053    /// When cloning git dependencies, perform a shallow clone and maintain shallowness on subsequent fetches.
1054    pub shallow_deps: bool,
1055}
1056
1057impl GitFeatures {
1058    pub fn all() -> Self {
1059        GitFeatures {
1060            shallow_index: true,
1061            shallow_deps: true,
1062        }
1063    }
1064
1065    fn expecting() -> String {
1066        let fields = ["`shallow-index`", "`shallow-deps`"];
1067        format!(
1068            "unstable 'git' only takes {} as valid inputs",
1069            fields.join(" and ")
1070        )
1071    }
1072}
1073
1074fn deserialize_git_features<'de, D>(deserializer: D) -> Result<Option<GitFeatures>, D::Error>
1075where
1076    D: serde::de::Deserializer<'de>,
1077{
1078    struct GitFeaturesVisitor;
1079
1080    impl<'de> serde::de::Visitor<'de> for GitFeaturesVisitor {
1081        type Value = Option<GitFeatures>;
1082
1083        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1084            formatter.write_str(&GitFeatures::expecting())
1085        }
1086
1087        fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
1088        where
1089            E: serde::de::Error,
1090        {
1091            if v {
1092                Ok(Some(GitFeatures::all()))
1093            } else {
1094                Ok(None)
1095            }
1096        }
1097
1098        fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
1099        where
1100            E: serde::de::Error,
1101        {
1102            Ok(parse_git(s.split(",")).map_err(serde::de::Error::custom)?)
1103        }
1104
1105        fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1106        where
1107            D: serde::de::Deserializer<'de>,
1108        {
1109            let git = GitFeatures::deserialize(deserializer)?;
1110            Ok(Some(git))
1111        }
1112
1113        fn visit_map<V>(self, map: V) -> Result<Self::Value, V::Error>
1114        where
1115            V: serde::de::MapAccess<'de>,
1116        {
1117            let mvd = serde::de::value::MapAccessDeserializer::new(map);
1118            Ok(Some(GitFeatures::deserialize(mvd)?))
1119        }
1120    }
1121
1122    deserializer.deserialize_any(GitFeaturesVisitor)
1123}
1124
1125fn parse_git(it: impl Iterator<Item = impl AsRef<str>>) -> CargoResult<Option<GitFeatures>> {
1126    let mut out = GitFeatures::default();
1127    let GitFeatures {
1128        shallow_index,
1129        shallow_deps,
1130    } = &mut out;
1131
1132    for e in it {
1133        match e.as_ref() {
1134            "shallow-index" => *shallow_index = true,
1135            "shallow-deps" => *shallow_deps = true,
1136            _ => {
1137                bail!(GitFeatures::expecting())
1138            }
1139        }
1140    }
1141    Ok(Some(out))
1142}
1143
1144#[derive(Debug, Copy, Clone, Default, Deserialize, Ord, PartialOrd, Eq, PartialEq)]
1145#[serde(default)]
1146pub struct GitoxideFeatures {
1147    /// All fetches are done with `gitoxide`, which includes git dependencies as well as the crates index.
1148    pub fetch: bool,
1149    /// Checkout git dependencies using `gitoxide` (submodules are still handled by git2 ATM, and filters
1150    /// like linefeed conversions are unsupported).
1151    pub checkout: bool,
1152}
1153
1154impl GitoxideFeatures {
1155    pub fn all() -> Self {
1156        GitoxideFeatures {
1157            fetch: true,
1158            checkout: true,
1159        }
1160    }
1161
1162    fn expecting() -> String {
1163        let fields = ["`fetch`", "`checkout`"];
1164        format!(
1165            "unstable 'gitoxide' only takes {} as valid inputs, for shallow fetches see `-Zgit=shallow-index,shallow-deps`",
1166            fields.join(" and ")
1167        )
1168    }
1169}
1170
1171fn deserialize_gitoxide_features<'de, D>(
1172    deserializer: D,
1173) -> Result<Option<GitoxideFeatures>, D::Error>
1174where
1175    D: serde::de::Deserializer<'de>,
1176{
1177    struct GitoxideFeaturesVisitor;
1178
1179    impl<'de> serde::de::Visitor<'de> for GitoxideFeaturesVisitor {
1180        type Value = Option<GitoxideFeatures>;
1181
1182        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1183            formatter.write_str(&GitoxideFeatures::expecting())
1184        }
1185
1186        fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
1187        where
1188            E: serde::de::Error,
1189        {
1190            Ok(parse_gitoxide(s.split(",")).map_err(serde::de::Error::custom)?)
1191        }
1192
1193        fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
1194        where
1195            E: serde::de::Error,
1196        {
1197            if v {
1198                Ok(Some(GitoxideFeatures::all()))
1199            } else {
1200                Ok(None)
1201            }
1202        }
1203
1204        fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1205        where
1206            D: serde::de::Deserializer<'de>,
1207        {
1208            let gitoxide = GitoxideFeatures::deserialize(deserializer)?;
1209            Ok(Some(gitoxide))
1210        }
1211
1212        fn visit_map<V>(self, map: V) -> Result<Self::Value, V::Error>
1213        where
1214            V: serde::de::MapAccess<'de>,
1215        {
1216            let mvd = serde::de::value::MapAccessDeserializer::new(map);
1217            Ok(Some(GitoxideFeatures::deserialize(mvd)?))
1218        }
1219    }
1220
1221    deserializer.deserialize_any(GitoxideFeaturesVisitor)
1222}
1223
1224fn parse_gitoxide(
1225    it: impl Iterator<Item = impl AsRef<str>>,
1226) -> CargoResult<Option<GitoxideFeatures>> {
1227    let mut out = GitoxideFeatures::default();
1228    let GitoxideFeatures { fetch, checkout } = &mut out;
1229
1230    for e in it {
1231        match e.as_ref() {
1232            "fetch" => *fetch = true,
1233            "checkout" => *checkout = true,
1234            _ => {
1235                bail!(GitoxideFeatures::expecting())
1236            }
1237        }
1238    }
1239    Ok(Some(out))
1240}
1241
1242impl CliUnstable {
1243    /// Parses `-Z` flags from the command line, and returns messages that warn
1244    /// if any flag has already been stabilized.
1245    pub fn parse(
1246        &mut self,
1247        flags: &[String],
1248        nightly_features_allowed: bool,
1249    ) -> CargoResult<Vec<String>> {
1250        if !flags.is_empty() && !nightly_features_allowed {
1251            bail!(
1252                "the `-Z` flag is only accepted on the nightly channel of Cargo, \
1253                 but this is the `{}` channel\n\
1254                 {}",
1255                channel(),
1256                SEE_CHANNELS
1257            );
1258        }
1259        let mut warnings = Vec::new();
1260        // We read flags twice, first to get allowed-features (if specified),
1261        // and then to read the remaining unstable flags.
1262        for flag in flags {
1263            if flag.starts_with("allow-features=") {
1264                self.add(flag, &mut warnings)?;
1265            }
1266        }
1267        for flag in flags {
1268            self.add(flag, &mut warnings)?;
1269        }
1270
1271        self.implicitly_enable_features_if_needed();
1272
1273        Ok(warnings)
1274    }
1275
1276    fn add(&mut self, flag: &str, warnings: &mut Vec<String>) -> CargoResult<()> {
1277        let mut parts = flag.splitn(2, '=');
1278        let k = parts.next().unwrap();
1279        let v = parts.next();
1280
1281        fn parse_bool(key: &str, value: Option<&str>) -> CargoResult<bool> {
1282            match value {
1283                None | Some("yes") => Ok(true),
1284                Some("no") => Ok(false),
1285                Some(s) => bail!("flag -Z{} expected `no` or `yes`, found: `{}`", key, s),
1286            }
1287        }
1288
1289        fn parse_option_bool(key: &str, value: Option<&str>) -> CargoResult<Option<bool>> {
1290            match value {
1291                None => Ok(None),
1292                Some("yes") => Ok(Some(true)),
1293                Some("no") => Ok(Some(false)),
1294                Some(s) => bail!("flag -Z{key} expected `no` or `yes`, found: `{s}`"),
1295            }
1296        }
1297
1298        /// Parse a comma-separated list
1299        fn parse_list(value: Option<&str>) -> Vec<String> {
1300            match value {
1301                None => Vec::new(),
1302                Some("") => Vec::new(),
1303                Some(v) => v.split(',').map(|s| s.to_string()).collect(),
1304            }
1305        }
1306
1307        // Asserts that there is no argument to the flag.
1308        fn parse_empty(key: &str, value: Option<&str>) -> CargoResult<bool> {
1309            if let Some(v) = value {
1310                bail!("flag -Z{} does not take a value, found: `{}`", key, v);
1311            }
1312            Ok(true)
1313        }
1314
1315        let mut stabilized_warn = |key: &str, version: &str, message: &str| {
1316            warnings.push(format!(
1317                "flag `-Z {}` has been stabilized in the {} release, \
1318                 and is no longer necessary\n{}",
1319                key,
1320                version,
1321                indented_lines(message)
1322            ));
1323        };
1324
1325        // Use this if the behavior now requires another mechanism to enable.
1326        let stabilized_err = |key: &str, version: &str, message: &str| {
1327            Err(anyhow::format_err!(
1328                "flag `-Z {}` has been stabilized in the {} release\n{}",
1329                key,
1330                version,
1331                indented_lines(message)
1332            ))
1333        };
1334
1335        if let Some(allowed) = &self.allow_features {
1336            if k != "allow-features" && !allowed.contains(k) {
1337                bail!(
1338                    "the feature `{}` is not in the list of allowed features: [{}]",
1339                    k,
1340                    itertools::join(allowed, ", ")
1341                );
1342            }
1343        }
1344
1345        match k {
1346            // Permanently unstable features
1347            // Sorted alphabetically:
1348            "allow-features" => self.allow_features = Some(parse_list(v).into_iter().collect()),
1349            "embed-metadata" => self.embed_metadata = parse_option_bool(k, v)?,
1350            "print-im-a-teapot" => self.print_im_a_teapot = parse_bool(k, v)?,
1351
1352            // Stabilized features
1353            // Sorted by version, then alphabetically:
1354            "compile-progress" => stabilized_warn(k, "1.30", STABILIZED_COMPILE_PROGRESS),
1355            "offline" => stabilized_err(k, "1.36", STABILIZED_OFFLINE)?,
1356            "cache-messages" => stabilized_warn(k, "1.40", STABILIZED_CACHE_MESSAGES),
1357            "install-upgrade" => stabilized_warn(k, "1.41", STABILIZED_INSTALL_UPGRADE),
1358            "config-profile" => stabilized_warn(k, "1.43", STABILIZED_CONFIG_PROFILE),
1359            "crate-versions" => stabilized_warn(k, "1.47", STABILIZED_CRATE_VERSIONS),
1360            "features" => {
1361                // `-Z features` has been stabilized since 1.51,
1362                // but `-Z features=compare` is still allowed for convenience
1363                // to validate that the feature resolver resolves features
1364                // in the same way as the dependency resolver,
1365                // until we feel confident to remove entirely.
1366                //
1367                // See rust-lang/cargo#11168
1368                let feats = parse_list(v);
1369                let stab_is_not_empty = feats.iter().any(|feat| {
1370                    matches!(
1371                        feat.as_str(),
1372                        "build_dep" | "host_dep" | "dev_dep" | "itarget" | "all"
1373                    )
1374                });
1375                if stab_is_not_empty || feats.is_empty() {
1376                    // Make this stabilized_err once -Zfeature support is removed.
1377                    stabilized_warn(k, "1.51", STABILIZED_FEATURES);
1378                }
1379                self.features = Some(feats);
1380            }
1381            "package-features" => stabilized_warn(k, "1.51", STABILIZED_PACKAGE_FEATURES),
1382            "configurable-env" => stabilized_warn(k, "1.56", STABILIZED_CONFIGURABLE_ENV),
1383            "extra-link-arg" => stabilized_warn(k, "1.56", STABILIZED_EXTRA_LINK_ARG),
1384            "patch-in-config" => stabilized_warn(k, "1.56", STABILIZED_PATCH_IN_CONFIG),
1385            "named-profiles" => stabilized_warn(k, "1.57", STABILIZED_NAMED_PROFILES),
1386            "future-incompat-report" => {
1387                stabilized_warn(k, "1.59.0", STABILIZED_FUTURE_INCOMPAT_REPORT)
1388            }
1389            "namespaced-features" => stabilized_warn(k, "1.60", STABILISED_NAMESPACED_FEATURES),
1390            "timings" => stabilized_warn(k, "1.60", STABILIZED_TIMINGS),
1391            "weak-dep-features" => stabilized_warn(k, "1.60", STABILIZED_WEAK_DEP_FEATURES),
1392            "multitarget" => stabilized_warn(k, "1.64", STABILISED_MULTITARGET),
1393            "sparse-registry" => stabilized_warn(k, "1.68", STABILISED_SPARSE_REGISTRY),
1394            "terminal-width" => stabilized_warn(k, "1.68", STABILIZED_TERMINAL_WIDTH),
1395            "doctest-in-workspace" => stabilized_warn(k, "1.72", STABILIZED_DOCTEST_IN_WORKSPACE),
1396            "credential-process" => stabilized_warn(k, "1.74", STABILIZED_CREDENTIAL_PROCESS),
1397            "lints" => stabilized_warn(k, "1.74", STABILIZED_LINTS),
1398            "registry-auth" => stabilized_warn(k, "1.74", STABILIZED_REGISTRY_AUTH),
1399            "check-cfg" => stabilized_warn(k, "1.80", STABILIZED_CHECK_CFG),
1400            "doctest-xcompile" => stabilized_warn(k, "1.89", STABILIZED_DOCTEST_XCOMPILE),
1401            "package-workspace" => stabilized_warn(k, "1.89", STABILIZED_PACKAGE_WORKSPACE),
1402            "build-dir" => stabilized_warn(k, "1.91", STABILIZED_BUILD_DIR),
1403            "config-include" => stabilized_warn(k, "1.93", STABILIZED_CONFIG_INCLUDE),
1404            "lockfile-path" => stabilized_warn(k, "1.97", STABILIZED_LOCKFILE_PATH),
1405            "warnings" => stabilized_warn(k, "1.97", STABILIZED_WARNINGS),
1406            "build-dir-new-layout" => stabilized_warn(k, "1.100", STABILIZED_BUILD_DIR_NEW_LAYOUT),
1407            "cargo-lints" => stabilized_warn(k, "1.100", STABILIZED_CARGO_LINTS),
1408            "min-publish-age" => stabilized_warn(k, "1.100", STABILIZED_MIN_PUBLISH_AGE),
1409
1410            // Unstable features
1411            // Sorted alphabetically:
1412            "advanced-env" => self.advanced_env = parse_empty(k, v)?,
1413            "any-build-script-metadata" => self.any_build_script_metadata = parse_empty(k, v)?,
1414            "asymmetric-token" => self.asymmetric_token = parse_empty(k, v)?,
1415            "avoid-dev-deps" => self.avoid_dev_deps = parse_empty(k, v)?,
1416            "binary-dep-depinfo" => self.binary_dep_depinfo = parse_empty(k, v)?,
1417            "bindeps" => self.bindeps = parse_empty(k, v)?,
1418            "build-analysis" => self.build_analysis = parse_empty(k, v)?,
1419            "build-std" => self.build_std = Some(parse_list(v)),
1420            "build-std-features" => self.build_std_features = Some(parse_list(v)),
1421            "codegen-backend" => self.codegen_backend = parse_empty(k, v)?,
1422            "direct-minimal-versions" => self.direct_minimal_versions = parse_empty(k, v)?,
1423            "dual-proc-macros" => self.dual_proc_macros = parse_empty(k, v)?,
1424            "feature-unification" => self.feature_unification = parse_empty(k, v)?,
1425            "fine-grain-locking" => self.fine_grain_locking = parse_empty(k, v)?,
1426            "fix-edition" => {
1427                let fe = v
1428                    .ok_or_else(|| anyhow::anyhow!("-Zfix-edition expected a value"))?
1429                    .parse()?;
1430                self.fix_edition = Some(fe);
1431            }
1432            "gc" => self.gc = parse_empty(k, v)?,
1433            "git" => {
1434                self.git =
1435                    v.map_or_else(|| Ok(Some(GitFeatures::all())), |v| parse_git(v.split(',')))?
1436            }
1437            "gitoxide" => {
1438                self.gitoxide = v.map_or_else(
1439                    || Ok(Some(GitoxideFeatures::all())),
1440                    |v| parse_gitoxide(v.split(',')),
1441                )?
1442            }
1443            "host-config" => self.host_config = parse_empty(k, v)?,
1444            "json-target-spec" => self.json_target_spec = parse_empty(k, v)?,
1445            "hint-msrv" => self.hint_msrv = parse_empty(k, v)?,
1446            "next-lockfile-bump" => self.next_lockfile_bump = parse_empty(k, v)?,
1447            "minimal-versions" => self.minimal_versions = parse_empty(k, v)?,
1448            "msrv-policy" => self.msrv_policy = parse_empty(k, v)?,
1449            // can also be set in .cargo/config or with and ENV
1450            "mtime-on-use" => self.mtime_on_use = parse_empty(k, v)?,
1451            "no-index-update" => self.no_index_update = parse_empty(k, v)?,
1452            "panic-abort-tests" => self.panic_abort_tests = parse_empty(k, v)?,
1453            "public-dependency" => self.public_dependency = parse_empty(k, v)?,
1454            "profile-hint-mostly-unused" => self.profile_hint_mostly_unused = parse_empty(k, v)?,
1455            "profile-rustflags" => self.profile_rustflags = parse_empty(k, v)?,
1456            "trim-paths" => self.trim_paths = parse_empty(k, v)?,
1457            "publish-timeout" => self.publish_timeout = parse_empty(k, v)?,
1458            "root-dir" => self.root_dir = v.map(|v| v.into()),
1459            "rustc-unicode" => self.rustc_unicode = parse_empty(k, v)?,
1460            "rustdoc-depinfo" => self.rustdoc_depinfo = parse_empty(k, v)?,
1461            "rustdoc-map" => self.rustdoc_map = parse_empty(k, v)?,
1462            "rustdoc-mergeable-info" => self.rustdoc_mergeable_info = parse_empty(k, v)?,
1463            "rustdoc-scrape-examples" => self.rustdoc_scrape_examples = parse_empty(k, v)?,
1464            "sbom" => self.sbom = parse_empty(k, v)?,
1465            "section-timings" => self.section_timings = parse_empty(k, v)?,
1466            "separate-nightlies" => self.separate_nightlies = parse_empty(k, v)?,
1467            "checksum-freshness" => self.checksum_freshness = parse_empty(k, v)?,
1468            "skip-rustdoc-fingerprint" => self.skip_rustdoc_fingerprint = parse_empty(k, v)?,
1469            "script" => self.script = parse_empty(k, v)?,
1470            "target-applies-to-host" => self.target_applies_to_host = parse_empty(k, v)?,
1471            "panic-immediate-abort" => self.panic_immediate_abort = parse_empty(k, v)?,
1472            "unstable-options" => self.unstable_options = parse_empty(k, v)?,
1473            _ => bail!(
1474                "\
1475            unknown `-Z` flag specified: {k}\n\n\
1476            For available unstable features, see \
1477            https://doc.rust-lang.org/nightly/cargo/reference/unstable.html\n\
1478            If you intended to use an unstable rustc feature, try setting `RUSTFLAGS=\"-Z{k}\"`"
1479            ),
1480        }
1481
1482        Ok(())
1483    }
1484
1485    /// Generates an error if `-Z unstable-options` was not used for a new,
1486    /// unstable command-line flag.
1487    pub fn fail_if_stable_opt(&self, flag: &str, issue: u32) -> CargoResult<()> {
1488        self.fail_if_stable_opt_custom_z(flag, issue, "unstable-options", self.unstable_options)
1489    }
1490
1491    pub fn fail_if_stable_opt_custom_z(
1492        &self,
1493        flag: &str,
1494        issue: u32,
1495        z_name: &str,
1496        enabled: bool,
1497    ) -> CargoResult<()> {
1498        if !enabled {
1499            let see = format!(
1500                "See https://github.com/rust-lang/cargo/issues/{issue} for more \
1501                 information about the `{flag}` flag."
1502            );
1503            // NOTE: a `config` isn't available here, check the channel directly
1504            let channel = channel();
1505            if channel == "nightly" || channel == "dev" {
1506                bail!(
1507                    "the `{flag}` flag is unstable, pass `-Z {z_name}` to enable it\n\
1508                     {see}"
1509                );
1510            } else {
1511                bail!(
1512                    "the `{flag}` flag is unstable, and only available on the nightly channel \
1513                     of Cargo, but this is the `{channel}` channel\n\
1514                     {SEE_CHANNELS}\n\
1515                     {see}"
1516                );
1517            }
1518        }
1519        Ok(())
1520    }
1521
1522    /// Generates an error if `-Z unstable-options` was not used for a new,
1523    /// unstable subcommand.
1524    pub fn fail_if_stable_command(
1525        &self,
1526        gctx: &GlobalContext,
1527        command: &str,
1528        issue: u32,
1529        z_name: &str,
1530        enabled: bool,
1531    ) -> CargoResult<()> {
1532        if enabled {
1533            return Ok(());
1534        }
1535        let see = format!(
1536            "See https://github.com/rust-lang/cargo/issues/{} for more \
1537            information about the `cargo {}` command.",
1538            issue, command
1539        );
1540        if gctx.nightly_features_allowed {
1541            bail!(
1542                "the `cargo {command}` command is unstable, pass `-Z {z_name}` \
1543                 to enable it\n\
1544                 {see}",
1545            );
1546        } else {
1547            bail!(
1548                "the `cargo {}` command is unstable, and only available on the \
1549                 nightly channel of Cargo, but this is the `{}` channel\n\
1550                 {}\n\
1551                 {}",
1552                command,
1553                channel(),
1554                SEE_CHANNELS,
1555                see
1556            );
1557        }
1558    }
1559
1560    fn implicitly_enable_features_if_needed(&mut self) {
1561        if self.fine_grain_locking && !self.build_dir_new_layout {
1562            debug!("-Zbuild-dir-new-layout implicitly enabled by -Zfine-grain-locking");
1563            self.build_dir_new_layout = true;
1564        }
1565    }
1566}
1567
1568/// Returns the current release channel ("stable", "beta", "nightly", "dev").
1569pub fn channel() -> String {
1570    #[expect(
1571        clippy::disallowed_methods,
1572        reason = "testing only, no reason for config support"
1573    )]
1574    if let Ok(override_channel) = env::var("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS") {
1575        return override_channel;
1576    }
1577    #[expect(
1578        clippy::disallowed_methods,
1579        reason = "consistency with rustc, not specified behavior"
1580    )]
1581    if let Ok(staging) = env::var("RUSTC_BOOTSTRAP") {
1582        if staging == "1" {
1583            return "dev".to_string();
1584        }
1585    }
1586    crate::version()
1587        .release_channel
1588        .unwrap_or_else(|| String::from("dev"))
1589}
1590
1591#[expect(
1592    clippy::disallowed_methods,
1593    reason = "Temporary opt out that is not part of the public interface"
1594)]
1595fn is_new_build_dir_layout_opt_out() -> bool {
1596    std::env::var("__CARGO_TEMPORARY_BUILD_DIR_NEW_LAYOUT_OPT_OUT").as_deref() == Ok("1")
1597}
1598
1599/// Generate a link to Cargo documentation for the current release channel
1600/// `path` is the URL component after `https://doc.rust-lang.org/{channel}/cargo/`
1601pub fn cargo_docs_link(path: &str) -> String {
1602    let url_channel = match channel().as_str() {
1603        "dev" | "nightly" => "nightly/",
1604        "beta" => "beta/",
1605        _ => "",
1606    };
1607    format!("https://doc.rust-lang.org/{url_channel}cargo/{path}")
1608}