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}