//! This modules contains types storing information of target platforms.
//!
//! Normally, call [`RustcTargetData::new`] to construct all the target
//! platform once, and then query info on your demand. For example,
//!
//! * [`RustcTargetData::dep_platform_activated`] to check if platform is activated.
//! * [`RustcTargetData::info`] to get a [`TargetInfo`] for an in-depth query.
//! * [`TargetInfo::rustc_outputs`] to get a list of supported file types.

use crate::core::compiler::apply_env_config;
use crate::core::compiler::{BuildRunner, CompileKind, CompileMode, CompileTarget, CrateType};
use crate::core::{Dependency, Package, Target, TargetKind, Workspace};
use crate::util::context::{GlobalContext, StringList, TargetConfig};
use crate::util::interning::InternedString;
use crate::util::{CargoResult, Rustc};
use anyhow::Context as _;
use cargo_platform::{Cfg, CfgExpr};
use cargo_util::{paths, ProcessBuilder};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::hash_map::{Entry, HashMap};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::str::{self, FromStr};

/// Information about the platform target gleaned from querying rustc.
///
/// [`RustcTargetData`] keeps several of these, one for the host and the others
/// for other specified targets. If no target is specified, it uses a clone from
/// the host.
#[derive(Clone)]
pub struct TargetInfo {
    /// A base process builder for discovering crate type information. In
    /// particular, this is used to determine the output filename prefix and
    /// suffix for a crate type.
    crate_type_process: ProcessBuilder,
    /// Cache of output filename prefixes and suffixes.
    ///
    /// The key is the crate type name (like `cdylib`) and the value is
    /// `Some((prefix, suffix))`, for example `libcargo.so` would be
    /// `Some(("lib", ".so"))`. The value is `None` if the crate type is not
    /// supported.
    crate_types: RefCell<HashMap<CrateType, Option<(String, String)>>>,
    /// `cfg` information extracted from `rustc --print=cfg`.
    cfg: Vec<Cfg>,
    /// `supports_std` information extracted from `rustc --print=target-spec-json`
    pub supports_std: Option<bool>,
    /// Supported values for `-Csplit-debuginfo=` flag, queried from rustc
    support_split_debuginfo: Vec<String>,
    /// Path to the sysroot.
    pub sysroot: PathBuf,
    /// Path to the "lib" directory in the sysroot which rustc uses for linking
    /// target libraries.
    pub sysroot_target_libdir: PathBuf,
    /// Extra flags to pass to `rustc`, see [`extra_args`].
    pub rustflags: Rc<[String]>,
    /// Extra flags to pass to `rustdoc`, see [`extra_args`].
    pub rustdocflags: Rc<[String]>,
}

/// Kind of each file generated by a Unit, part of `FileType`.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum FileFlavor {
    /// Not a special file type.
    Normal,
    /// Like `Normal`, but not directly executable.
    /// For example, a `.wasm` file paired with the "normal" `.js` file.
    Auxiliary,
    /// Something you can link against (e.g., a library).
    Linkable,
    /// An `.rmeta` Rust metadata file.
    Rmeta,
    /// Piece of external debug information (e.g., `.dSYM`/`.pdb` file).
    DebugInfo,
    /// SBOM (Software Bill of Materials pre-cursor) file (e.g. cargo-sbon.json).
    Sbom,
}

/// Type of each file generated by a Unit.
#[derive(Debug)]
pub struct FileType {
    /// The kind of file.
    pub flavor: FileFlavor,
    /// The crate-type that generates this file.
    ///
    /// `None` for things that aren't associated with a specific crate type,
    /// for example `rmeta` files.
    pub crate_type: Option<CrateType>,
    /// The suffix for the file (for example, `.rlib`).
    /// This is an empty string for executables on Unix-like platforms.
    suffix: String,
    /// The prefix for the file (for example, `lib`).
    /// This is an empty string for things like executables.
    prefix: String,
    /// Flag to convert hyphen to underscore when uplifting.
    should_replace_hyphens: bool,
}

impl FileType {
    /// The filename for this `FileType` created by rustc.
    pub fn output_filename(&self, target: &Target, metadata: Option<&str>) -> String {
        match metadata {
            Some(metadata) => format!(
                "{}{}-{}{}",
                self.prefix,
                target.crate_name(),
                metadata,
                self.suffix
            ),
            None => format!("{}{}{}", self.prefix, target.crate_name(), self.suffix),
        }
    }

    /// The filename for this `FileType` that Cargo should use when "uplifting"
    /// it to the destination directory.
    pub fn uplift_filename(&self, target: &Target) -> String {
        let name = match target.binary_filename() {
            Some(name) => name,
            None => {
                // For binary crate type, `should_replace_hyphens` will always be false.
                if self.should_replace_hyphens {
                    target.crate_name()
                } else {
                    target.name().to_string()
                }
            }
        };

        format!("{}{}{}", self.prefix, name, self.suffix)
    }

    /// Creates a new instance representing a `.rmeta` file.
    pub fn new_rmeta() -> FileType {
        // Note that even binaries use the `lib` prefix.
        FileType {
            flavor: FileFlavor::Rmeta,
            crate_type: None,
            suffix: ".rmeta".to_string(),
            prefix: "lib".to_string(),
            should_replace_hyphens: true,
        }
    }
}

impl TargetInfo {
    /// Learns the information of target platform from `rustc` invocation(s).
    ///
    /// Generally, the first time calling this function is expensive, as it may
    /// query `rustc` several times. To reduce the cost, output of each `rustc`
    /// invocation is cached by [`Rustc::cached_output`].
    ///
    /// Search `Tricky` to learn why querying `rustc` several times is needed.
    #[tracing::instrument(skip_all)]
    pub fn new(
        gctx: &GlobalContext,
        requested_kinds: &[CompileKind],
        rustc: &Rustc,
        kind: CompileKind,
    ) -> CargoResult<TargetInfo> {
        let mut rustflags =
            extra_args(gctx, requested_kinds, &rustc.host, None, kind, Flags::Rust)?;
        let mut turn = 0;
        loop {
            let extra_fingerprint = kind.fingerprint_hash();

            // Query rustc for several kinds of info from each line of output:
            // 0) file-names (to determine output file prefix/suffix for given crate type)
            // 1) sysroot
            // 2) split-debuginfo
            // 3) cfg
            //
            // Search `--print` to see what we query so far.
            let mut process = rustc.workspace_process();
            apply_env_config(gctx, &mut process)?;
            process
                .arg("-")
                .arg("--crate-name")
                .arg("___")
                .arg("--print=file-names")
                .args(&rustflags)
                .env_remove("RUSTC_LOG");

            // Removes `FD_CLOEXEC` set by `jobserver::Client` to pass jobserver
            // as environment variables specify.
            if let Some(client) = gctx.jobserver_from_env() {
                process.inherit_jobserver(client);
            }

            if let CompileKind::Target(target) = kind {
                process.arg("--target").arg(target.rustc_target());
            }

            let crate_type_process = process.clone();
            const KNOWN_CRATE_TYPES: &[CrateType] = &[
                CrateType::Bin,
                CrateType::Rlib,
                CrateType::Dylib,
                CrateType::Cdylib,
                CrateType::Staticlib,
                CrateType::ProcMacro,
            ];
            for crate_type in KNOWN_CRATE_TYPES.iter() {
                process.arg("--crate-type").arg(crate_type.as_str());
            }

            process.arg("--print=sysroot");
            process.arg("--print=split-debuginfo");
            process.arg("--print=crate-name"); // `___` as a delimiter.
            process.arg("--print=cfg");

            // parse_crate_type() relies on "unsupported/unknown crate type" error message,
            // so make warnings always emitted as warnings.
            process.arg("-Wwarnings");

            let (output, error) = rustc
                .cached_output(&process, extra_fingerprint)
                .with_context(|| {
                    "failed to run `rustc` to learn about target-specific information"
                })?;

            let mut lines = output.lines();
            let mut map = HashMap::new();
            for crate_type in KNOWN_CRATE_TYPES {
                let out = parse_crate_type(crate_type, &process, &output, &error, &mut lines)?;
                map.insert(crate_type.clone(), out);
            }

            let Some(line) = lines.next() else {
                return error_missing_print_output("sysroot", &process, &output, &error);
            };
            let sysroot = PathBuf::from(line);
            let sysroot_target_libdir = {
                let mut libdir = sysroot.clone();
                libdir.push("lib");
                libdir.push("rustlib");
                libdir.push(match &kind {
                    CompileKind::Host => rustc.host.as_str(),
                    CompileKind::Target(target) => target.short_name(),
                });
                libdir.push("lib");
                libdir
            };

            let support_split_debuginfo = {
                // HACK: abuse `--print=crate-name` to use `___` as a delimiter.
                let mut res = Vec::new();
                loop {
                    match lines.next() {
                        Some(line) if line == "___" => break,
                        Some(line) => res.push(line.into()),
                        None => {
                            return error_missing_print_output(
                                "split-debuginfo",
                                &process,
                                &output,
                                &error,
                            )
                        }
                    }
                }
                res
            };

            let cfg = lines
                .map(|line| Ok(Cfg::from_str(line)?))
                .filter(TargetInfo::not_user_specific_cfg)
                .collect::<CargoResult<Vec<_>>>()
                .with_context(|| {
                    format!(
                        "failed to parse the cfg from `rustc --print=cfg`, got:\n{}",
                        output
                    )
                })?;

            // recalculate `rustflags` from above now that we have `cfg`
            // information
            let new_flags = extra_args(
                gctx,
                requested_kinds,
                &rustc.host,
                Some(&cfg),
                kind,
                Flags::Rust,
            )?;

            // Tricky: `RUSTFLAGS` defines the set of active `cfg` flags, active
            // `cfg` flags define which `.cargo/config` sections apply, and they
            // in turn can affect `RUSTFLAGS`! This is a bona fide mutual
            // dependency, and it can even diverge (see `cfg_paradox` test).
            //
            // So what we do here is running at most *two* iterations of
            // fixed-point iteration, which should be enough to cover
            // practically useful cases, and warn if that's not enough for
            // convergence.
            let reached_fixed_point = new_flags == rustflags;
            if !reached_fixed_point && turn == 0 {
                turn += 1;
                rustflags = new_flags;
                continue;
            }
            if !reached_fixed_point {
                gctx.shell().warn("non-trivial mutual dependency between target-specific configuration and RUSTFLAGS")?;
            }

            let mut supports_std: Option<bool> = None;

            // The '--print=target-spec-json' is an unstable option of rustc, therefore only
            // try to fetch this information if rustc allows nightly features. Additionally,
            // to avoid making two rustc queries when not required, only try to fetch the
            // target-spec when the '-Zbuild-std' option is passed.
            if gctx.cli_unstable().build_std.is_some() {
                let mut target_spec_process = rustc.workspace_process();
                apply_env_config(gctx, &mut target_spec_process)?;
                target_spec_process
                    .arg("--print=target-spec-json")
                    .arg("-Zunstable-options")
                    .args(&rustflags)
                    .env_remove("RUSTC_LOG");

                if let CompileKind::Target(target) = kind {
                    target_spec_process
                        .arg("--target")
                        .arg(target.rustc_target());
                }

                #[derive(Deserialize)]
                struct Metadata {
                    pub std: Option<bool>,
                }

                #[derive(Deserialize)]
                struct TargetSpec {
                    pub metadata: Metadata,
                }

                if let Ok(output) = target_spec_process.output() {
                    if let Ok(spec) = serde_json::from_slice::<TargetSpec>(&output.stdout) {
                        supports_std = spec.metadata.std;
                    }
                }
            }

            return Ok(TargetInfo {
                crate_type_process,
                crate_types: RefCell::new(map),
                sysroot,
                sysroot_target_libdir,
                rustflags: rustflags.into(),
                rustdocflags: extra_args(
                    gctx,
                    requested_kinds,
                    &rustc.host,
                    Some(&cfg),
                    kind,
                    Flags::Rustdoc,
                )?
                .into(),
                cfg,
                supports_std,
                support_split_debuginfo,
            });
        }
    }

    fn not_user_specific_cfg(cfg: &CargoResult<Cfg>) -> bool {
        if let Ok(Cfg::Name(cfg_name)) = cfg {
            // This should also include "debug_assertions", but it causes
            // regressions. Maybe some day in the distant future it can be
            // added (and possibly change the warning to an error).
            if cfg_name == "proc_macro" {
                return false;
            }
        }
        true
    }

    /// All the target [`Cfg`] settings.
    pub fn cfg(&self) -> &[Cfg] {
        &self.cfg
    }

    /// Returns the list of file types generated by the given crate type.
    ///
    /// Returns `None` if the target does not support the given crate type.
    fn file_types(
        &self,
        crate_type: &CrateType,
        flavor: FileFlavor,
        target_triple: &str,
    ) -> CargoResult<Option<Vec<FileType>>> {
        let crate_type = if *crate_type == CrateType::Lib {
            CrateType::Rlib
        } else {
            crate_type.clone()
        };

        let mut crate_types = self.crate_types.borrow_mut();
        let entry = crate_types.entry(crate_type.clone());
        let crate_type_info = match entry {
            Entry::Occupied(o) => &*o.into_mut(),
            Entry::Vacant(v) => {
                let value = self.discover_crate_type(v.key())?;
                &*v.insert(value)
            }
        };
        let Some((prefix, suffix)) = crate_type_info else {
            return Ok(None);
        };
        let mut ret = vec![FileType {
            suffix: suffix.clone(),
            prefix: prefix.clone(),
            flavor,
            crate_type: Some(crate_type.clone()),
            should_replace_hyphens: crate_type != CrateType::Bin,
        }];

        // Window shared library import/export files.
        if crate_type.is_dynamic() {
            // Note: Custom JSON specs can alter the suffix. For now, we'll
            // just ignore non-DLL suffixes.
            if target_triple.ends_with("-windows-msvc") && suffix == ".dll" {
                // See https://docs.microsoft.com/en-us/cpp/build/reference/working-with-import-libraries-and-export-files
                // for more information about DLL import/export files.
                ret.push(FileType {
                    suffix: ".dll.lib".to_string(),
                    prefix: prefix.clone(),
                    flavor: FileFlavor::Auxiliary,
                    crate_type: Some(crate_type.clone()),
                    should_replace_hyphens: true,
                });
                // NOTE: lld does not produce these
                ret.push(FileType {
                    suffix: ".dll.exp".to_string(),
                    prefix: prefix.clone(),
                    flavor: FileFlavor::Auxiliary,
                    crate_type: Some(crate_type.clone()),
                    should_replace_hyphens: true,
                });
            } else if suffix == ".dll"
                && (target_triple.ends_with("windows-gnu")
                    || target_triple.ends_with("windows-gnullvm")
                    || target_triple.ends_with("cygwin"))
            {
                // See https://cygwin.com/cygwin-ug-net/dll.html for more
                // information about GNU import libraries.
                // LD can link DLL directly, but LLD requires the import library.
                ret.push(FileType {
                    suffix: ".dll.a".to_string(),
                    prefix: "lib".to_string(),
                    flavor: FileFlavor::Auxiliary,
                    crate_type: Some(crate_type.clone()),
                    should_replace_hyphens: true,
                })
            }
        }

        if target_triple.starts_with("wasm32-") && crate_type == CrateType::Bin && suffix == ".js" {
            // emscripten binaries generate a .js file, which loads a .wasm
            // file.
            ret.push(FileType {
                suffix: ".wasm".to_string(),
                prefix: prefix.clone(),
                flavor: FileFlavor::Auxiliary,
                crate_type: Some(crate_type.clone()),
                // Name `foo-bar` will generate a `foo_bar.js` and
                // `foo_bar.wasm`. Cargo will translate the underscore and
                // copy `foo_bar.js` to `foo-bar.js`. However, the wasm
                // filename is embedded in the .js file with an underscore, so
                // it should not contain hyphens.
                should_replace_hyphens: true,
            });
            // And a map file for debugging. This is only emitted with debug=2
            // (-g4 for emcc).
            ret.push(FileType {
                suffix: ".wasm.map".to_string(),
                prefix: prefix.clone(),
                flavor: FileFlavor::DebugInfo,
                crate_type: Some(crate_type.clone()),
                should_replace_hyphens: true,
            });
        }

        // Handle separate debug files.
        let is_apple = target_triple.contains("-apple-");
        if matches!(
            crate_type,
            CrateType::Bin | CrateType::Dylib | CrateType::Cdylib | CrateType::ProcMacro
        ) {
            if is_apple {
                let suffix = if crate_type == CrateType::Bin {
                    ".dSYM".to_string()
                } else {
                    ".dylib.dSYM".to_string()
                };
                ret.push(FileType {
                    suffix,
                    prefix: prefix.clone(),
                    flavor: FileFlavor::DebugInfo,
                    crate_type: Some(crate_type),
                    // macOS tools like lldb use all sorts of magic to locate
                    // dSYM files. See https://lldb.llvm.org/use/symbols.html
                    // for some details. It seems like a `.dSYM` located next
                    // to the executable with the same name is one method. The
                    // dSYM should have the same hyphens as the executable for
                    // the names to match.
                    should_replace_hyphens: false,
                })
            } else if target_triple.ends_with("-msvc") || target_triple.ends_with("-uefi") {
                ret.push(FileType {
                    suffix: ".pdb".to_string(),
                    prefix: prefix.clone(),
                    flavor: FileFlavor::DebugInfo,
                    crate_type: Some(crate_type),
                    // The absolute path to the pdb file is embedded in the
                    // executable. If the exe/pdb pair is moved to another
                    // machine, then debuggers will look in the same directory
                    // of the exe with the original pdb filename. Since the
                    // original name contains underscores, they need to be
                    // preserved.
                    should_replace_hyphens: true,
                })
            } else {
                // Because DWARF Package (dwp) files are produced after the
                // fact by another tool, there is nothing in the binary that
                // provides a means to locate them. By convention, debuggers
                // take the binary filename and append ".dwp" (including to
                // binaries that already have an extension such as shared libs)
                // to find the dwp.
                ret.push(FileType {
                    // It is important to preserve the existing suffix for
                    // e.g. shared libraries, where the dwp for libfoo.so is
                    // expected to be at libfoo.so.dwp.
                    suffix: format!("{suffix}.dwp"),
                    prefix: prefix.clone(),
                    flavor: FileFlavor::DebugInfo,
                    crate_type: Some(crate_type.clone()),
                    // Likewise, the dwp needs to match the primary artifact's
                    // hyphenation exactly.
                    should_replace_hyphens: crate_type != CrateType::Bin,
                })
            }
        }

        Ok(Some(ret))
    }

    fn discover_crate_type(&self, crate_type: &CrateType) -> CargoResult<Option<(String, String)>> {
        let mut process = self.crate_type_process.clone();

        process.arg("--crate-type").arg(crate_type.as_str());

        let output = process.exec_with_output().with_context(|| {
            format!(
                "failed to run `rustc` to learn about crate-type {} information",
                crate_type
            )
        })?;

        let error = str::from_utf8(&output.stderr).unwrap();
        let output = str::from_utf8(&output.stdout).unwrap();
        parse_crate_type(crate_type, &process, output, error, &mut output.lines())
    }

    /// Returns all the file types generated by rustc for the given `mode`/`target_kind`.
    ///
    /// The first value is a Vec of file types generated, the second value is
    /// a list of `CrateTypes` that are not supported by the given target.
    pub fn rustc_outputs(
        &self,
        mode: CompileMode,
        target_kind: &TargetKind,
        target_triple: &str,
        gctx: &GlobalContext,
    ) -> CargoResult<(Vec<FileType>, Vec<CrateType>)> {
        match mode {
            CompileMode::Build => self.calc_rustc_outputs(target_kind, target_triple, gctx),
            CompileMode::Test => {
                match self.file_types(&CrateType::Bin, FileFlavor::Normal, target_triple)? {
                    Some(fts) => Ok((fts, Vec::new())),
                    None => Ok((Vec::new(), vec![CrateType::Bin])),
                }
            }
            CompileMode::Check { .. } => Ok((vec![FileType::new_rmeta()], Vec::new())),
            CompileMode::Doc { .. }
            | CompileMode::Doctest
            | CompileMode::Docscrape
            | CompileMode::RunCustomBuild => {
                panic!("asked for rustc output for non-rustc mode")
            }
        }
    }

    fn calc_rustc_outputs(
        &self,
        target_kind: &TargetKind,
        target_triple: &str,
        gctx: &GlobalContext,
    ) -> CargoResult<(Vec<FileType>, Vec<CrateType>)> {
        let mut unsupported = Vec::new();
        let mut result = Vec::new();
        let crate_types = target_kind.rustc_crate_types();
        for crate_type in &crate_types {
            let flavor = if crate_type.is_linkable() {
                FileFlavor::Linkable
            } else {
                FileFlavor::Normal
            };
            let file_types = self.file_types(crate_type, flavor, target_triple)?;
            match file_types {
                Some(types) => {
                    result.extend(types);
                }
                None => {
                    unsupported.push(crate_type.clone());
                }
            }
        }
        if !result.is_empty() {
            if gctx.cli_unstable().no_embed_metadata
                && crate_types
                    .iter()
                    .any(|ct| ct.benefits_from_no_embed_metadata())
            {
                // Add .rmeta when we apply -Zembed-metadata=no to the unit.
                result.push(FileType::new_rmeta());
            } else if !crate_types.iter().any(|ct| ct.requires_upstream_objects()) {
                // Only add rmeta if pipelining
                result.push(FileType::new_rmeta());
            }
        }
        Ok((result, unsupported))
    }

    /// Checks if the debuginfo-split value is supported by this target
    pub fn supports_debuginfo_split(&self, split: InternedString) -> bool {
        self.support_split_debuginfo
            .iter()
            .any(|sup| sup.as_str() == split.as_str())
    }

    /// Checks if a target maybe support std.
    ///
    /// If no explicitly stated in target spec json, we treat it as "maybe support".
    ///
    /// This is only useful for `-Zbuild-std` to determine the default set of
    /// crates it is going to build.
    pub fn maybe_support_std(&self) -> bool {
        matches!(self.supports_std, Some(true) | None)
    }
}

/// Takes rustc output (using specialized command line args), and calculates the file prefix and
/// suffix for the given crate type, or returns `None` if the type is not supported. (e.g., for a
/// Rust library like `libcargo.rlib`, we have prefix "lib" and suffix "rlib").
///
/// The caller needs to ensure that the lines object is at the correct line for the given crate
/// type: this is not checked.
///
/// This function can not handle more than one file per type (with wasm32-unknown-emscripten, there
/// are two files for bin (`.wasm` and `.js`)).
fn parse_crate_type(
    crate_type: &CrateType,
    cmd: &ProcessBuilder,
    output: &str,
    error: &str,
    lines: &mut str::Lines<'_>,
) -> CargoResult<Option<(String, String)>> {
    let not_supported = error.lines().any(|line| {
        (line.contains("unsupported crate type") || line.contains("unknown crate type"))
            && line.contains(&format!("crate type `{}`", crate_type))
    });
    if not_supported {
        return Ok(None);
    }
    let Some(line) = lines.next() else {
        anyhow::bail!(
            "malformed output when learning about crate-type {} information\n{}",
            crate_type,
            output_err_info(cmd, output, error)
        )
    };
    let mut parts = line.trim().split("___");
    let prefix = parts.next().unwrap();
    let Some(suffix) = parts.next() else {
        return error_missing_print_output("file-names", cmd, output, error);
    };

    Ok(Some((prefix.to_string(), suffix.to_string())))
}

/// Helper for creating an error message for missing output from a certain `--print` request.
fn error_missing_print_output<T>(
    request: &str,
    cmd: &ProcessBuilder,
    stdout: &str,
    stderr: &str,
) -> CargoResult<T> {
    let err_info = output_err_info(cmd, stdout, stderr);
    anyhow::bail!(
        "output of --print={request} missing when learning about \
     target-specific information from rustc\n{err_info}",
    )
}

/// Helper for creating an error message when parsing rustc output fails.
fn output_err_info(cmd: &ProcessBuilder, stdout: &str, stderr: &str) -> String {
    let mut result = format!("command was: {}\n", cmd);
    if !stdout.is_empty() {
        result.push_str("\n--- stdout\n");
        result.push_str(stdout);
    }
    if !stderr.is_empty() {
        result.push_str("\n--- stderr\n");
        result.push_str(stderr);
    }
    if stdout.is_empty() && stderr.is_empty() {
        result.push_str("(no output received)");
    }
    result
}

/// Compiler flags for either rustc or rustdoc.
#[derive(Debug, Copy, Clone)]
enum Flags {
    Rust,
    Rustdoc,
}

impl Flags {
    fn as_key(self) -> &'static str {
        match self {
            Flags::Rust => "rustflags",
            Flags::Rustdoc => "rustdocflags",
        }
    }

    fn as_env(self) -> &'static str {
        match self {
            Flags::Rust => "RUSTFLAGS",
            Flags::Rustdoc => "RUSTDOCFLAGS",
        }
    }
}

/// Acquire extra flags to pass to the compiler from various locations.
///
/// The locations are:
///
///  - the `CARGO_ENCODED_RUSTFLAGS` environment variable
///  - the `RUSTFLAGS` environment variable
///
/// then if none of those were found
///
///  - `target.*.rustflags` from the config (.cargo/config)
///  - `target.cfg(..).rustflags` from the config
///  - `host.*.rustflags` from the config if compiling a host artifact or without `--target`
///     (requires `-Zhost-config`)
///
/// then if none of those were found
///
///  - `build.rustflags` from the config
///
/// The behavior differs slightly when cross-compiling (or, specifically, when `--target` is
/// provided) for artifacts that are always built for the host (plugins, build scripts, ...).
/// For those artifacts, _only_ `host.*.rustflags` is respected, and no other configuration
/// sources, _regardless of the value of `target-applies-to-host`_. This is counterintuitive, but
/// necessary to retain backwards compatibility with older versions of Cargo.
///
/// Rules above also applies to rustdoc. Just the key would be `rustdocflags`/`RUSTDOCFLAGS`.
fn extra_args(
    gctx: &GlobalContext,
    requested_kinds: &[CompileKind],
    host_triple: &str,
    target_cfg: Option<&[Cfg]>,
    kind: CompileKind,
    flags: Flags,
) -> CargoResult<Vec<String>> {
    let target_applies_to_host = gctx.target_applies_to_host()?;

    // Host artifacts should not generally pick up rustflags from anywhere except [host].
    //
    // The one exception to this is if `target-applies-to-host = true`, which opts into a
    // particular (inconsistent) past Cargo behavior where host artifacts _do_ pick up rustflags
    // set elsewhere when `--target` isn't passed.
    if kind.is_host() {
        if target_applies_to_host && requested_kinds == [CompileKind::Host] {
            // This is the past Cargo behavior where we fall back to the same logic as for other
            // artifacts without --target.
        } else {
            // In all other cases, host artifacts just get flags from [host], regardless of
            // --target. Or, phrased differently, no `--target` behaves the same as `--target
            // <host>`, and host artifacts are always "special" (they don't pick up `RUSTFLAGS` for
            // example).
            return Ok(rustflags_from_host(gctx, flags, host_triple)?.unwrap_or_else(Vec::new));
        }
    }

    // All other artifacts pick up the RUSTFLAGS, [target.*], and [build], in that order.
    // NOTE: It is impossible to have a [host] section and reach this logic with kind.is_host(),
    // since [host] implies `target-applies-to-host = false`, which always early-returns above.

    if let Some(rustflags) = rustflags_from_env(gctx, flags) {
        Ok(rustflags)
    } else if let Some(rustflags) =
        rustflags_from_target(gctx, host_triple, target_cfg, kind, flags)?
    {
        Ok(rustflags)
    } else if let Some(rustflags) = rustflags_from_build(gctx, flags)? {
        Ok(rustflags)
    } else {
        Ok(Vec::new())
    }
}

/// Gets compiler flags from environment variables.
/// See [`extra_args`] for more.
fn rustflags_from_env(gctx: &GlobalContext, flags: Flags) -> Option<Vec<String>> {
    // First try CARGO_ENCODED_RUSTFLAGS from the environment.
    // Prefer this over RUSTFLAGS since it's less prone to encoding errors.
    if let Ok(a) = gctx.get_env(format!("CARGO_ENCODED_{}", flags.as_env())) {
        if a.is_empty() {
            return Some(Vec::new());
        }
        return Some(a.split('\x1f').map(str::to_string).collect());
    }

    // Then try RUSTFLAGS from the environment
    if let Ok(a) = gctx.get_env(flags.as_env()) {
        let args = a
            .split(' ')
            .map(str::trim)
            .filter(|s| !s.is_empty())
            .map(str::to_string);
        return Some(args.collect());
    }

    // No rustflags to be collected from the environment
    None
}

/// Gets compiler flags from `[target]` section in the config.
/// See [`extra_args`] for more.
fn rustflags_from_target(
    gctx: &GlobalContext,
    host_triple: &str,
    target_cfg: Option<&[Cfg]>,
    kind: CompileKind,
    flag: Flags,
) -> CargoResult<Option<Vec<String>>> {
    let mut rustflags = Vec::new();

    // Then the target.*.rustflags value...
    let target = match &kind {
        CompileKind::Host => host_triple,
        CompileKind::Target(target) => target.short_name(),
    };
    let key = format!("target.{}.{}", target, flag.as_key());
    if let Some(args) = gctx.get::<Option<StringList>>(&key)? {
        rustflags.extend(args.as_slice().iter().cloned());
    }
    // ...including target.'cfg(...)'.rustflags
    if let Some(target_cfg) = target_cfg {
        gctx.target_cfgs()?
            .iter()
            .filter_map(|(key, cfg)| {
                match flag {
                    Flags::Rust => cfg
                        .rustflags
                        .as_ref()
                        .map(|rustflags| (key, &rustflags.val)),
                    // `target.cfg(…).rustdocflags` is currently not supported.
                    Flags::Rustdoc => None,
                }
            })
            .filter(|(key, _rustflags)| CfgExpr::matches_key(key, target_cfg))
            .for_each(|(_key, cfg_rustflags)| {
                rustflags.extend(cfg_rustflags.as_slice().iter().cloned());
            });
    }

    if rustflags.is_empty() {
        Ok(None)
    } else {
        Ok(Some(rustflags))
    }
}

/// Gets compiler flags from `[host]` section in the config.
/// See [`extra_args`] for more.
fn rustflags_from_host(
    gctx: &GlobalContext,
    flag: Flags,
    host_triple: &str,
) -> CargoResult<Option<Vec<String>>> {
    let target_cfg = gctx.host_cfg_triple(host_triple)?;
    let list = match flag {
        Flags::Rust => &target_cfg.rustflags,
        Flags::Rustdoc => {
            // host.rustdocflags is not a thing, since it does not make sense
            return Ok(None);
        }
    };
    Ok(list.as_ref().map(|l| l.val.as_slice().to_vec()))
}

/// Gets compiler flags from `[build]` section in the config.
/// See [`extra_args`] for more.
fn rustflags_from_build(gctx: &GlobalContext, flag: Flags) -> CargoResult<Option<Vec<String>>> {
    // Then the `build.rustflags` value.
    let build = gctx.build_config()?;
    let list = match flag {
        Flags::Rust => &build.rustflags,
        Flags::Rustdoc => &build.rustdocflags,
    };
    Ok(list.as_ref().map(|l| l.as_slice().to_vec()))
}

/// Collection of information about `rustc` and the host and target.
pub struct RustcTargetData<'gctx> {
    /// Information about `rustc` itself.
    pub rustc: Rustc,

    /// Config
    pub gctx: &'gctx GlobalContext,
    requested_kinds: Vec<CompileKind>,

    /// Build information for the "host", which is information about when
    /// `rustc` is invoked without a `--target` flag. This is used for
    /// selecting a linker, and applying link overrides.
    ///
    /// The configuration read into this depends on whether or not
    /// `target-applies-to-host=true`.
    host_config: TargetConfig,
    /// Information about the host platform.
    host_info: TargetInfo,

    /// Build information for targets that we're building for.
    target_config: HashMap<CompileTarget, TargetConfig>,
    /// Information about the target platform that we're building for.
    target_info: HashMap<CompileTarget, TargetInfo>,
}

impl<'gctx> RustcTargetData<'gctx> {
    #[tracing::instrument(skip_all)]
    pub fn new(
        ws: &Workspace<'gctx>,
        requested_kinds: &[CompileKind],
    ) -> CargoResult<RustcTargetData<'gctx>> {
        let gctx = ws.gctx();
        let rustc = gctx.load_global_rustc(Some(ws))?;
        let mut target_config = HashMap::new();
        let mut target_info = HashMap::new();
        let target_applies_to_host = gctx.target_applies_to_host()?;
        let host_target = CompileTarget::new(&rustc.host)?;
        let host_info = TargetInfo::new(gctx, requested_kinds, &rustc, CompileKind::Host)?;

        // This config is used for link overrides and choosing a linker.
        let host_config = if target_applies_to_host {
            gctx.target_cfg_triple(&rustc.host)?
        } else {
            gctx.host_cfg_triple(&rustc.host)?
        };

        // This is a hack. The unit_dependency graph builder "pretends" that
        // `CompileKind::Host` is `CompileKind::Target(host)` if the
        // `--target` flag is not specified. Since the unit_dependency code
        // needs access to the target config data, create a copy so that it
        // can be found. See `rebuild_unit_graph_shared` for why this is done.
        if requested_kinds.iter().any(CompileKind::is_host) {
            target_config.insert(host_target, gctx.target_cfg_triple(&rustc.host)?);

            // If target_applies_to_host is true, the host_info is the target info,
            // otherwise we need to build target info for the target.
            if target_applies_to_host {
                target_info.insert(host_target, host_info.clone());
            } else {
                let host_target_info = TargetInfo::new(
                    gctx,
                    requested_kinds,
                    &rustc,
                    CompileKind::Target(host_target),
                )?;
                target_info.insert(host_target, host_target_info);
            }
        };

        let mut res = RustcTargetData {
            rustc,
            gctx,
            requested_kinds: requested_kinds.into(),
            host_config,
            host_info,
            target_config,
            target_info,
        };

        // Get all kinds we currently know about.
        //
        // For now, targets can only ever come from the root workspace
        // units and artifact dependencies, so this
        // correctly represents all the kinds that can happen. When we have
        // other ways for targets to appear at places that are not the root units,
        // we may have to revisit this.
        fn artifact_targets(package: &Package) -> impl Iterator<Item = CompileKind> + '_ {
            package
                .manifest()
                .dependencies()
                .iter()
                .filter_map(|d| d.artifact()?.target()?.to_compile_kind())
        }
        let all_kinds = requested_kinds
            .iter()
            .copied()
            .chain(ws.members().flat_map(|p| {
                p.manifest()
                    .default_kind()
                    .into_iter()
                    .chain(p.manifest().forced_kind())
                    .chain(artifact_targets(p))
            }));
        for kind in all_kinds {
            res.merge_compile_kind(kind)?;
        }

        Ok(res)
    }

    /// Insert `kind` into our `target_info` and `target_config` members if it isn't present yet.
    pub fn merge_compile_kind(&mut self, kind: CompileKind) -> CargoResult<()> {
        if let CompileKind::Target(target) = kind {
            if !self.target_config.contains_key(&target) {
                self.target_config
                    .insert(target, self.gctx.target_cfg_triple(target.short_name())?);
            }
            if !self.target_info.contains_key(&target) {
                self.target_info.insert(
                    target,
                    TargetInfo::new(self.gctx, &self.requested_kinds, &self.rustc, kind)?,
                );
            }
        }
        Ok(())
    }

    /// Returns a "short" name for the given kind, suitable for keying off
    /// configuration in Cargo or presenting to users.
    pub fn short_name<'a>(&'a self, kind: &'a CompileKind) -> &'a str {
        match kind {
            CompileKind::Host => &self.rustc.host,
            CompileKind::Target(target) => target.short_name(),
        }
    }

    /// Whether a dependency should be compiled for the host or target platform,
    /// specified by `CompileKind`.
    pub fn dep_platform_activated(&self, dep: &Dependency, kind: CompileKind) -> bool {
        // If this dependency is only available for certain platforms,
        // make sure we're only enabling it for that platform.
        let Some(platform) = dep.platform() else {
            return true;
        };
        let name = self.short_name(&kind);
        platform.matches(name, self.cfg(kind))
    }

    /// Gets the list of `cfg`s printed out from the compiler for the specified kind.
    pub fn cfg(&self, kind: CompileKind) -> &[Cfg] {
        self.info(kind).cfg()
    }

    /// Information about the given target platform, learned by querying rustc.
    ///
    /// # Panics
    ///
    /// Panics, if the target platform described by `kind` can't be found.
    /// See [`get_info`](Self::get_info) for a non-panicking alternative.
    pub fn info(&self, kind: CompileKind) -> &TargetInfo {
        self.get_info(kind).unwrap()
    }

    /// Information about the given target platform, learned by querying rustc.
    ///
    /// Returns `None` if the target platform described by `kind` can't be found.
    pub fn get_info(&self, kind: CompileKind) -> Option<&TargetInfo> {
        match kind {
            CompileKind::Host => Some(&self.host_info),
            CompileKind::Target(s) => self.target_info.get(&s),
        }
    }

    /// Gets the target configuration for a particular host or target.
    pub fn target_config(&self, kind: CompileKind) -> &TargetConfig {
        match kind {
            CompileKind::Host => &self.host_config,
            CompileKind::Target(s) => &self.target_config[&s],
        }
    }

    pub fn get_unsupported_std_targets(&self) -> Vec<&str> {
        let mut unsupported = Vec::new();
        for (target, target_info) in &self.target_info {
            if target_info.supports_std == Some(false) {
                unsupported.push(target.short_name());
            }
        }
        unsupported
    }
}

/// Structure used to deal with Rustdoc fingerprinting
#[derive(Debug, Serialize, Deserialize)]
pub struct RustDocFingerprint {
    pub rustc_vv: String,
}

impl RustDocFingerprint {
    /// This function checks whether the latest version of `Rustc` used to compile this
    /// `Workspace`'s docs was the same as the one is currently being used in this `cargo doc`
    /// call.
    ///
    /// In case it's not, it takes care of removing the `doc/` folder as well as overwriting
    /// the rustdoc fingerprint info in order to guarantee that we won't end up with mixed
    /// versions of the `js/html/css` files that `rustdoc` autogenerates which do not have
    /// any versioning.
    pub fn check_rustdoc_fingerprint(build_runner: &BuildRunner<'_, '_>) -> CargoResult<()> {
        if build_runner
            .bcx
            .gctx
            .cli_unstable()
            .skip_rustdoc_fingerprint
        {
            return Ok(());
        }
        let actual_rustdoc_target_data = RustDocFingerprint {
            rustc_vv: build_runner.bcx.rustc().verbose_version.clone(),
        };

        let fingerprint_path = build_runner
            .files()
            .host_root()
            .join(".rustdoc_fingerprint.json");
        let write_fingerprint = || -> CargoResult<()> {
            paths::write(
                &fingerprint_path,
                serde_json::to_string(&actual_rustdoc_target_data)?,
            )
        };
        let Ok(rustdoc_data) = paths::read(&fingerprint_path) else {
            // If the fingerprint does not exist, do not clear out the doc
            // directories. Otherwise this ran into problems where projects
            // like bootstrap were creating the doc directory before running
            // `cargo doc` in a way that deleting it would break it.
            return write_fingerprint();
        };
        match serde_json::from_str::<RustDocFingerprint>(&rustdoc_data) {
            Ok(fingerprint) => {
                if fingerprint.rustc_vv == actual_rustdoc_target_data.rustc_vv {
                    return Ok(());
                } else {
                    tracing::debug!(
                        "doc fingerprint changed:\noriginal:\n{}\nnew:\n{}",
                        fingerprint.rustc_vv,
                        actual_rustdoc_target_data.rustc_vv
                    );
                }
            }
            Err(e) => {
                tracing::debug!("could not deserialize {:?}: {}", fingerprint_path, e);
            }
        };
        // Fingerprint does not match, delete the doc directories and write a new fingerprint.
        tracing::debug!(
            "fingerprint {:?} mismatch, clearing doc directories",
            fingerprint_path
        );
        build_runner
            .bcx
            .all_kinds
            .iter()
            .map(|kind| build_runner.files().layout(*kind).doc())
            .filter(|path| path.exists())
            .try_for_each(|path| clean_doc(path))?;
        write_fingerprint()?;
        return Ok(());

        fn clean_doc(path: &Path) -> CargoResult<()> {
            let entries = path
                .read_dir()
                .with_context(|| format!("failed to read directory `{}`", path.display()))?;
            for entry in entries {
                let entry = entry?;
                // Don't remove hidden files. Rustdoc does not create them,
                // but the user might have.
                if entry
                    .file_name()
                    .to_str()
                    .map_or(false, |name| name.starts_with('.'))
                {
                    continue;
                }
                let path = entry.path();
                if entry.file_type()?.is_dir() {
                    paths::remove_dir_all(path)?;
                } else {
                    paths::remove_file(path)?;
                }
            }
            Ok(())
        }
    }
}
