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