#
cargo_test_support/
cross_compile.rs

    1//! Support for cross-compile tests with the `--target` flag.
    2//!
    3//! Note that cross-testing is very limited. You need to install the
    4//! "alternate" target to the host (32-bit for 64-bit hosts or vice-versa).
    5//!
    6//! Set `CFG_DISABLE_CROSS_TESTS=1` environment variable to disable these tests
    7//! if you are unable to use the alternate target. Unfortunately 32-bit
    8//! support on macOS is going away, so macOS users are out of luck.
    9//!
    10//! These tests are all disabled on rust-lang/rust's CI, but run in Cargo's CI.
    11
    12use crate::{basic_manifest, main_file, project};
    13use cargo_util::ProcessError;
    14use std::env;
    15use std::fmt::Write;
    16use std::process::{Command, Output};
    17use std::sync::atomic::{AtomicBool, Ordering};
    18use std::sync::Once;
    19
    20/// Whether or not the resulting cross binaries can run on the host.
    21static CAN_RUN_ON_HOST: AtomicBool = AtomicBool::new(false);
    22
    23pub fn disabled() -> bool {
    24    // First, disable if requested.
    25    match env::var("CFG_DISABLE_CROSS_TESTS") {
    26        Ok(ref s) if *s == "1" => return true,
    27        _ => {}
    28    }
    29
    30    // It requires setting `target.linker` for cross-compilation to work on aarch64,
    31    // so not going to bother now.
    32    if cfg!(all(target_arch = "aarch64", target_os = "linux")) {
    33        return true;
    34    }
    35
    36    // Cross tests are only tested to work on macos, linux, and MSVC windows.
    37    if !(cfg!(target_os = "macos") || cfg!(target_os = "linux") || cfg!(target_env = "msvc")) {
    38        return true;
    39    }
    40
    41    // It's not particularly common to have a cross-compilation setup, so
    42    // try to detect that before we fail a bunch of tests through no fault
    43    // of the user.
    44    static CAN_BUILD_CROSS_TESTS: AtomicBool = AtomicBool::new(false);
    45    static CHECK: Once = Once::new();
    46
    47    let cross_target = alternate();
    48
    49    let run_cross_test = || -> anyhow::Result<Output> {
    50        let p = project()
    51            .at("cross_test")
    52            .file("Cargo.toml", &basic_manifest("cross_test", "1.0.0"))
    53            .file("src/main.rs", &main_file(r#""testing!""#, &[]))
    54            .build();
    55
    56        let build_result = p
    57            .cargo("build --target")
    58            .arg(&cross_target)
    59            .exec_with_output();
    60
    61        if build_result.is_ok() {
    62            CAN_BUILD_CROSS_TESTS.store(true, Ordering::SeqCst);
    63        }
    64
    65        let result = p
    66            .cargo("run --target")
    67            .arg(&cross_target)
    68            .exec_with_output();
    69
    70        if result.is_ok() {
    71            CAN_RUN_ON_HOST.store(true, Ordering::SeqCst);
    72        }
    73        build_result
    74    };
    75
    76    CHECK.call_once(|| {
    77        drop(run_cross_test());
    78    });
    79
    80    if CAN_BUILD_CROSS_TESTS.load(Ordering::SeqCst) {
    81        // We were able to compile a simple project, so the user has the
    82        // necessary `std::` bits installed. Therefore, tests should not
    83        // be disabled.
    84        return false;
    85    }
    86
    87    // We can't compile a simple cross project. We want to warn the user
    88    // by failing a single test and having the remainder of the cross tests
    89    // pass. We don't use `std::sync::Once` here because panicking inside its
    90    // `call_once` method would poison the `Once` instance, which is not what
    91    // we want.
    92    static HAVE_WARNED: AtomicBool = AtomicBool::new(false);
    93
    94    if HAVE_WARNED.swap(true, Ordering::SeqCst) {
    95        // We are some other test and somebody else is handling the warning.
    96        // Just disable the current test.
    97        return true;
    98    }
    99
    100    // We are responsible for warning the user, which we do by panicking.
    101    let mut message = format!(
    102        "
    103Cannot cross compile to {}.
    104
    105This failure can be safely ignored. If you would prefer to not see this
    106failure, you can set the environment variable CFG_DISABLE_CROSS_TESTS to \"1\".
    107
    108Alternatively, you can install the necessary libraries to enable cross
    109compilation tests. Cross compilation tests depend on your host platform.
    110",
    111        cross_target
    112    );
    113
    114    if cfg!(target_os = "linux") {
    115        message.push_str(
    116            "
    117Linux cross tests target i686-unknown-linux-gnu, which requires the ability to
    118build and run 32-bit targets. This requires the 32-bit libraries to be
    119installed. For example, on Ubuntu, run `sudo apt install gcc-multilib` to
    120install the necessary libraries.
    121",
    122        );
    123    } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
    124        message.push_str(
    125            "
    126macOS on aarch64 cross tests to target x86_64-apple-darwin.
    127This should be natively supported via Xcode, nothing additional besides the
    128rustup target should be needed.
    129",
    130        );
    131    } else if cfg!(target_os = "macos") {
    132        message.push_str(
    133            "
    134macOS on x86_64 cross tests to target x86_64-apple-ios, which requires the iOS
    135SDK to be installed. This should be included with Xcode automatically. If you
    136are using the Xcode command line tools, you'll need to install the full Xcode
    137app (from the Apple App Store), and switch to it with this command:
    138
    139    sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
    140
    141Some cross-tests want to *run* the executables on the host. These tests will
    142be ignored if this is not possible. On macOS, this means you need an iOS
    143simulator installed to run these tests. To install a simulator, open Xcode, go
    144to preferences > Components, and download the latest iOS simulator.
    145",
    146        );
    147    } else if cfg!(target_os = "windows") {
    148        message.push_str(
    149            "
    150Windows cross tests target i686-pc-windows-msvc, which requires the ability
    151to build and run 32-bit targets. This should work automatically if you have
    152properly installed Visual Studio build tools.
    153",
    154        );
    155    } else {
    156        // The check at the top should prevent this.
    157        panic!("platform should have been skipped");
    158    }
    159
    160    let rustup_available = Command::new("rustup").output().is_ok();
    161    if rustup_available {
    162        write!(
    163            message,
    164            "
    165Make sure that the appropriate `rustc` target is installed with rustup:
    166
    167    rustup target add {}
    168",
    169            cross_target
    170        )
    171        .unwrap();
    172    } else {
    173        write!(
    174            message,
    175            "
    176rustup does not appear to be installed. Make sure that the appropriate
    177`rustc` target is installed for the target `{}`.
    178",
    179            cross_target
    180        )
    181        .unwrap();
    182    }
    183
    184    // Show the actual error message.
    185    match run_cross_test() {
    186        Ok(_) => message.push_str("\nUh oh, second run succeeded?\n"),
    187        Err(err) => match err.downcast_ref::<ProcessError>() {
    188            Some(proc_err) => write!(message, "\nTest error: {}\n", proc_err).unwrap(),
    189            None => write!(message, "\nUnexpected non-process error: {}\n", err).unwrap(),
    190        },
    191    }
    192
    193    panic!("{}", message);
    194}
    195
    196/// The arch triple of the test-running host.
    197pub fn native() -> &'static str {
    198    env!("NATIVE_ARCH")
    199}
    200
    201pub fn native_arch() -> &'static str {
    202    match native()
    203        .split("-")
    204        .next()
    205        .expect("Target triple has unexpected format")
    206    {
    207        "x86_64" => "x86_64",
    208        "aarch64" => "aarch64",
    209        "i686" => "x86",
    210        _ => panic!("This test should be gated on cross_compile::disabled."),
    211    }
    212}
    213
    214/// The alternate target-triple to build with.
    215///
    216/// Only use this function on tests that check `cross_compile::disabled`.
    217pub fn alternate() -> &'static str {
    218    try_alternate().expect("This test should be gated on cross_compile::disabled.")
    219}
    220
    221/// A possible alternate target-triple to build with.
    222pub(crate) fn try_alternate() -> Option<&'static str> {
    223    if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
    224        Some("x86_64-apple-darwin")
    225    } else if cfg!(target_os = "macos") {
    226        Some("x86_64-apple-ios")
    227    } else if cfg!(target_os = "linux") {
    228        Some("i686-unknown-linux-gnu")
    229    } else if cfg!(all(target_os = "windows", target_env = "msvc")) {
    230        Some("i686-pc-windows-msvc")
    231    } else if cfg!(all(target_os = "windows", target_env = "gnu")) {
    232        Some("i686-pc-windows-gnu")
    233    } else {
    234        None
    235    }
    236}
    237
    238pub fn alternate_arch() -> &'static str {
    239    if cfg!(target_os = "macos") {
    240        "x86_64"
    241    } else {
    242        "x86"
    243    }
    244}
    245
    246/// A target-triple that is neither the host nor the target.
    247///
    248/// Rustc may not work with it and it's alright, apart from being a
    249/// valid target triple it is supposed to be used only as a
    250/// placeholder for targets that should not be considered.
    251pub fn unused() -> &'static str {
    252    "wasm32-unknown-unknown"
    253}
    254
    255/// Whether or not the host can run cross-compiled executables.
    256pub fn can_run_on_host() -> bool {
    257    if disabled() {
    258        return false;
    259    }
    260    // macos is currently configured to cross compile to x86_64-apple-ios
    261    // which requires a simulator to run. Azure's CI image appears to have the
    262    // SDK installed, but are not configured to launch iOS images with a
    263    // simulator.
    264    if cfg!(target_os = "macos") {
    265        if CAN_RUN_ON_HOST.load(Ordering::SeqCst) {
    266            return true;
    267        } else {
    268            println!("Note: Cannot run on host, skipping.");
    269            return false;
    270        }
    271    } else {
    272        assert!(CAN_RUN_ON_HOST.load(Ordering::SeqCst));
    273        return true;
    274    }
    275}
    276
    277/// Check if the given target has been installed.
    278///
    279/// Generally [`disabled`] should be used to check if cross-compilation is allowed.
    280/// And [`alternate`] to get the cross target.
    281///
    282/// You should only use this as a last resort to skip tests,
    283/// because it doesn't report skipped tests as ignored.
    284pub fn requires_target_installed(target: &str) -> bool {
    285    let has_target = std::process::Command::new("rustup")
    286        .args(["target", "list", "--installed"])
    287        .output()
    288        .ok()
    289        .map(|output| {
    290            String::from_utf8(output.stdout)
    291                .map(|stdout| stdout.contains(target))
    292                .unwrap_or_default()
    293        })
    294        .unwrap_or_default();
    295    if !has_target {
    296        let msg =
    297            format!("to run this test, run `rustup target add {target} --toolchain <toolchain>`",);
    298        if cargo_util::is_ci() {
    299            panic!("{msg}");
    300        } else {
    301            eprintln!("{msg}");
    302        }
    303    }
    304    has_target
    305}