Skip to content

Commit

Permalink
Fix cross-compiling for Apple platforms (#1389)
Browse files Browse the repository at this point in the history
  • Loading branch information
madsmtm authored Feb 8, 2025
1 parent 9a9c248 commit 23c0ee3
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 40 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ jobs:
rust: stable
target: x86_64-apple-ios-macabi
no_run: --no-run # FIXME(madsmtm): Fix running tests
- build: cross-macos-aarch64
os: ubuntu-latest
rust: stable
target: aarch64-apple-darwin
no_run: --no-run
- build: cross-ios-aarch64
os: ubuntu-latest
rust: stable
target: aarch64-apple-ios
no_run: --no-run
- build: windows-aarch64
os: windows-latest
rust: stable
Expand Down Expand Up @@ -155,6 +165,31 @@ jobs:
env:
CC: ${{ matrix.CC }}
CXX: ${{ matrix.CXX }}
- name: Install llvm tools (for llvm-ar)
if: startsWith(matrix.build, 'cross-macos') || startsWith(matrix.build, 'cross-ios')
run: sudo apt-get install llvm
- name: Download macOS SDK
if: startsWith(matrix.build, 'cross-macos')
run: |
wget https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.3.sdk.tar.xz
tar -xf MacOSX11.3.sdk.tar.xz
echo "SDKROOT=$(pwd)/MacOSX11.3.sdk" >> $GITHUB_ENV
- name: Download iOS SDK
if: startsWith(matrix.build, 'cross-ios')
run: |
wget https://github.com/xybp888/iOS-SDKs/releases/download/iOS18.1-SDKs/iPhoneOS18.1.sdk.zip
unzip iPhoneOS18.1.sdk.zip
echo "SDKROOT=$(pwd)/iPhoneOS18.1.sdk" >> $GITHUB_ENV
- name: Set up Apple cross-compilation
if: startsWith(matrix.build, 'cross-macos') || startsWith(matrix.build, 'cross-ios')
run: |
# Test with clang/llvm for now, has better cross-compilation support (GCC requires downloading a different toolchain)
echo "CC=clang" >> $GITHUB_ENV
echo "CXX=clang++" >> $GITHUB_ENV
echo "AR=llvm-ar" >> $GITHUB_ENV
# Link with rust-lld
UPPERCASE_TARGET_NAME=$(echo "${{ matrix.target }}" | tr '[:lower:]-' '[:upper:]_')
echo "CARGO_TARGET_${UPPERCASE_TARGET_NAME}_LINKER=rust-lld" >> $GITHUB_ENV
- name: setup dev environment
uses: ilammy/msvc-dev-cmd@v1
if: startsWith(matrix.build, 'windows-clang')
Expand Down
69 changes: 45 additions & 24 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2194,21 +2194,34 @@ impl Build {

// Pass `--target` with the LLVM target to configure Clang for cross-compiling.
//
// NOTE: In the past, we passed this, along with the deployment version in here
// on Apple targets, but versioned targets were found to have poor compatibility
// with older versions of Clang, especially around comes to configuration files.
// This is **required** for cross-compilation, as it's the only flag that
// consistently forces Clang to change the "toolchain" that is responsible for
// parsing target-specific flags:
// https://github.com/rust-lang/cc-rs/issues/1388
// https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/clang/lib/Driver/Driver.cpp#L1359-L1360
// https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/clang/lib/Driver/Driver.cpp#L6347-L6532
//
// Instead, we specify `-arch` along with `-mmacosx-version-min=`, `-mtargetos=`
// and similar flags in `.apple_flags()`.
// This can be confusing, because on e.g. host macOS, you can usually get by
// with `-arch` and `-mtargetos=`. But that only works because the _default_
// toolchain is `Darwin`, which enables parsing of darwin-specific options.
//
// Note that Clang errors when both `-mtargetos=` and `-target` are specified,
// so we omit this entirely on Apple targets (it's redundant when specifying
// both the `-arch` and the deployment target / OS flag) (in theory we _could_
// specify this on some of the Apple targets that use the older
// `-m*-version-min=`, but for consistency we omit it entirely).
if target.vendor != "apple" {
cmd.push_cc_arg(format!("--target={}", target.llvm_target).into());
}
// NOTE: In the past, we passed the deployment version in here on all Apple
// targets, but versioned targets were found to have poor compatibility with
// older versions of Clang, especially when it comes to configuration files:
// https://github.com/rust-lang/cc-rs/issues/1278
//
// So instead, we pass the deployment target with `-m*-version-min=`, and only
// pass it here on visionOS and Mac Catalyst where that option does not exist:
// https://github.com/rust-lang/cc-rs/issues/1383
let clang_target = if target.os == "visionos" || target.abi == "macabi" {
Cow::Owned(
target.versioned_llvm_target(&self.apple_deployment_target(target)),
)
} else {
Cow::Borrowed(target.llvm_target)
};

cmd.push_cc_arg(format!("--target={clang_target}").into());
}
}
ToolFamily::Msvc { clang_cl } => {
Expand Down Expand Up @@ -2648,21 +2661,29 @@ impl Build {
fn apple_flags(&self, cmd: &mut Tool) -> Result<(), Error> {
let target = self.get_target()?;

// Add `-arch` on all compilers. This is a Darwin/Apple-specific flag
// that works both on GCC and Clang.
// This is a Darwin/Apple-specific flag that works both on GCC and Clang, but it is only
// necessary on GCC since we specify `-target` on Clang.
// https://gcc.gnu.org/onlinedocs/gcc/Darwin-Options.html#:~:text=arch
// https://clang.llvm.org/docs/CommandGuide/clang.html#cmdoption-arch
let arch = map_darwin_target_from_rust_to_compiler_architecture(&target);
cmd.args.push("-arch".into());
cmd.args.push(arch.into());
if cmd.is_like_gnu() {
let arch = map_darwin_target_from_rust_to_compiler_architecture(&target);
cmd.args.push("-arch".into());
cmd.args.push(arch.into());
}

// Pass the deployment target via `-mmacosx-version-min=`, `-mtargetos=` and similar.
//
// It is also necessary on GCC, as it forces a compilation error if the compiler is not
// Pass the deployment target via `-mmacosx-version-min=`, `-miphoneos-version-min=` and
// similar. Also necessary on GCC, as it forces a compilation error if the compiler is not
// configured for Darwin: https://gcc.gnu.org/onlinedocs/gcc/Darwin-Options.html
let min_version = self.apple_deployment_target(&target);
cmd.args
.push(target.apple_version_flag(&min_version).into());
//
// On visionOS and Mac Catalyst, there is no -m*-version-min= flag:
// https://github.com/llvm/llvm-project/issues/88271
// And the workaround to use `-mtargetos=` cannot be used with the `--target` flag that we
// otherwise specify. So we avoid emitting that, and put the version in `--target` instead.
if cmd.is_like_gnu() || !(target.os == "visionos" || target.abi == "macabi") {
let min_version = self.apple_deployment_target(&target);
cmd.args
.push(target.apple_version_flag(&min_version).into());
}

// AppleClang sometimes requires sysroot even on macOS
if cmd.is_xctoolchain_clang() || target.os != "macos" {
Expand Down
23 changes: 23 additions & 0 deletions src/target/llvm.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
use super::TargetInfo;

impl TargetInfo<'_> {
/// The versioned LLVM/Clang target triple.
pub(crate) fn versioned_llvm_target(&self, version: &str) -> String {
// Only support versioned Apple targets for now.
assert_eq!(self.vendor, "apple");

let mut components = self.llvm_target.split("-");
let arch = components.next().expect("llvm_target should have arch");
let vendor = components.next().expect("llvm_target should have vendor");
let os = components.next().expect("LLVM target should have os");
let environment = components.next();
assert_eq!(components.next(), None, "too many LLVM target components");

if let Some(env) = environment {
format!("{arch}-{vendor}-{os}{version}-{env}")
} else {
format!("{arch}-{vendor}-{os}{version}")
}
}
}

/// Rust and Clang don't really agree on naming, so do a best-effort
/// conversion to support out-of-tree / custom target-spec targets.
pub(crate) fn guess_llvm_target_triple(
Expand Down
102 changes: 86 additions & 16 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,28 +524,99 @@ fn asm_flags() {
}

#[test]
fn gnu_apple_darwin() {
for (arch, ld64_arch, version) in &[("x86_64", "x86_64", "10.7"), ("aarch64", "arm64", "11.0")]
{
let target = format!("{}-apple-darwin", arch);
fn gnu_apple_sysroot() {
let targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"];

for target in targets {
let test = Test::gnu();
test.shim("fake-gcc")
.gcc()
.compiler("fake-gcc")
.target(&target)
.host(&target)
// Avoid test maintenance when minimum supported OSes change.
.__set_env("MACOSX_DEPLOYMENT_TARGET", version)
.file("foo.c")
.compile("foo");

let cmd = test.cmd(0);
cmd.must_have_in_order("-arch", ld64_arch);
cmd.must_have(format!("-mmacosx-version-min={version}"));
cmd.must_not_have("-isysroot");
}
}

#[test]
#[cfg(target_os = "macos")] // Invokes xcrun
fn gnu_apple_arch() {
let cases = [
("x86_64-apple-darwin", "x86_64"),
("x86_64h-apple-darwin", "x86_64h"),
("aarch64-apple-darwin", "arm64"),
("arm64e-apple-darwin", "arm64e"),
("i686-apple-darwin", "i386"),
("aarch64-apple-ios", "arm64"),
("armv7s-apple-ios", "armv7s"),
("arm64_32-apple-watchos", "arm64_32"),
("armv7k-apple-watchos", "armv7k"),
];

for (target, arch) in cases {
let test = Test::gnu();
test.shim("fake-gcc")
.gcc()
.compiler("fake-gcc")
.target(&target)
.host(&"aarch64-apple-darwin")
.file("foo.c")
.compile("foo");

let cmd = test.cmd(0);
cmd.must_have_in_order("-arch", arch);
}
}

#[test]
#[cfg(target_os = "macos")] // Invokes xcrun
fn gnu_apple_deployment_target() {
let cases = [
("x86_64-apple-darwin", "-mmacosx-version-min=10.12"),
("aarch64-apple-darwin", "-mmacosx-version-min=10.12"),
("aarch64-apple-ios", "-miphoneos-version-min=10.0"),
("aarch64-apple-ios-sim", "-mios-simulator-version-min=10.0"),
("x86_64-apple-ios", "-mios-simulator-version-min=10.0"),
("aarch64-apple-ios-macabi", "-mtargetos=ios10.0-macabi"),
("aarch64-apple-tvos", "-mappletvos-version-min=10.0"),
(
"aarch64-apple-tvos-sim",
"-mappletvsimulator-version-min=10.0",
),
("aarch64-apple-watchos", "-mwatchos-version-min=5.0"),
(
"aarch64-apple-watchos-sim",
"-mwatchsimulator-version-min=5.0",
),
("aarch64-apple-visionos", "-mtargetos=xros1.0"),
("aarch64-apple-visionos-sim", "-mtargetos=xros1.0-simulator"),
];

for (target, os_version_flag) in cases {
let test = Test::gnu();
test.shim("fake-gcc")
.gcc()
.compiler("fake-gcc")
.target(&target)
.host(&"aarch64-apple-darwin")
// Avoid dependency on environment in test.
.__set_env("MACOSX_DEPLOYMENT_TARGET", "10.12")
.__set_env("IPHONEOS_DEPLOYMENT_TARGET", "10.0")
.__set_env("TVOS_DEPLOYMENT_TARGET", "10.0")
.__set_env("WATCHOS_DEPLOYMENT_TARGET", "5.0")
.__set_env("XROS_DEPLOYMENT_TARGET", "1.0")
.file("foo.c")
.compile("foo");

let cmd = test.cmd(0);
cmd.must_have(os_version_flag);
}
}

#[cfg(target_os = "macos")]
#[test]
fn macos_cpp_minimums() {
Expand Down Expand Up @@ -616,7 +687,7 @@ fn clang_apple_tvos() {
.file("foo.c")
.compile("foo");

test.cmd(0).must_have_in_order("-arch", "arm64");
test.cmd(0).must_have("--target=arm64-apple-tvos");
test.cmd(0).must_have("-mappletvos-version-min=9.0");
}

Expand All @@ -640,10 +711,9 @@ fn clang_apple_mac_catalyst() {
.compile("foo");
let execution = test.cmd(0);

execution.must_have_in_order("-arch", "arm64");
execution.must_have("--target=arm64-apple-ios15.0-macabi");
// --target and -mtargetos= don't mix
execution.must_not_have("--target=arm64-apple-ios-macabi");
execution.must_have("-mtargetos=ios15.0-macabi");
execution.must_not_have("-mtargetos=");
execution.must_have_in_order("-isysroot", sdkroot);
execution.must_have_in_order(
"-isystem",
Expand Down Expand Up @@ -672,7 +742,8 @@ fn clang_apple_tvsimulator() {
.file("foo.c")
.compile("foo");

test.cmd(0).must_have_in_order("-arch", "x86_64");
test.cmd(0)
.must_have("--target=x86_64-apple-tvos-simulator");
test.cmd(0).must_have("-mappletvsimulator-version-min=9.0");
}

Expand All @@ -697,10 +768,9 @@ fn clang_apple_visionos() {

dbg!(test.cmd(0).args);

test.cmd(0).must_have_in_order("-arch", "arm64");
test.cmd(0).must_have("--target=arm64-apple-xros1.0");
// --target and -mtargetos= don't mix.
test.cmd(0).must_not_have("--target=arm64-apple-xros");
test.cmd(0).must_have("-mtargetos=xros1.0");
test.cmd(0).must_not_have("-mtargetos=");

// Flags that don't exist.
test.cmd(0).must_not_have("-mxros-version-min=1.0");
Expand Down

0 comments on commit 23c0ee3

Please # to comment.