//! Handles built-in and customizable compiler flag presets.
//!
//! [`Profiles`] is a collections of built-in profiles, and profiles defined
//! in the root manifest and configurations.
//!
//! To start using a profile, most of the time you start from [`Profiles::new`],
//! which does the followings:
//!
//! - Create a `Profiles` by merging profiles from configs onto the profile
//!   from root manifest (see [`merge_config_profiles`]).
//! - Add built-in profiles onto it (see [`Profiles::add_root_profiles`]).
//! - Process profile inheritance for each profiles. (see [`Profiles::add_maker`]).
//!
//! Then you can query a [`Profile`] via [`Profiles::get_profile`], which respects
//! the profile overridden hierarchy described in below. The [`Profile`] you get
//! is basically an immutable struct containing the compiler flag presets.
//!
//! ## Profile overridden hierarchy
//!
//! Profile settings can be overridden for specific packages and build-time crates.
//! The precedence is explained in [`ProfileMaker`].
//! The algorithm happens within [`ProfileMaker::get_profile`].

use crate::core::Feature;
use crate::core::compiler::{CompileKind, CompileTarget, Unit};
use crate::core::dependency::Artifact;
use crate::core::resolver::features::FeaturesFor;
use crate::core::{PackageId, PackageIdSpec, PackageIdSpecQuery, Resolve, Target, Workspace};
use crate::util::interning::InternedString;
use crate::util::toml::validate_profile;
use crate::util::{CargoResult, GlobalContext, closest_msg, context};
use anyhow::{Context as _, bail};
use cargo_util_schemas::manifest::TomlTrimPaths;
use cargo_util_schemas::manifest::TomlTrimPathsValue;
use cargo_util_schemas::manifest::{
    ProfilePackageSpec, StringOrBool, TomlDebugInfo, TomlProfile, TomlProfiles,
};
use cargo_util_terminal::Shell;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::hash::Hash;
use std::{cmp, fmt, hash};

/// Collection of all profiles.
///
/// To get a specific [`Profile`], you usually create this and call [`get_profile`] then.
///
/// [`get_profile`]: Profiles::get_profile
#[derive(Clone, Debug)]
pub struct Profiles {
    /// Incremental compilation can be overridden globally via:
    /// - `CARGO_INCREMENTAL` environment variable.
    /// - `build.incremental` config value.
    incremental: Option<bool>,
    /// Map of profile name to directory name for that profile.
    dir_names: HashMap<InternedString, InternedString>,
    /// The profile makers. Key is the profile name.
    by_name: HashMap<InternedString, ProfileMaker>,
    /// The original profiles written by the user in the manifest and config.
    ///
    /// This is here to assist with error reporting, as the `ProfileMaker`
    /// values have the inherits chains all merged together.
    original_profiles: BTreeMap<InternedString, TomlProfile>,
    /// The profile the user requested to use.
    requested_profile: InternedString,
    /// The host target for rustc being used by this `Profiles`.
    rustc_host: InternedString,
}

impl Profiles {
    pub fn new(ws: &Workspace<'_>, requested_profile: InternedString) -> CargoResult<Profiles> {
        let gctx = ws.gctx();
        let incremental = match gctx.get_env_os("CARGO_INCREMENTAL") {
            Some(v) => Some(v == "1"),
            None => gctx.build_config()?.incremental,
        };
        let mut profiles = merge_config_profiles(ws, requested_profile)?;
        let rustc_host = ws.gctx().load_global_rustc(Some(ws))?.host;

        let mut profile_makers = Profiles {
            incremental,
            dir_names: Self::predefined_dir_names(),
            by_name: HashMap::new(),
            original_profiles: profiles.clone(),
            requested_profile,
            rustc_host,
        };

        let trim_paths_enabled = ws.unstable_features().is_enabled(Feature::trim_paths())
            || gctx.cli_unstable().trim_paths;
        Self::add_root_profiles(&mut profile_makers, &profiles, trim_paths_enabled);

        // Merge with predefined profiles.
        use std::collections::btree_map::Entry;
        for (predef_name, mut predef_prof) in Self::predefined_profiles().into_iter() {
            match profiles.entry(predef_name.into()) {
                Entry::Vacant(vac) => {
                    vac.insert(predef_prof);
                }
                Entry::Occupied(mut oc) => {
                    // Override predefined with the user-provided Toml.
                    let r = oc.get_mut();
                    predef_prof.merge(r);
                    *r = predef_prof;
                }
            }
        }

        for (name, profile) in &profiles {
            profile_makers.add_maker(*name, profile, &profiles)?;
        }
        // Verify that the requested profile is defined *somewhere*.
        // This simplifies the API (no need for CargoResult), and enforces
        // assumptions about how config profiles are loaded.
        profile_makers.get_profile_maker(&requested_profile)?;
        Ok(profile_makers)
    }

    /// Returns the hard-coded directory names for built-in profiles.
    fn predefined_dir_names() -> HashMap<InternedString, InternedString> {
        [
            ("dev".into(), "debug".into()),
            ("test".into(), "debug".into()),
            ("bench".into(), "release".into()),
        ]
        .into()
    }

    /// Initialize `by_name` with the two "root" profiles, `dev`, and
    /// `release` given the user's definition.
    fn add_root_profiles(
        profile_makers: &mut Profiles,
        profiles: &BTreeMap<InternedString, TomlProfile>,
        trim_paths_enabled: bool,
    ) {
        profile_makers.by_name.insert(
            "dev".into(),
            ProfileMaker::new(Profile::default_dev(), profiles.get("dev").cloned()),
        );

        profile_makers.by_name.insert(
            "release".into(),
            ProfileMaker::new(
                Profile::default_release(trim_paths_enabled),
                profiles.get("release").cloned(),
            ),
        );
    }

    /// Returns the built-in profiles (not including dev/release, which are
    /// "root" profiles).
    fn predefined_profiles() -> Vec<(&'static str, TomlProfile)> {
        vec![
            (
                "bench",
                TomlProfile {
                    inherits: Some(String::from("release")),
                    ..TomlProfile::default()
                },
            ),
            (
                "test",
                TomlProfile {
                    inherits: Some(String::from("dev")),
                    ..TomlProfile::default()
                },
            ),
            (
                "doc",
                TomlProfile {
                    inherits: Some(String::from("dev")),
                    ..TomlProfile::default()
                },
            ),
        ]
    }

    /// Creates a `ProfileMaker`, and inserts it into `self.by_name`.
    fn add_maker(
        &mut self,
        name: InternedString,
        profile: &TomlProfile,
        profiles: &BTreeMap<InternedString, TomlProfile>,
    ) -> CargoResult<()> {
        match &profile.dir_name {
            None => {}
            Some(dir_name) => {
                self.dir_names.insert(name, dir_name.into());
            }
        }

        // dev/release are "roots" and don't inherit.
        if name == "dev" || name == "release" {
            if profile.inherits.is_some() {
                bail!(
                    "`inherits` must not be specified in root profile `{}`",
                    name
                );
            }
            // Already inserted from `add_root_profiles`, no need to do anything.
            return Ok(());
        }

        // Keep track for inherits cycles.
        let mut set = HashSet::new();
        set.insert(name);
        let maker = self.process_chain(name, profile, &mut set, profiles)?;
        self.by_name.insert(name, maker);
        Ok(())
    }

    /// Build a `ProfileMaker` by recursively following the `inherits` setting.
    ///
    /// * `name`: The name of the profile being processed.
    /// * `profile`: The TOML profile being processed.
    /// * `set`: Set of profiles that have been visited, used to detect cycles.
    /// * `profiles`: Map of all TOML profiles.
    ///
    /// Returns a `ProfileMaker` to be used for the given named profile.
    fn process_chain(
        &mut self,
        name: InternedString,
        profile: &TomlProfile,
        set: &mut HashSet<InternedString>,
        profiles: &BTreeMap<InternedString, TomlProfile>,
    ) -> CargoResult<ProfileMaker> {
        let mut maker = match &profile.inherits {
            Some(inherits_name) if inherits_name == "dev" || inherits_name == "release" => {
                // These are the root profiles added in `add_root_profiles`.
                self.get_profile_maker(&inherits_name).unwrap().clone()
            }
            Some(inherits_name) => {
                let inherits_name = inherits_name.into();
                if !set.insert(inherits_name) {
                    bail!(
                        "profile inheritance loop detected with profile `{}` inheriting `{}`",
                        name,
                        inherits_name
                    );
                }

                match profiles.get(&inherits_name) {
                    None => {
                        bail!(
                            "profile `{}` inherits from `{}`, but that profile is not defined",
                            name,
                            inherits_name
                        );
                    }
                    Some(parent) => self.process_chain(inherits_name, parent, set, profiles)?,
                }
            }
            None => {
                bail!(
                    "profile `{}` is missing an `inherits` directive \
                     (`inherits` is required for all profiles except `dev` or `release`)",
                    name
                );
            }
        };
        match &mut maker.toml {
            Some(toml) => toml.merge(profile),
            None => maker.toml = Some(profile.clone()),
        };
        Ok(maker)
    }

    /// Retrieves the profile for a target.
    /// `is_member` is whether or not this package is a member of the
    /// workspace.
    pub fn get_profile(
        &self,
        pkg_id: PackageId,
        is_member: bool,
        is_local: bool,
        unit_for: UnitFor,
        kind: CompileKind,
    ) -> Profile {
        let maker = self.get_profile_maker(&self.requested_profile).unwrap();
        let mut profile = maker.get_profile(Some(pkg_id), is_member, unit_for.is_for_host());

        // Dealing with `panic=abort` and `panic=unwind` requires some special
        // treatment. Be sure to process all the various options here.
        match unit_for.panic_setting() {
            PanicSetting::AlwaysUnwind => profile.panic = PanicStrategy::Unwind,
            PanicSetting::ReadProfile => {}
        }

        // Default macOS debug information to being stored in the "unpacked"
        // split-debuginfo format. At the time of this writing that's the only
        // platform which has a stable `-Csplit-debuginfo` option for rustc,
        // and it's typically much faster than running `dsymutil` on all builds
        // in incremental cases.
        if profile.debuginfo.is_turned_on() && profile.split_debuginfo.is_none() {
            let target = match &kind {
                CompileKind::Host => self.rustc_host.as_str(),
                CompileKind::Target(target) => target.short_name(),
            };
            if target.contains("-apple-") {
                profile.split_debuginfo = Some("unpacked".into());
            }
        }

        // Incremental can be globally overridden.
        if let Some(v) = self.incremental {
            profile.incremental = v;
        }

        // Only enable incremental compilation for sources the user can
        // modify (aka path sources). For things that change infrequently,
        // non-incremental builds yield better performance in the compiler
        // itself (aka crates.io / git dependencies)
        //
        // (see also https://github.com/rust-lang/cargo/issues/3972)
        if !is_local {
            profile.incremental = false;
        }
        profile.name = self.requested_profile;
        profile
    }

    /// The profile for *running* a `build.rs` script is only used for setting
    /// a few environment variables. To ensure proper de-duplication of the
    /// running `Unit`, this uses a stripped-down profile (so that unrelated
    /// profile flags don't cause `build.rs` to needlessly run multiple
    /// times).
    pub fn get_profile_run_custom_build(&self, for_unit_profile: &Profile) -> Profile {
        let mut result = Profile::default();
        result.name = for_unit_profile.name;
        result.root = for_unit_profile.root;
        result.debuginfo = for_unit_profile.debuginfo;
        result.opt_level = for_unit_profile.opt_level;
        result.debug_assertions = for_unit_profile.debug_assertions;
        result.trim_paths = for_unit_profile.trim_paths.clone();
        result
    }

    /// This returns the base profile. This is currently used for the
    /// `[Finished]` line. It is not entirely accurate, since it doesn't
    /// select for the package that was actually built.
    pub fn base_profile(&self) -> Profile {
        let profile_name = self.requested_profile;
        let maker = self.get_profile_maker(&profile_name).unwrap();
        maker.get_profile(None, /*is_member*/ true, /*is_for_host*/ false)
    }

    /// Gets the directory name for a profile, like `debug` or `release`.
    pub fn get_dir_name(&self) -> InternedString {
        *self
            .dir_names
            .get(&self.requested_profile)
            .unwrap_or(&self.requested_profile)
    }

    /// Used to check for overrides for non-existing packages.
    pub fn validate_packages(
        &self,
        profiles: Option<&TomlProfiles>,
        shell: &mut Shell,
        resolve: &Resolve,
    ) -> CargoResult<()> {
        for (name, profile) in &self.by_name {
            // If the user did not specify an override, skip this. This is here
            // to avoid generating errors for inherited profiles which don't
            // specify package overrides. The `by_name` profile has had the inherits
            // chain merged, so we need to look at the original source to check
            // if an override was specified.
            if self
                .original_profiles
                .get(name)
                .and_then(|orig| orig.package.as_ref())
                .is_none()
            {
                continue;
            }
            let found = validate_packages_unique(resolve, name, &profile.toml)?;
            // We intentionally do not validate unmatched packages for config
            // profiles, in case they are defined in a central location. This
            // iterates over the manifest profiles only.
            if let Some(profiles) = profiles {
                if let Some(toml_profile) = profiles.get(name) {
                    validate_packages_unmatched(shell, resolve, name, toml_profile, &found)?;
                }
            }
        }
        Ok(())
    }

    /// Returns the profile maker for the given profile name.
    fn get_profile_maker(&self, name: &str) -> CargoResult<&ProfileMaker> {
        self.by_name
            .get(name)
            .ok_or_else(|| anyhow::format_err!("profile `{}` is not defined", name))
    }

    /// Returns an iterator over all profile names known to Cargo.
    pub fn profile_names(&self) -> impl Iterator<Item = InternedString> + '_ {
        self.by_name.keys().copied()
    }
}

/// An object used for handling the profile hierarchy.
///
/// The precedence of profiles are (first one wins):
///
/// - Profiles in `.cargo/config` files (using same order as below).
/// - `[profile.dev.package.name]` -- a named package.
/// - `[profile.dev.package."*"]` -- this cannot apply to workspace members.
/// - `[profile.dev.build-override]` -- this can only apply to `build.rs` scripts
///   and their dependencies.
/// - `[profile.dev]`
/// - Default (hard-coded) values.
#[derive(Debug, Clone)]
struct ProfileMaker {
    /// The starting, hard-coded defaults for the profile.
    default: Profile,
    /// The TOML profile defined in `Cargo.toml` or config.
    ///
    /// This is None if the user did not specify one, in which case the
    /// `default` is used. Note that the built-in defaults for test/bench/doc
    /// always set this since they need to declare the `inherits` value.
    toml: Option<TomlProfile>,
}

impl ProfileMaker {
    /// Creates a new `ProfileMaker`.
    ///
    /// Note that this does not process `inherits`, the caller is responsible for that.
    fn new(default: Profile, toml: Option<TomlProfile>) -> ProfileMaker {
        ProfileMaker { default, toml }
    }

    /// Generates a new `Profile`.
    fn get_profile(
        &self,
        pkg_id: Option<PackageId>,
        is_member: bool,
        is_for_host: bool,
    ) -> Profile {
        let mut profile = self.default.clone();

        // First apply profile-specific settings, things like
        // `[profile.release]`
        if let Some(toml) = &self.toml {
            merge_profile(&mut profile, toml);
        }

        // Next start overriding those settings. First comes build dependencies
        // which default to opt-level 0...
        if is_for_host {
            // For-host units are things like procedural macros, build scripts, and
            // their dependencies. For these units most projects simply want them
            // to compile quickly and the runtime doesn't matter too much since
            // they tend to process very little data. For this reason we default
            // them to a "compile as quickly as possible" mode which for now means
            // basically turning down the optimization level and avoid limiting
            // codegen units. This ensures that we spend little time optimizing as
            // well as enabling parallelism by not constraining codegen units.
            profile.opt_level = "0".into();
            profile.codegen_units = None;

            // For build dependencies, we usually don't need debuginfo, and
            // removing it will compile faster. However, that can conflict with
            // a unit graph optimization, reusing units that are shared between
            // build dependencies and runtime dependencies: when the runtime
            // target is the same as the build host, we only need to build a
            // dependency once and reuse the results, instead of building twice.
            // We defer the choice of the debuginfo level until we can check if
            // a unit is shared. If that's the case, we'll use the deferred value
            // below so the unit can be reused, otherwise we can avoid emitting
            // the unit's debuginfo.
            profile.debuginfo = DebugInfo::Deferred(profile.debuginfo.into_inner());
        }
        // ... and next comes any other sorts of overrides specified in
        // profiles, such as `[profile.release.build-override]` or
        // `[profile.release.package.foo]`
        if let Some(toml) = &self.toml {
            merge_toml_overrides(pkg_id, is_member, is_for_host, &mut profile, toml);
        }
        profile
    }
}

/// Merge package and build overrides from the given TOML profile into the given `Profile`.
fn merge_toml_overrides(
    pkg_id: Option<PackageId>,
    is_member: bool,
    is_for_host: bool,
    profile: &mut Profile,
    toml: &TomlProfile,
) {
    if is_for_host {
        if let Some(build_override) = &toml.build_override {
            merge_profile(profile, build_override);
        }
    }
    if let Some(overrides) = toml.package.as_ref() {
        if !is_member {
            if let Some(all) = overrides.get(&ProfilePackageSpec::All) {
                merge_profile(profile, all);
            }
        }
        if let Some(pkg_id) = pkg_id {
            let mut matches = overrides
                .iter()
                .filter_map(|(key, spec_profile)| match *key {
                    ProfilePackageSpec::All => None,
                    ProfilePackageSpec::Spec(ref s) => {
                        if s.matches(pkg_id) {
                            Some(spec_profile)
                        } else {
                            None
                        }
                    }
                });
            if let Some(spec_profile) = matches.next() {
                merge_profile(profile, spec_profile);
                // `validate_packages` should ensure that there are
                // no additional matches.
                assert!(
                    matches.next().is_none(),
                    "package `{}` matched multiple package profile overrides",
                    pkg_id
                );
            }
        }
    }
}

/// Merge the given TOML profile into the given `Profile`.
///
/// Does not merge overrides (see `merge_toml_overrides`).
fn merge_profile(profile: &mut Profile, toml: &TomlProfile) {
    if let Some(ref opt_level) = toml.opt_level {
        profile.opt_level = opt_level.0.as_str().into();
    }
    match toml.lto {
        Some(StringOrBool::Bool(b)) => profile.lto = Lto::Bool(b),
        Some(StringOrBool::String(ref n)) if is_off(n.as_str()) => profile.lto = Lto::Off,
        Some(StringOrBool::String(ref n)) => profile.lto = Lto::Named(n.into()),
        None => {}
    }
    if toml.codegen_backend.is_some() {
        profile.codegen_backend = toml.codegen_backend.as_ref().map(InternedString::from);
    }
    if toml.codegen_units.is_some() {
        profile.codegen_units = toml.codegen_units;
    }
    if let Some(debuginfo) = toml.debug {
        profile.debuginfo = DebugInfo::Resolved(debuginfo);
    }
    if let Some(debug_assertions) = toml.debug_assertions {
        profile.debug_assertions = debug_assertions;
    }
    if let Some(split_debuginfo) = &toml.split_debuginfo {
        profile.split_debuginfo = Some(split_debuginfo.into());
    }
    if let Some(rpath) = toml.rpath {
        profile.rpath = rpath;
    }
    if let Some(panic) = &toml.panic {
        profile.panic = match panic.as_str() {
            "unwind" => PanicStrategy::Unwind,
            "abort" => PanicStrategy::Abort,
            "immediate-abort" => PanicStrategy::ImmediateAbort,
            // This should be validated in TomlProfile::validate
            _ => panic!("Unexpected panic setting `{}`", panic),
        };
    }
    if let Some(overflow_checks) = toml.overflow_checks {
        profile.overflow_checks = overflow_checks;
    }
    if let Some(incremental) = toml.incremental {
        profile.incremental = incremental;
    }
    if let Some(flags) = &toml.rustflags {
        profile.rustflags = flags.iter().map(InternedString::from).collect();
    }
    if let Some(trim_paths) = &toml.trim_paths {
        profile.trim_paths = Some(trim_paths.clone());
    }
    if let Some(hint_mostly_unused) = toml.hint_mostly_unused {
        profile.hint_mostly_unused = Some(hint_mostly_unused);
    }
    profile.strip = match toml.strip {
        Some(StringOrBool::Bool(true)) => Strip::Resolved(StripInner::Named("symbols".into())),
        Some(StringOrBool::Bool(false)) => Strip::Resolved(StripInner::None),
        Some(StringOrBool::String(ref n)) if n.as_str() == "none" => {
            Strip::Resolved(StripInner::None)
        }
        Some(StringOrBool::String(ref n)) => Strip::Resolved(StripInner::Named(n.into())),
        None => Strip::Deferred(StripInner::None),
    };
}

/// The root profile (dev/release).
///
/// This is currently only used for the `PROFILE` env var for build scripts
/// for backwards compatibility. We should probably deprecate `PROFILE` and
/// encourage using things like `DEBUG` and `OPT_LEVEL` instead.
#[derive(Clone, Copy, Eq, PartialOrd, Ord, PartialEq, Debug)]
pub enum ProfileRoot {
    Release,
    Debug,
}

/// Profile settings used to determine which compiler flags to use for a
/// target.
#[derive(Clone, Eq, PartialOrd, Ord, serde::Serialize)]
pub struct Profile {
    pub name: InternedString,
    pub opt_level: InternedString,
    #[serde(skip)] // named profiles are unstable
    pub root: ProfileRoot,
    pub lto: Lto,
    // `None` means use rustc default.
    pub codegen_backend: Option<InternedString>,
    // `None` means use rustc default.
    pub codegen_units: Option<u32>,
    pub debuginfo: DebugInfo,
    pub split_debuginfo: Option<InternedString>,
    pub debug_assertions: bool,
    pub overflow_checks: bool,
    pub rpath: bool,
    pub incremental: bool,
    pub panic: PanicStrategy,
    pub strip: Strip,
    #[serde(skip_serializing_if = "Vec::is_empty")] // remove when `rustflags` is stabilized
    // Note that `rustflags` is used for the cargo-feature `profile_rustflags`
    pub rustflags: Vec<InternedString>,
    // remove when `-Ztrim-paths` is stabilized
    #[serde(skip_serializing_if = "Option::is_none")]
    pub trim_paths: Option<TomlTrimPaths>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hint_mostly_unused: Option<bool>,
}

impl Default for Profile {
    fn default() -> Profile {
        Profile {
            name: "".into(),
            opt_level: "0".into(),
            root: ProfileRoot::Debug,
            lto: Lto::Bool(false),
            codegen_backend: None,
            codegen_units: None,
            debuginfo: DebugInfo::Resolved(TomlDebugInfo::None),
            debug_assertions: false,
            split_debuginfo: None,
            overflow_checks: false,
            rpath: false,
            incremental: false,
            panic: PanicStrategy::Unwind,
            strip: Strip::Deferred(StripInner::None),
            rustflags: vec![],
            trim_paths: None,
            hint_mostly_unused: None,
        }
    }
}

compact_debug! {
    impl fmt::Debug for Profile {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            let (default, default_name) = match self.name.as_str() {
                "dev" => (Profile::default_dev(), "default_dev()"),
                "release" => (Profile::default_release(false), "default_release()"),
                _ => (Profile::default(), "default()"),
            };
            [debug_the_fields(
                name
                opt_level
                lto
                root
                codegen_backend
                codegen_units
                debuginfo
                split_debuginfo
                debug_assertions
                overflow_checks
                rpath
                incremental
                panic
                strip
                rustflags
                trim_paths
                hint_mostly_unused
            )]
        }
    }
}

impl fmt::Display for Profile {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Profile({})", self.name)
    }
}

impl hash::Hash for Profile {
    fn hash<H>(&self, state: &mut H)
    where
        H: hash::Hasher,
    {
        self.comparable().hash(state);
    }
}

impl cmp::PartialEq for Profile {
    fn eq(&self, other: &Self) -> bool {
        self.comparable() == other.comparable()
    }
}

impl Profile {
    /// Returns a built-in `dev` profile.
    fn default_dev() -> Profile {
        Profile {
            name: "dev".into(),
            root: ProfileRoot::Debug,
            debuginfo: DebugInfo::Resolved(TomlDebugInfo::Full),
            debug_assertions: true,
            overflow_checks: true,
            incremental: true,
            ..Profile::default()
        }
    }

    /// Returns a built-in `release` profile.
    fn default_release(trim_paths_enabled: bool) -> Profile {
        let trim_paths = trim_paths_enabled.then(|| TomlTrimPathsValue::Object.into());
        Profile {
            name: "release".into(),
            root: ProfileRoot::Release,
            opt_level: "3".into(),
            trim_paths,
            ..Profile::default()
        }
    }

    /// Compares all fields except `name`, which doesn't affect compilation.
    /// This is necessary for `Unit` deduplication for things like "test" and
    /// "dev" which are essentially the same.
    fn comparable(&self) -> impl Hash + Eq + '_ {
        (
            self.opt_level,
            self.lto,
            self.codegen_backend,
            self.codegen_units,
            self.debuginfo,
            self.split_debuginfo,
            self.debug_assertions,
            self.overflow_checks,
            self.rpath,
            (self.incremental, self.panic, self.strip),
            &self.rustflags,
            &self.trim_paths,
        )
    }
}

/// The debuginfo level setting.
///
/// This is semantically a [`TomlDebugInfo`], and should be used as so via the
/// [`DebugInfo::into_inner`] method for all intents and purposes.
///
/// Internally, it's used to model a debuginfo level whose value can be deferred
/// for optimization purposes: host dependencies usually don't need the same
/// level as target dependencies. For dependencies that are shared between the
/// two however, that value also affects reuse: different debuginfo levels would
/// cause to build a unit twice. By deferring the choice until we know
/// whether to choose the optimized value or the default value, we can make sure
/// the unit is only built once and the unit graph is still optimized.
#[derive(Debug, Copy, Clone, serde::Serialize)]
#[serde(untagged)]
pub enum DebugInfo {
    /// A debuginfo level that is fixed and will not change.
    ///
    /// This can be set by a profile, user, or default value.
    Resolved(TomlDebugInfo),
    /// For internal purposes: a deferred debuginfo level that can be optimized
    /// away, but has this value otherwise.
    ///
    /// Behaves like `Resolved` in all situations except for the default build
    /// dependencies profile: whenever a build dependency is not shared with
    /// runtime dependencies, this level is weakened to a lower level that is
    /// faster to build (see [`DebugInfo::weaken`]).
    ///
    /// In all other situations, this level value will be the one to use.
    Deferred(TomlDebugInfo),
}

impl DebugInfo {
    /// The main way to interact with this debuginfo level, turning it into a [`TomlDebugInfo`].
    pub fn into_inner(self) -> TomlDebugInfo {
        match self {
            DebugInfo::Resolved(v) | DebugInfo::Deferred(v) => v,
        }
    }

    /// Returns true if any debuginfo will be generated. Helper
    /// for a common operation on the usual `Option` representation.
    pub(crate) fn is_turned_on(&self) -> bool {
        !matches!(self.into_inner(), TomlDebugInfo::None)
    }

    pub(crate) fn is_deferred(&self) -> bool {
        matches!(self, DebugInfo::Deferred(_))
    }

    /// Force the deferred, preferred, debuginfo level to a finalized explicit value.
    pub(crate) fn finalize(self) -> Self {
        match self {
            DebugInfo::Deferred(v) => DebugInfo::Resolved(v),
            _ => self,
        }
    }

    /// Reset to the lowest level: no debuginfo.
    pub(crate) fn weaken(self) -> Self {
        DebugInfo::Resolved(TomlDebugInfo::None)
    }
}

impl PartialEq for DebugInfo {
    fn eq(&self, other: &DebugInfo) -> bool {
        self.into_inner().eq(&other.into_inner())
    }
}

impl Eq for DebugInfo {}

impl Hash for DebugInfo {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.into_inner().hash(state);
    }
}

impl PartialOrd for DebugInfo {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        self.into_inner().partial_cmp(&other.into_inner())
    }
}

impl Ord for DebugInfo {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.into_inner().cmp(&other.into_inner())
    }
}

/// The link-time-optimization setting.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub enum Lto {
    /// Explicitly no LTO, disables thin-LTO.
    Off,
    /// True = "Fat" LTO
    /// False = rustc default (no args), currently "thin LTO"
    Bool(bool),
    /// Named LTO settings like "thin".
    Named(InternedString),
}

impl serde::ser::Serialize for Lto {
    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
    {
        match self {
            Lto::Off => "off".serialize(s),
            Lto::Bool(b) => b.to_string().serialize(s),
            Lto::Named(n) => n.serialize(s),
        }
    }
}

/// The `panic` setting.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, PartialOrd, Ord, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum PanicStrategy {
    Unwind,
    Abort,
    ImmediateAbort,
}

impl fmt::Display for PanicStrategy {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            PanicStrategy::Unwind => "unwind",
            PanicStrategy::Abort => "abort",
            PanicStrategy::ImmediateAbort => "immediate-abort",
        }
        .fmt(f)
    }
}

#[derive(
    Clone, Copy, PartialEq, Eq, Debug, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub enum StripInner {
    /// Don't remove any symbols
    None,
    /// Named Strip settings
    Named(InternedString),
}

impl fmt::Display for StripInner {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            StripInner::None => "none",
            StripInner::Named(s) => s.as_str(),
        }
        .fmt(f)
    }
}

/// The setting for choosing which symbols to strip.
///
/// This is semantically a [`StripInner`], and should be used as so via the
/// [`Strip::into_inner`] method for all intents and purposes.
///
/// Internally, it's used to model a strip option whose value can be deferred
/// for optimization purposes: when no package being compiled requires debuginfo,
/// then we can strip debuginfo to remove pre-existing debug symbols from the
/// standard library.
#[derive(Clone, Copy, Debug, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Strip {
    /// A strip option that is fixed and will not change.
    Resolved(StripInner),
    /// A strip option that might be overridden by Cargo for optimization
    /// purposes.
    Deferred(StripInner),
}

impl Strip {
    /// The main way to interact with this strip option, turning it into a [`StripInner`].
    pub fn into_inner(self) -> StripInner {
        match self {
            Strip::Resolved(v) | Strip::Deferred(v) => v,
        }
    }

    pub(crate) fn is_deferred(&self) -> bool {
        matches!(self, Strip::Deferred(_))
    }

    /// Reset to stripping debuginfo.
    pub(crate) fn strip_debuginfo(self) -> Self {
        Strip::Resolved(StripInner::Named("debuginfo".into()))
    }
}

impl PartialEq for Strip {
    fn eq(&self, other: &Self) -> bool {
        self.into_inner().eq(&other.into_inner())
    }
}

impl Hash for Strip {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.into_inner().hash(state);
    }
}

impl PartialOrd for Strip {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        self.into_inner().partial_cmp(&other.into_inner())
    }
}

impl Ord for Strip {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.into_inner().cmp(&other.into_inner())
    }
}

/// Flags used in creating `Unit`s to indicate the purpose for the target, and
/// to ensure the target's dependencies have the correct settings.
///
/// This means these are passed down from the root of the dependency tree to apply
/// to most child dependencies.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct UnitFor {
    /// A target for `build.rs` or any of its dependencies, or a proc-macro or
    /// any of its dependencies. This enables `build-override` profiles for
    /// these targets.
    ///
    /// An invariant is that if `host_features` is true, `host` must be true.
    ///
    /// Note that this is `true` for `RunCustomBuild` units, even though that
    /// unit should *not* use build-override profiles. This is a bit of a
    /// special case. When computing the `RunCustomBuild` unit, it manually
    /// uses the `get_profile_run_custom_build` method to get the correct
    /// profile information for the unit. `host` needs to be true so that all
    /// of the dependencies of that `RunCustomBuild` unit have this flag be
    /// sticky (and forced to `true` for all further dependencies) — which is
    /// the whole point of `UnitFor`.
    host: bool,
    /// A target for a build dependency or proc-macro (or any of its
    /// dependencies). This is used for computing features of build
    /// dependencies and proc-macros independently of other dependency kinds.
    ///
    /// The subtle difference between this and `host` is that the build script
    /// for a non-host package sets this to `false` because it wants the
    /// features of the non-host package (whereas `host` is true because the
    /// build script is being built for the host). `host_features` becomes
    /// `true` for build-dependencies or proc-macros, or any of their
    /// dependencies. For example, with this dependency tree:
    ///
    /// ```text
    /// foo
    /// ├── foo build.rs
    /// │   └── shared_dep (BUILD dependency)
    /// │       └── shared_dep build.rs
    /// └── shared_dep (Normal dependency)
    ///     └── shared_dep build.rs
    /// ```
    ///
    /// In this example, `foo build.rs` is `HOST=true`, `HOST_FEATURES=false`.
    /// This is so that `foo build.rs` gets the profile settings for build
    /// scripts (`HOST=true`) and features of foo (`HOST_FEATURES=false`) because
    /// build scripts need to know which features their package is being built
    /// with.
    ///
    /// But in the case of `shared_dep`, when built as a build dependency,
    /// both flags are true (it only wants the build-dependency features).
    /// When `shared_dep` is built as a normal dependency, then `shared_dep
    /// build.rs` is `HOST=true`, `HOST_FEATURES=false` for the same reasons that
    /// foo's build script is set that way.
    host_features: bool,
    /// How Cargo processes the `panic` setting or profiles.
    panic_setting: PanicSetting,

    /// The compile kind of the root unit for which artifact dependencies are built.
    /// This is required particularly for the `target = "target"` setting of artifact
    /// dependencies which mean to inherit the `--target` specified on the command-line.
    /// However, that is a multi-value argument and root units are already created to
    /// reflect one unit per --target. Thus we have to build one artifact with the
    /// correct target for each of these trees.
    /// Note that this will always be set as we don't initially know if there are
    /// artifacts that make use of it.
    root_compile_kind: CompileKind,

    /// This is only set for artifact dependencies which have their
    /// `<target-triple>|target` set.
    /// If so, this information is used as part of the key for resolving their features,
    /// allowing for target-dependent feature resolution within the entire dependency tree.
    /// Note that this target corresponds to the target used to build the units in that
    /// dependency tree, too, but this copy of it is specifically used for feature lookup.
    artifact_target_for_features: Option<CompileTarget>,
}

/// How Cargo processes the `panic` setting or profiles.
///
/// This is done to handle test/benches inheriting from dev/release,
/// as well as forcing `for_host` units to always unwind.
/// It also interacts with [`-Z panic-abort-tests`].
///
/// [`-Z panic-abort-tests`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#panic-abort-tests
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
enum PanicSetting {
    /// Used to force a unit to always be compiled with the `panic=unwind`
    /// strategy, notably for build scripts, proc macros, etc.
    AlwaysUnwind,

    /// Indicates that this unit will read its `profile` setting and use
    /// whatever is configured there.
    ReadProfile,
}

impl UnitFor {
    /// A unit for a normal target/dependency (i.e., not custom build,
    /// proc macro/plugin, or test/bench).
    pub fn new_normal(root_compile_kind: CompileKind) -> UnitFor {
        UnitFor {
            host: false,
            host_features: false,
            panic_setting: PanicSetting::ReadProfile,
            root_compile_kind,
            artifact_target_for_features: None,
        }
    }

    /// A unit for a custom build script or proc-macro or its dependencies.
    ///
    /// The `host_features` parameter is whether or not this is for a build
    /// dependency or proc-macro (something that requires being built "on the
    /// host"). Build scripts for non-host units should use `false` because
    /// they want to use the features of the package they are running for.
    pub fn new_host(host_features: bool, root_compile_kind: CompileKind) -> UnitFor {
        UnitFor {
            host: true,
            host_features,
            // Force build scripts to always use `panic=unwind` for now to
            // maximally share dependencies with procedural macros.
            panic_setting: PanicSetting::AlwaysUnwind,
            root_compile_kind,
            artifact_target_for_features: None,
        }
    }

    /// A unit for a compiler plugin or their dependencies.
    pub fn new_compiler(root_compile_kind: CompileKind) -> UnitFor {
        UnitFor {
            host: false,
            // The feature resolver doesn't know which dependencies are
            // plugins, so for now plugins don't split features. Since plugins
            // are mostly deprecated, just leave this as false.
            host_features: false,
            // Force plugins to use `panic=abort` so panics in the compiler do
            // not abort the process but instead end with a reasonable error
            // message that involves catching the panic in the compiler.
            panic_setting: PanicSetting::AlwaysUnwind,
            root_compile_kind,
            artifact_target_for_features: None,
        }
    }

    /// A unit for a test/bench target or their dependencies.
    ///
    /// Note that `config` is taken here for unstable CLI features to detect
    /// whether `panic=abort` is supported for tests. Historical versions of
    /// rustc did not support this, but newer versions do with an unstable
    /// compiler flag.
    pub fn new_test(gctx: &GlobalContext, root_compile_kind: CompileKind) -> UnitFor {
        UnitFor {
            host: false,
            host_features: false,
            // We're testing out an unstable feature (`-Zpanic-abort-tests`)
            // which inherits the panic setting from the dev/release profile
            // (basically avoid recompiles) but historical defaults required
            // that we always unwound.
            panic_setting: if gctx.cli_unstable().panic_abort_tests {
                PanicSetting::ReadProfile
            } else {
                PanicSetting::AlwaysUnwind
            },
            root_compile_kind,
            artifact_target_for_features: None,
        }
    }

    /// This is a special case for unit tests of a proc-macro.
    ///
    /// Proc-macro unit tests are forced to be run on the host.
    pub fn new_host_test(gctx: &GlobalContext, root_compile_kind: CompileKind) -> UnitFor {
        let mut unit_for = UnitFor::new_test(gctx, root_compile_kind);
        unit_for.host = true;
        unit_for.host_features = true;
        unit_for
    }

    /// Returns a new copy updated based on the target dependency.
    ///
    /// This is where the magic happens that the `host`/`host_features` settings
    /// transition in a sticky fashion. As the dependency graph is being
    /// built, once those flags are set, they stay set for the duration of
    /// that portion of tree.
    pub fn with_dependency(
        self,
        parent: &Unit,
        dep_target: &Target,
        root_compile_kind: CompileKind,
    ) -> UnitFor {
        // A build script or proc-macro transitions this to being built for the host.
        let dep_for_host = dep_target.for_host();
        // This is where feature decoupling of host versus target happens.
        //
        // Once host features are desired, they are always desired.
        //
        // A proc-macro should always use host features.
        //
        // Dependencies of a build script should use host features (subtle
        // point: the build script itself does *not* use host features, that's
        // why the parent is checked here, and not the dependency).
        let host_features =
            self.host_features || parent.target.is_custom_build() || dep_target.proc_macro();
        // Build scripts and proc macros, and all of their dependencies are
        // AlwaysUnwind.
        let panic_setting = if dep_for_host {
            PanicSetting::AlwaysUnwind
        } else {
            self.panic_setting
        };
        let artifact_target_for_features =
            // build.rs and proc-macros are always for host.
            if dep_target.proc_macro() || parent.target.is_custom_build() {
                None
            } else {
                self.artifact_target_for_features
            };
        UnitFor {
            host: self.host || dep_for_host,
            host_features,
            panic_setting,
            root_compile_kind,
            artifact_target_for_features,
        }
    }

    pub fn for_custom_build(self) -> UnitFor {
        UnitFor {
            host: true,
            host_features: self.host_features,
            // Force build scripts to always use `panic=unwind` for now to
            // maximally share dependencies with procedural macros.
            panic_setting: PanicSetting::AlwaysUnwind,
            root_compile_kind: self.root_compile_kind,
            artifact_target_for_features: self.artifact_target_for_features,
        }
    }

    /// Set the artifact compile target for use in features using the given `artifact`.
    pub(crate) fn with_artifact_features(mut self, artifact: &Artifact) -> UnitFor {
        self.artifact_target_for_features = artifact.target().and_then(|t| t.to_compile_target());
        self
    }

    /// Set the artifact compile target as determined by a resolved compile target. This is used if `target = "target"`.
    pub(crate) fn with_artifact_features_from_resolved_compile_kind(
        mut self,
        kind: Option<CompileKind>,
    ) -> UnitFor {
        self.artifact_target_for_features = kind.and_then(|kind| match kind {
            CompileKind::Host => None,
            CompileKind::Target(triple) => Some(triple),
        });
        self
    }

    /// Returns `true` if this unit is for a build script or any of its
    /// dependencies, or a proc macro or any of its dependencies.
    pub fn is_for_host(&self) -> bool {
        self.host
    }

    pub fn is_for_host_features(&self) -> bool {
        self.host_features
    }

    /// Returns how `panic` settings should be handled for this profile
    fn panic_setting(&self) -> PanicSetting {
        self.panic_setting
    }

    /// We might contain a parent artifact compile kind for features already, but will
    /// gladly accept the one of this dependency as an override as it defines how
    /// the artifact is built.
    /// If we are an artifact but don't specify a `target`, we assume the default
    /// compile kind that is suitable in this situation.
    pub(crate) fn map_to_features_for(&self, dep_artifact: Option<&Artifact>) -> FeaturesFor {
        FeaturesFor::from_for_host_or_artifact_target(
            self.is_for_host_features(),
            match dep_artifact {
                Some(artifact) => artifact
                    .target()
                    .and_then(|t| t.to_resolved_compile_target(self.root_compile_kind)),
                None => self.artifact_target_for_features,
            },
        )
    }

    pub(crate) fn root_compile_kind(&self) -> CompileKind {
        self.root_compile_kind
    }
}

/// Takes the manifest profiles, and overlays the config profiles on-top.
///
/// Returns a new copy of the profile map with all the mergers complete.
fn merge_config_profiles(
    ws: &Workspace<'_>,
    requested_profile: InternedString,
) -> CargoResult<BTreeMap<InternedString, TomlProfile>> {
    let mut profiles = match ws.profiles() {
        Some(profiles) => profiles
            .get_all()
            .iter()
            .map(|(k, v)| (InternedString::new(k), v.clone()))
            .collect(),
        None => BTreeMap::new(),
    };
    // Set of profile names to check if defined in config only.
    let mut check_to_add = HashSet::new();
    check_to_add.insert(requested_profile);
    // Merge config onto manifest profiles.
    for (name, profile) in &mut profiles {
        if let Some(config_profile) = get_config_profile(ws, name)? {
            profile.merge(&config_profile);
        }
        if let Some(inherits) = &profile.inherits {
            check_to_add.insert(inherits.into());
        }
    }
    // Add the built-in profiles. This is important for things like `cargo
    // test` which implicitly use the "dev" profile for dependencies.
    for name in ["dev", "release", "test", "bench"] {
        check_to_add.insert(name.into());
    }
    // Add config-only profiles.
    // Need to iterate repeatedly to get all the inherits values.
    let mut current = HashSet::new();
    while !check_to_add.is_empty() {
        std::mem::swap(&mut current, &mut check_to_add);
        for name in current.drain() {
            if !profiles.contains_key(name.as_str()) {
                if let Some(config_profile) = get_config_profile(ws, &name)? {
                    if let Some(inherits) = &config_profile.inherits {
                        check_to_add.insert(inherits.into());
                    }
                    profiles.insert(name, config_profile);
                }
            }
        }
    }
    Ok(profiles)
}

/// Helper for fetching a profile from config.
fn get_config_profile(ws: &Workspace<'_>, name: &str) -> CargoResult<Option<TomlProfile>> {
    let profile: Option<context::Value<TomlProfile>> =
        ws.gctx().get(&format!("profile.{}", name))?;
    let Some(profile) = profile else {
        return Ok(None);
    };
    let mut warnings = Vec::new();
    validate_profile(
        &profile.val,
        name,
        ws.gctx().cli_unstable(),
        ws.unstable_features(),
        &mut warnings,
    )
    .with_context(|| {
        format!(
            "config profile `{}` is not valid (defined in `{}`)",
            name, profile.definition
        )
    })?;
    for warning in warnings {
        ws.gctx().shell().warn(warning)?;
    }
    Ok(Some(profile.val))
}

/// Validate that a package does not match multiple package override specs.
///
/// For example `[profile.dev.package.bar]` and `[profile.dev.package."bar:0.5.0"]`
/// would both match `bar:0.5.0` which would be ambiguous.
fn validate_packages_unique(
    resolve: &Resolve,
    name: &str,
    toml: &Option<TomlProfile>,
) -> CargoResult<HashSet<PackageIdSpec>> {
    let Some(toml) = toml else {
        return Ok(HashSet::new());
    };
    let Some(overrides) = toml.package.as_ref() else {
        return Ok(HashSet::new());
    };
    // Verify that a package doesn't match multiple spec overrides.
    let mut found = HashSet::new();
    for pkg_id in resolve.iter() {
        let matches: Vec<&PackageIdSpec> = overrides
            .keys()
            .filter_map(|key| match *key {
                ProfilePackageSpec::All => None,
                ProfilePackageSpec::Spec(ref spec) => {
                    if spec.matches(pkg_id) {
                        Some(spec)
                    } else {
                        None
                    }
                }
            })
            .collect();
        match matches.len() {
            0 => {}
            1 => {
                found.insert(matches[0].clone());
            }
            _ => {
                let specs = matches
                    .iter()
                    .map(|spec| spec.to_string())
                    .collect::<Vec<_>>()
                    .join(", ");
                bail!(
                    "multiple package overrides in profile `{}` match package `{}`\n\
                     found package specs: {}",
                    name,
                    pkg_id,
                    specs
                );
            }
        }
    }
    Ok(found)
}

/// Check for any profile override specs that do not match any known packages.
///
/// This helps check for typos and mistakes.
fn validate_packages_unmatched(
    shell: &mut Shell,
    resolve: &Resolve,
    name: &str,
    toml: &TomlProfile,
    found: &HashSet<PackageIdSpec>,
) -> CargoResult<()> {
    let Some(overrides) = toml.package.as_ref() else {
        return Ok(());
    };

    // Verify every override matches at least one package.
    let missing_specs = overrides.keys().filter_map(|key| {
        if let ProfilePackageSpec::Spec(ref spec) = *key {
            if !found.contains(spec) {
                return Some(spec);
            }
        }
        None
    });
    for spec in missing_specs {
        // See if there is an exact name match.
        let name_matches: Vec<String> = resolve
            .iter()
            .filter_map(|pkg_id| {
                if pkg_id.name() == spec.name() {
                    Some(pkg_id.to_string())
                } else {
                    None
                }
            })
            .collect();
        if name_matches.is_empty() {
            let suggestion = closest_msg(
                &spec.name(),
                resolve.iter(),
                |p| p.name().as_str(),
                "package",
            );
            shell.warn(format!(
                "profile package spec `{}` in profile `{}` did not match any packages{}",
                spec, name, suggestion
            ))?;
        } else {
            shell.warn(format!(
                "profile package spec `{}` in profile `{}` \
                 has a version or URL that does not match any of the packages: {}",
                spec,
                name,
                name_matches.join(", ")
            ))?;
        }
    }
    Ok(())
}

/// Returns `true` if a string is a toggle that turns an option off.
fn is_off(s: &str) -> bool {
    matches!(s, "off" | "n" | "no" | "none")
}
