// Copyright 2017 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use cargo::core::{enable_nightly_features, PackageId, Shell, Target, TargetKind, Verbosity, Workspace};
use cargo::core::compiler::{Context, Executor, Unit, BuildConfig, CompileMode};
use cargo::ops::{compile_with_exec, CompileFilter, CompileOptions, Packages};
use cargo::util::{homedir, important_paths, CargoResult, Config as CargoConfig, ConfigValue,
                  ProcessBuilder};
use failure;
use serde_json;

use crate::actions::progress::ProgressUpdate;
use rls_data::Analysis;
use crate::build::{BufWriter, BuildResult, CompilationContext, Internals, PackageArg};
use crate::build::environment::{self, Environment, EnvironmentLock};
use crate::config::Config;
use rls_vfs::Vfs;

use std::collections::{BTreeMap, HashMap, HashSet};
use std::env;
use std::ffi::OsString;
use std::fmt::Write;
use std::fs::{read_dir, remove_file};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::Sender;
use std::thread;

// Runs an in-process instance of Cargo.
pub(super) fn cargo(internals: &Internals, package_arg: PackageArg, progress_sender: Sender<ProgressUpdate>) -> BuildResult {
    let compilation_cx = internals.compilation_cx.clone();
    let config = internals.config.clone();
    let vfs = internals.vfs.clone();
    let env_lock = internals.env_lock.clone();

    let diagnostics = Arc::new(Mutex::new(vec![]));
    let diagnostics_clone = diagnostics.clone();
    let analysis = Arc::new(Mutex::new(vec![]));
    let analysis_clone = analysis.clone();
    let out = Arc::new(Mutex::new(vec![]));
    let out_clone = out.clone();

    // Cargo may or may not spawn threads to run the various builds, since
    // we may be in separate threads we need to block and wait our thread.
    // However, if Cargo doesn't run a separate thread, then we'll just wait
    // forever. Therefore, we spawn an extra thread here to be safe.
    let handle = thread::spawn(move || {
        run_cargo(
            compilation_cx,
            package_arg,
            config,
            vfs,
            env_lock,
            diagnostics,
            analysis,
            out,
            progress_sender,
        )
    });

    match handle
        .join()
        .map_err(|_| failure::err_msg("thread panicked"))
        .and_then(|res| res)
    {
        Ok(ref cwd) => {
            let diagnostics = Arc::try_unwrap(diagnostics_clone)
                .unwrap()
                .into_inner()
                .unwrap();
            let analysis = Arc::try_unwrap(analysis_clone)
                .unwrap()
                .into_inner()
                .unwrap();
            BuildResult::Success(cwd.clone(), diagnostics, analysis, true)
        }
        Err(err) => {
            // This message goes like this to the UI via showMessage. In VSCode
            // this ends up on one single line, so it's important to keep it concise.
            let stdout = String::from_utf8(out_clone.lock().unwrap().to_owned()).unwrap();
            let stdout_msg = if stdout.is_empty() {
                "".to_string()
            } else {
                format!("({})", stdout)
            };
            let msg = format!("Cargo failed: {}{}", err, stdout_msg);
            debug!("{}", msg);
            BuildResult::Err(msg, None)
        }
    }
}

fn run_cargo(
    compilation_cx: Arc<Mutex<CompilationContext>>,
    package_arg: PackageArg,
    rls_config: Arc<Mutex<Config>>,
    vfs: Arc<Vfs>,
    env_lock: Arc<EnvironmentLock>,
    compiler_messages: Arc<Mutex<Vec<String>>>,
    analysis: Arc<Mutex<Vec<Analysis>>>,
    out: Arc<Mutex<Vec<u8>>>,
    progress_sender: Sender<ProgressUpdate>,
) -> CargoResult<PathBuf> {
    // Lock early to guarantee synchronized access to env var for the scope of Cargo routine.
    // Additionally we need to pass inner lock to RlsExecutor, since it needs to hand it down
    // during exec() callback when calling linked compiler in parallel, for which we need to
    // guarantee consistent environment variables.
    let (lock_guard, inner_lock) = env_lock.lock();

    let mut restore_env = Environment::push_with_lock(&HashMap::new(), None, lock_guard);

    let build_dir = {
        let mut compilation_cx = compilation_cx.lock().unwrap();
        // Since Cargo build routine will try to regenerate the unit dep graph,
        // we need to clear the existing dep graph.
        compilation_cx.build_plan.clear();

        compilation_cx.build_dir.as_ref().unwrap().clone()
    };

    // Note that this may not be equal build_dir when inside a workspace member
    let manifest_path = important_paths::find_root_manifest_for_wd(&build_dir)?;
    trace!("root manifest_path: {:?}", &manifest_path);
    // Cargo constructs relative paths from the manifest dir, so we have to pop "Cargo.toml"
    let manifest_dir = manifest_path.parent().unwrap();

    let mut shell = Shell::from_write(Box::new(BufWriter(Arc::clone(&out))));
    shell.set_verbosity(Verbosity::Quiet);

    let config = {
        let rls_config = rls_config.lock().unwrap();

        let target_dir = rls_config.target_dir.as_ref().as_ref().map(|p| p as &Path);
        make_cargo_config(manifest_dir, target_dir, restore_env.get_old_cwd(), shell)
    };

    enable_nightly_features();
    let ws = Workspace::new(&manifest_path, &config)?;

    let packages = match package_arg {
        PackageArg::Unknown | PackageArg::All => vec![],
        PackageArg::Package(s) => vec![s]
    };

    // TODO: It might be feasible to keep this CargoOptions structure cached and regenerate
    // it on every relevant configuration change
    let (opts, rustflags, clear_env_rust_log, cfg_test) =
        {
            // We mustn't lock configuration for the whole build process
            let rls_config = rls_config.lock().unwrap();

            let opts = CargoOptions::new(&rls_config);
            trace!("Cargo compilation options:\n{:?}", opts);
            let rustflags = prepare_cargo_rustflags(&rls_config);

            for package in &packages {
                if ws.members().find(|x| *x.name() == *package).is_none() {
                    warn!("cargo - couldn't find member package `{}` specified in `analyze_package` configuration", package);
                }
            }

            (opts, rustflags, rls_config.clear_env_rust_log, rls_config.cfg_test)
        };

    let spec = Packages::from_flags(false, Vec::new(), packages)?;

    let compile_opts = CompileOptions {
        spec,
        filter: CompileFilter::new(
            opts.lib,
            opts.bin,
            opts.bins,
            // TODO: Support more crate target types
            Vec::new(),
            cfg_test, // Check all integration tests under tests/
            Vec::new(),
            false,
            Vec::new(),
            false,
            opts.all_targets,
        ),
        build_config: BuildConfig::new(
            &config,
            opts.jobs,
            &opts.target,
            CompileMode::Check { test: cfg_test },
        )?,
        features: opts.features,
        all_features: opts.all_features,
        no_default_features: opts.no_default_features,
        ..CompileOptions::new(&config, CompileMode::Check { test: cfg_test })?
    };

    // Create a custom environment for running cargo, the environment is reset
    // afterwards automatically
    restore_env.push_var("RUSTFLAGS", &Some(rustflags.into()));

    if clear_env_rust_log {
        restore_env.push_var("RUST_LOG", &None);
    }

    let reached_primary = Arc::new(AtomicBool::new(false));

    let exec = RlsExecutor::new(
        &ws,
        Arc::clone(&compilation_cx),
        rls_config,
        inner_lock,
        vfs,
        compiler_messages,
        analysis,
        progress_sender,
        reached_primary.clone(),
    );

    match compile_with_exec(&ws, &compile_opts, Arc::new(exec)) {
        Ok(_) => {
            trace!(
                "Created build plan after Cargo compilation routine: {:?}",
                compilation_cx.lock().unwrap().build_plan
            );
        }
        Err(e) => {
            debug!("Error running compile_with_exec: {:?}", e);
        }
    }

    if !reached_primary.load(Ordering::SeqCst) {
        return Err(format_err!("Error compiling dependent crate"))
    }

    Ok(compilation_cx.lock().unwrap().cwd.clone().unwrap_or_else(|| {
        restore_env.get_old_cwd().to_path_buf()
    }))
}

struct RlsExecutor {
    compilation_cx: Arc<Mutex<CompilationContext>>,
    config: Arc<Mutex<Config>>,
    /// Because of the Cargo API design, we first acquire outer lock before creating the executor
    /// and calling the compilation function. This, resulting, inner lock is used to synchronize
    /// env var access during underlying `rustc()` calls during parallel `exec()` callback threads.
    env_lock: environment::InnerLock,
    vfs: Arc<Vfs>,
    analysis: Arc<Mutex<Vec<Analysis>>>,
    /// Packages which are directly a member of the workspace, for which
    /// analysis and diagnostics will be provided
    member_packages: Mutex<HashSet<PackageId>>,
    /// JSON compiler messages emitted for each primary compiled crate
    compiler_messages: Arc<Mutex<Vec<String>>>,
    progress_sender: Mutex<Sender<ProgressUpdate>>,
    /// Set to true if attempt to compile a primary crate. If we don't track
    /// this then errors which prevent giving type info won't be shown to the
    /// user. This feels a bit hacky, but I can't see how to otherwise
    /// distinguish compile errors on dependent crates from the primary crate
    /// (which are handled directly by the RLS).
    reached_primary: Arc<AtomicBool>,
}

impl RlsExecutor {
    fn new(
        ws: &Workspace,
        compilation_cx: Arc<Mutex<CompilationContext>>,
        config: Arc<Mutex<Config>>,
        env_lock: environment::InnerLock,
        vfs: Arc<Vfs>,
        compiler_messages: Arc<Mutex<Vec<String>>>,
        analysis: Arc<Mutex<Vec<Analysis>>>,
        progress_sender: Sender<ProgressUpdate>,
        reached_primary: Arc<AtomicBool>,
    ) -> RlsExecutor {
        let member_packages = ws.members().map(|x| x.package_id().clone()).collect();

        RlsExecutor {
            compilation_cx,
            config,
            env_lock,
            vfs,
            analysis,
            member_packages: Mutex::new(member_packages),
            compiler_messages,
            progress_sender: Mutex::new(progress_sender),
            reached_primary,
        }
    }

    /// Returns whether a given package is a primary one (every member of the
    /// workspace is considered as such).
    fn is_primary_crate(&self, id: &PackageId) -> bool {
        self.member_packages.lock().unwrap().contains(id)
    }
}

impl Executor for RlsExecutor {
    /// Called after a rustc process invocation is prepared up-front for a given
    /// unit of work (may still be modified for runtime-known dependencies, when
    /// the work is actually executed). This is called even for a target that
    /// is fresh and won't be compiled.
    fn init(&self, cx: &Context, unit: &Unit) {
        let mut compilation_cx = self.compilation_cx.lock().unwrap();
        let plan = &mut compilation_cx.build_plan;
        let only_primary = |unit: &Unit| self.is_primary_crate(unit.pkg.package_id());

        if let Err(err) = plan.emplace_dep_with_filter(unit, cx, &only_primary) {
            error!("{:?}", err);
        }
    }

    fn force_rebuild(&self, unit: &Unit) -> bool {
        // We need to force rebuild every package in the
        // workspace, even if it's not dirty at a time, to cache compiler
        // invocations in the build plan.
        // We only do a cargo build if we want to force rebuild the last
        // crate (e.g., because some args changed). Therefore we should
        // always force rebuild the primary crate.
        let id = unit.pkg.package_id();
        // FIXME build scripts - this will force rebuild build scripts as
        // well as the primary crate. But this is not too bad - it means
        // we will rarely rebuild more than we have to.
        self.is_primary_crate(id)
    }

    fn exec(&self, mut cargo_cmd: ProcessBuilder, id: &PackageId, target: &Target) -> CargoResult<()> {
        // Use JSON output so that we can parse the rustc output.
        cargo_cmd.arg("--error-format=json");
        // Delete any stale data. We try and remove any json files with
        // the same crate name as Cargo would emit. This includes files
        // with the same crate name but different hashes, e.g., those
        // made with a different compiler.
        let cargo_args = cargo_cmd.get_args();
        let crate_name =
            parse_arg(cargo_args, "--crate-name").expect("no crate-name in rustc command line");
        trace!("exec: {} {:?}", crate_name, cargo_cmd);

        // Send off a window/progress notification for this compile target.
        // At the moment, we don't know the number of things cargo is going to compile,
        // so we just send the name of each thing we find.
        {
            let progress_sender = self.progress_sender.lock().unwrap();
            progress_sender.send(ProgressUpdate::Message(crate_name.clone()))
                .expect("Failed to send progress update");
        }

        let out_dir = parse_arg(cargo_args, "--out-dir").expect("no out-dir in rustc command line");
        let analysis_dir = Path::new(&out_dir).join("save-analysis");
        if let Ok(dir_contents) = read_dir(&analysis_dir) {
            let lib_crate_name = "lib".to_owned() + &crate_name;
            for entry in dir_contents {
                let entry = entry.expect("unexpected error reading save-analysis directory");
                let name = entry.file_name();
                let name = name.to_str().unwrap();
                if (name.starts_with(&crate_name) || name.starts_with(&lib_crate_name))
                    && name.ends_with(".json")
                {
                    if let Err(e) = remove_file(entry.path()) {
                        debug!("Error deleting file, {}: {}", name, e);
                    }
                }
            }
        }

        // Prepare our own call to `rustc` as follows:
        // 1. Use $RUSTC wrapper if specified, otherwise use RLS executable
        //    as an rustc shim (needed to distribute via the stable channel)
        // 2. For non-primary packages or build scripts, execute the call
        // 3. Otherwise, we'll want to use the compilation to drive the analysis:
        //    i.  Modify arguments to account for the RLS settings (e.g.
        //        compiling under cfg(test) mode or passing a custom sysroot)
        //    ii. Execute the call and store the final args/envs to be used for
        //        later in-process execution of the compiler
        let mut cmd = cargo_cmd.clone();

        // RLS executable can be spawned in a different directory than the one
        // that Cargo was spawned in, so be sure to use absolute RLS path (which
        // env::current_exe() returns) for the shim.
        let rustc_shim = env::var("RUSTC").ok()
            .or_else(|| env::current_exe().ok().and_then(|x| x.to_str().map(String::from)))
            .expect("Couldn't set executable for RLS rustc shim");
        cmd.program(rustc_shim);
        cmd.env(crate::RUSTC_SHIM_ENV_VAR_NAME, "1");

        // Add args and envs to cmd.
        let mut args: Vec<_> = cargo_args
            .iter()
            .map(|a| a.clone().into_string().unwrap())
            .collect();
        let envs = cargo_cmd.get_envs().clone();

        let sysroot =
            current_sysroot().expect("need to specify SYSROOT env var or use rustup or multirust");

        {
            let config = self.config.lock().unwrap();
            if config.sysroot.is_none() {
                args.push("--sysroot".to_owned());
                args.push(sysroot);
            }
        }
        cmd.args_replace(&args);
        for (k, v) in &envs {
            if let Some(v) = v {
                cmd.env(k, v);
            }
        }

        // We only want to intercept rustc call targeting current crate to cache
        // args/envs generated by cargo so we can run only rustc later ourselves
        // Currently we don't cache nor modify build script args
        let is_build_script = *target.kind() == TargetKind::CustomBuild;
        if !self.is_primary_crate(id) || is_build_script {
            let build_script_notice = if is_build_script {
                " (build script)"
            } else {
                ""
            };
            trace!(
                "rustc not intercepted - {}{} - args: {:?} envs: {:?}",
                id.name(),
                build_script_notice,
                cmd.get_args(),
                cmd.get_envs(),
            );

            if rls_blacklist::CRATE_BLACKLIST.contains(&&*crate_name) {
                // By running the original command (rather than using our shim), we
                // avoid producing save-analysis data.
                trace!("crate is blacklisted");
                return cargo_cmd.exec();
            }
            // Only include public symbols in externally compiled deps data
            let mut save_config = rls_data::config::Config::default();
            save_config.pub_only = true;
            save_config.reachable_only = true;
            let save_config = serde_json::to_string(&save_config)?;
            cmd.env("RUST_SAVE_ANALYSIS_CONFIG", &OsString::from(save_config));

            return cmd.exec();
        }

        trace!(
            "rustc intercepted - args: {:?} envs: {:?}",
            args,
            envs,
        );

        self.reached_primary.store(true, Ordering::SeqCst);

        // Cache executed command for the build plan
        {
            let mut cx = self.compilation_cx.lock().unwrap();
            cx.build_plan.cache_compiler_job(id, target, &cmd);
        }

        // Prepare modified cargo-generated args/envs for future rustc calls
        let rustc = cargo_cmd.get_program().to_owned().into_string().unwrap();
        args.insert(0, rustc);

        // Store the modified cargo-generated args/envs for future rustc calls
        {
            let mut compilation_cx = self.compilation_cx.lock().unwrap();
            compilation_cx.args = args.clone();
            compilation_cx.envs = envs.clone();
            compilation_cx.cwd = cargo_cmd.get_cwd().map(|p| p.to_path_buf());
        }

        let build_dir = {
            let cx = self.compilation_cx.lock().unwrap();
            cx.build_dir.clone().unwrap()
        };

        if let BuildResult::Success(_, mut messages, mut analysis, success) = super::rustc::rustc(
            &self.vfs,
            &args,
            &envs,
            cargo_cmd.get_cwd(),
            &build_dir,
            Arc::clone(&self.config),
            &self.env_lock.as_facade(),
        ) {
            self.compiler_messages.lock().unwrap().append(&mut messages);
            self.analysis.lock().unwrap().append(&mut analysis);

            if !success {
                return Err(format_err!("Build error"));
            }
        }

        Ok(())
    }
}

#[derive(Debug)]
struct CargoOptions {
    target: Option<String>,
    lib: bool,
    bin: Vec<String>,
    bins: bool,
    all_features: bool,
    no_default_features: bool,
    features: Vec<String>,
    jobs: Option<u32>,
    all_targets: bool,
}

impl Default for CargoOptions {
    fn default() -> CargoOptions {
        CargoOptions {
            target: None,
            lib: false,
            bin: vec![],
            bins: false,
            all_features: false,
            no_default_features: false,
            features: vec![],
            jobs: None,
            all_targets: false
        }
    }
}

impl CargoOptions {
    fn new(config: &Config) -> CargoOptions {
        CargoOptions {
            target: config.target.clone(),
            features: config.features.clone(),
            all_features: config.all_features,
            no_default_features: config.no_default_features,
            jobs: config.jobs,
            all_targets: config.all_targets,
            ..CargoOptions::default()
        }
    }
}

fn prepare_cargo_rustflags(config: &Config) -> String {
    let mut flags = env::var("RUSTFLAGS").unwrap_or_else(|_| String::new());

    if let Some(config_flags) = &config.rustflags {
        write!(flags, " {}", config_flags.as_str()).unwrap();
    }

    if let Some(sysroot) = &config.sysroot {
        write!(flags, " --sysroot {}", sysroot).unwrap();
    }

    dedup_flags(&flags)
}

/// Construct a cargo configuration for the given build and target directories
/// and shell.
pub fn make_cargo_config(build_dir: &Path,
                         target_dir: Option<&Path>,
                         cwd: &Path,
                         shell: Shell) -> CargoConfig {
    let config = CargoConfig::new(
        shell,
        cwd.to_path_buf(),
        homedir(build_dir).unwrap(),
    );

    // Cargo is expecting the config to come from a config file and keeps
    // track of the path to that file. We'll make one up, it shouldn't be
    // used for much. Cargo does use it for finding a root path. Since
    // we pass an absolute path for the build directory, that doesn't
    // matter too much. However, Cargo still takes the grandparent of this
    // path, so we need to have at least two path elements.
    let config_path = build_dir.join("config").join("rls-config.toml");

    let mut config_value_map = config.load_values().unwrap();
    {
        let build_value = config_value_map
            .entry("build".to_owned())
            .or_insert_with(|| ConfigValue::Table(HashMap::new(), config_path.clone()));

        let target_dir = target_dir
            .map(|d| d.to_str().unwrap().to_owned())
            .unwrap_or_else(|| {
                build_dir
                    .join("target")
                    .join("rls")
                    .to_str()
                    .unwrap()
                    .to_owned()
            });
        let td_value = ConfigValue::String(target_dir, config_path);
        if let ConfigValue::Table(ref mut build_table, _) = *build_value {
            build_table.insert("target-dir".to_owned(), td_value);
        } else {
            unreachable!();
        }
    }

    config.set_values(config_value_map).unwrap();
    config
}

fn parse_arg(args: &[OsString], arg: &str) -> Option<String> {
    for (i, a) in args.iter().enumerate() {
        if a == arg {
            return Some(args[i + 1].clone().into_string().unwrap());
        }
    }
    None
}

fn current_sysroot() -> Option<String> {
    let home = env::var("RUSTUP_HOME").or_else(|_| env::var("MULTIRUST_HOME"));
    let toolchain = env::var("RUSTUP_TOOLCHAIN").or_else(|_| env::var("MULTIRUST_TOOLCHAIN"));
    if let (Ok(home), Ok(toolchain)) = (home, toolchain) {
        Some(format!("{}/toolchains/{}", home, toolchain))
    } else {
        let rustc_exe = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
        env::var("SYSROOT").map(|s| s.to_owned()).ok().or_else(|| {
            Command::new(rustc_exe)
                .arg("--print")
                .arg("sysroot")
                .output()
                .ok()
                .and_then(|out| String::from_utf8(out.stdout).ok())
                .map(|s| s.trim().to_owned())
        })
    }
}

/// `flag_str` is a string of command line args for Rust. This function removes any
/// duplicate flags.
fn dedup_flags(flag_str: &str) -> String {
    // The basic strategy here is that we split flag_str into a set of keys and
    // values and dedup any duplicate keys, using the last value in flag_str.
    // This is a bit complicated because of the variety of ways args can be specified.

    // Retain flags order to prevent complete project rebuild due to RUSTFLAGS fingerprint change
    let mut flags = BTreeMap::new();
    let mut bits = flag_str.split_whitespace().peekable();

    while let Some(bit) = bits.next() {
        let mut bit = bit.to_owned();
        // Handle `-Z foo` the same way as `-Zfoo`.
        if bit.len() == 2 && bits.peek().is_some() && !bits.peek().unwrap().starts_with('-') {
            let bit_clone = bit.clone();
            let mut bit_chars = bit_clone.chars();
            if bit_chars.next().unwrap() == '-' && bit_chars.next().unwrap() != '-' {
                bit.push_str(bits.next().unwrap());
            }
        }

        if bit.starts_with('-') {
            if bit.contains('=') {
                // Split only on the first equals sign (there may be
                // more than one)
                let bits: Vec<_> = bit.splitn(2, '=').collect();
                assert!(bits.len() == 2);
                flags.insert(bits[0].to_owned() + "=", bits[1].to_owned());
            } else if bits.peek().is_some() && !bits.peek().unwrap().starts_with('-') {
                flags.insert(bit, bits.next().unwrap().to_owned());
            } else {
                flags.insert(bit, String::new());
            }
        } else {
            // A standalone arg with no flag, no deduplication to do. We merge these
            // together, which is probably not ideal, but is simple.
            flags
                .entry(String::new())
                .or_insert_with(String::new)
                .push_str(&format!(" {}", bit));
        }
    }

    // Put the map back together as a string.
    let mut result = String::new();
    for (k, v) in &flags {
        if k.is_empty() {
            result.push_str(v);
        } else {
            result.push(' ');
            result.push_str(k);
            if !v.is_empty() {
                if !k.ends_with('=') {
                    result.push(' ');
                }
                result.push_str(v);
            }
        }
    }
    result
}

#[cfg(test)]
mod test {
    use super::dedup_flags;

    #[test]
    fn test_dedup_flags() {
        // These should all be preserved.
        assert!(dedup_flags("") == "");
        assert!(dedup_flags("-Zfoo") == " -Zfoo");
        assert!(dedup_flags("-Z foo") == " -Zfoo");
        assert!(dedup_flags("-Zfoo bar") == " -Zfoo bar");
        let result = dedup_flags("-Z foo foo bar");
        assert!(result.matches("foo").count() == 2);
        assert!(result.matches("bar").count() == 1);

        // These should dedup.
        assert!(dedup_flags("-Zfoo -Zfoo") == " -Zfoo");
        assert!(dedup_flags("-Zfoo -Zfoo -Zfoo") == " -Zfoo");
        let result = dedup_flags("-Zfoo -Zfoo -Zbar");
        assert!(result.matches("foo").count() == 1);
        assert!(result.matches("bar").count() == 1);
        let result = dedup_flags("-Zfoo -Zbar -Zfoo -Zbar -Zbar");
        assert!(result.matches("foo").count() == 1);
        assert!(result.matches("bar").count() == 1);
        assert!(dedup_flags("-Zfoo -Z foo") == " -Zfoo");

        assert!(dedup_flags("--error-format=json --error-format=json") == " --error-format=json");
        assert!(dedup_flags("--error-format=foo --error-format=json") == " --error-format=json");

        assert!(
            dedup_flags(
                "-C link-args=-fuse-ld=gold -C target-cpu=native -C link-args=-fuse-ld=gold"
            ) == " -Clink-args=-fuse-ld=gold -Ctarget-cpu=native"
        );
    }
}
