//! Support for cross-compile tests with the `--target` flag.
//!
//! Note that cross-testing is very limited. You need to install the
//! "alternate" target to the host (32-bit for 64-bit hosts or vice-versa).
//!
//! Set `CFG_DISABLE_CROSS_TESTS=1` environment variable to disable these tests
//! if you are unable to use the alternate target. Unfortunately 32-bit
//! support on macOS is going away, so macOS users are out of luck.
//!
//! These tests are all disabled on rust-lang/rust's CI, but run in Cargo's CI.

use crate::prelude::*;
use cargo_test_support::{basic_manifest, cross_compile::alternate, main_file, project};
use cargo_util::ProcessError;
use std::fmt::Write;
use std::{
    process::{Command, Output},
    sync::{
        Once,
        atomic::{AtomicBool, Ordering},
    },
};

/// Whether or not the resulting cross binaries can run on the host.
static CAN_RUN_ON_HOST: AtomicBool = AtomicBool::new(false);

pub fn disabled() -> bool {
    // First, disable if requested.
    match std::env::var("CFG_DISABLE_CROSS_TESTS") {
        Ok(ref s) if *s == "1" => return true,
        _ => {}
    }

    // It requires setting `target.linker` for cross-compilation to work on aarch64,
    // so not going to bother now.
    if cfg!(all(target_arch = "aarch64", target_os = "linux")) {
        return true;
    }

    // Cross tests are only tested to work on macos, linux, and MSVC windows.
    if !(cfg!(target_os = "macos") || cfg!(target_os = "linux") || cfg!(target_env = "msvc")) {
        return true;
    }

    // It's not particularly common to have a cross-compilation setup, so
    // try to detect that before we fail a bunch of tests through no fault
    // of the user.
    static CAN_BUILD_CROSS_TESTS: AtomicBool = AtomicBool::new(false);
    static CHECK: Once = Once::new();

    let cross_target = alternate();

    let run_cross_test = || -> anyhow::Result<Output> {
        let p = project()
            .at("cross_test")
            .file("Cargo.toml", &basic_manifest("cross_test", "1.0.0"))
            .file("src/main.rs", &main_file(r#""testing!""#, &[]))
            .build();

        let build_result = p
            .cargo("build --target")
            .arg(&cross_target)
            .exec_with_output();

        if build_result.is_ok() {
            CAN_BUILD_CROSS_TESTS.store(true, Ordering::SeqCst);
        }

        let result = p
            .cargo("run --target")
            .arg(&cross_target)
            .exec_with_output();

        if result.is_ok() {
            CAN_RUN_ON_HOST.store(true, Ordering::SeqCst);
        }
        build_result
    };

    CHECK.call_once(|| {
        drop(run_cross_test());
    });

    if CAN_BUILD_CROSS_TESTS.load(Ordering::SeqCst) {
        // We were able to compile a simple project, so the user has the
        // necessary `std::` bits installed. Therefore, tests should not
        // be disabled.
        return false;
    }

    // We can't compile a simple cross project. We want to warn the user
    // by failing a single test and having the remainder of the cross tests
    // pass. We don't use `std::sync::Once` here because panicking inside its
    // `call_once` method would poison the `Once` instance, which is not what
    // we want.
    static HAVE_WARNED: AtomicBool = AtomicBool::new(false);

    if HAVE_WARNED.swap(true, Ordering::SeqCst) {
        // We are some other test and somebody else is handling the warning.
        // Just disable the current test.
        return true;
    }

    // We are responsible for warning the user, which we do by panicking.
    let mut message = format!(
        "
    Cannot cross compile to {}.

    This failure can be safely ignored. If you would prefer to not see this
    failure, you can set the environment variable CFG_DISABLE_CROSS_TESTS to \"1\".

    Alternatively, you can install the necessary libraries to enable cross
    compilation tests. Cross compilation tests depend on your host platform.
    ",
        cross_target
    );

    if cfg!(target_os = "linux") {
        message.push_str(
            "
    Linux cross tests target i686-unknown-linux-gnu, which requires the ability to
    build and run 32-bit targets. This requires the 32-bit libraries to be
    installed. For example, on Ubuntu, run `sudo apt install gcc-multilib` to
    install the necessary libraries.
    ",
        );
    } else if cfg!(target_os = "macos") {
        message.push_str(
            "
    macOS on aarch64 cross tests to target x86_64-apple-darwin.
    This should be natively supported via Xcode, nothing additional besides the
    rustup target should be needed.
    ",
        );
    } else if cfg!(target_os = "windows") {
        message.push_str(
            "
    Windows cross tests target i686-pc-windows-msvc, which requires the ability
    to build and run 32-bit targets. This should work automatically if you have
    properly installed Visual Studio build tools.
    ",
        );
    } else {
        // The check at the top should prevent this.
        panic!("platform should have been skipped");
    }

    let rustup_available = Command::new("rustup").output().is_ok();
    if rustup_available {
        write!(
            message,
            "
    Make sure that the appropriate `rustc` target is installed with rustup:

        rustup target add {}
    ",
            cross_target
        )
        .unwrap();
    } else {
        write!(
            message,
            "
    rustup does not appear to be installed. Make sure that the appropriate
    `rustc` target is installed for the target `{}`.
    ",
            cross_target
        )
        .unwrap();
    }

    // Show the actual error message.
    match run_cross_test() {
        Ok(_) => message.push_str("\nUh oh, second run succeeded?\n"),
        Err(err) => match err.downcast_ref::<ProcessError>() {
            Some(proc_err) => write!(message, "\nTest error: {}\n", proc_err).unwrap(),
            None => write!(message, "\nUnexpected non-process error: {}\n", err).unwrap(),
        },
    }

    panic!("{}", message);
}

/// Whether or not the host can run cross-compiled executables.
pub fn can_run_on_host() -> bool {
    if disabled() {
        return false;
    }
    assert!(CAN_RUN_ON_HOST.load(Ordering::SeqCst));
    return true;
}
