chinedufn / swift-bridge

swift-bridge facilitates Rust and Swift interop.
https://chinedufn.github.io/swift-bridge
Apache License 2.0
810 stars 58 forks source link

Multi-platform compilation example #277

Open sax opened 3 months ago

sax commented 3 months ago

I have an application where my initial platform was macosx but where I'm now adding multi-platform support. I'm not sure if this is yet stable enough to do a pull request into the guides, but I thought I'd put it here in case it's helpful to others. @chinedufn if you want to use any of this, please feel free to do so without any need for attribution.

My first attempt was just to bundle more targets into a single build artifact. I quickly discovered that lipo only allows a single artifact per architecture (aarch64 vs x86_64), and cannot combine multiple build targets on the same architecture. Therefor I decided to create a different artifact per target, and configure my XCode project to link to a specific file per target platform.

Note that I am using lipo from the coreutils package installed via Homebrew, and in the following snippets I'm replacing my real crate name with <rust-bridge> or <rust_bridge>.

My build-rust.sh script is as follows:

#!/usr/bin/env bash

##################################################
# We call this from an Xcode run script.
##################################################

set -e

export BRIDGE_CRATE=<rust-bridge>
export PATH="$HOME/.cargo/bin:$PATH:/opt/homebrew/bin"
export RUST_LIB_NAME=lib<rust_bridge>
export RUST_LIB="${RUST_LIB_NAME}.a"

### <snipped code to ensure rustc/cargo are in the PATH>

if [[ -z "${PROJECT_DIR}" ]]; then
  PROJECT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
  echo "PROJECT_DIR: ${PROJECT_DIR}"
fi

if [[ -z "$CONFIGURATION" ]]; then
  CONFIGURATION="Debug"
fi

cd $PROJECT_DIR/..

TARGETS=""
NIGHTLY="NO"
if [[ "${PLATFORM_NAME}" = "macosx" ]]; then
  TARGETS=(aarch64-apple-darwin x86_64-apple-darwin)
elif [[ "${PLATFORM_NAME}" = "iphonesimulator" ]]; then
  TARGETS=(aarch64-apple-ios-sim x86_64-apple-ios)
elif [[ "${PLATFORM_NAME}" = "iphoneos" ]]; then
  TARGETS=(aarch64-apple-ios)
elif [[ "${PLATFORM_NAME}" = "appletvsimulator" ]]; then
  TARGETS=(aarch64-apple-tvos-sim)
  NIGHTLY="YES"
elif [[ "${PLATFORM_NAME}" = "appletvos" ]]; then
  TARGETS=(aarch64-apple-tvos x86_64-apple-tvos)
  NIGHTLY="YES"
elif [[ "${PLATFORM_NAME}" = "watchsimulator" ]]; then
  TARGETS=(aarch64-apple-watchos-sim)
  NIGHTLY="YES"
elif [[ "${PLATFORM_NAME}" = "watchos" ]]; then
  TARGETS=(aarch64-apple-watchos)
  NIGHTLY="YES"
elif [[ "${PLATFORM_NAME}" = "xrsimulator" ]]; then
  TARGETS=(aarch64-apple-visionos-sim)
  NIGHTLY="YES"
elif [[ "${PLATFORM_NAME}" = "xros" ]]; then
  TARGETS=(aarch64-apple-visionos)
  NIGHTLY="YES"
else
  echo "Unsupported platform \`${PLATFORM_NAME}\`" >&2
  exit 1
fi

echo "Rust version ($(which rustc 2>&1)): $(rustc --version 2>&1)" >&2
echo "Rustup version ($(which rustup 2>&1)): $(rustup --version 2>&1)" >&2
echo "BUILDING configuration: ${CONFIGURATION}; platform: ${PLATFORM_NAME}"

## Allows x86_64 architecture in XCode Cloud to cross-compile to aarch64
export SDKROOT=$(xcrun -sdk macosx --show-sdk-path)
export MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version)

if [[ $CONFIGURATION == "Release" ]]; then
  if [[ "${NIGHTLY}"] = "YES" ]]; then
    for target in ${TARGETS[@]}; do
      echo "BUILDING FOR RELEASE (${target})" >&2
      RUSTFLAGS="$RUSTFLAGS -A dead_code" cargo +nightly build -Zbuild-std --package ${BRIDGE_CRATE} --no-default-features --release --target "${target}"
    done
  else
    for target in ${TARGETS[@]}; do
      echo "BUILDING FOR RELEASE (${target})" >&2
      cargo build --package ${BRIDGE_CRATE} --release --target "${target}"
    done
  fi

  UNIVERSAL_BUILD_TARGET="./target/universal/release"

  declare -a COMPILED_LIBS
  for target in "${TARGETS[@]}"; do
    COMPILED_LIBS+=("./target/${target}/release/${RUST_LIB}")
  done

elif [[ $CONFIGURATION == "Debug" ]]; then
  if [[ "${NIGHTLY}" = "YES" ]]; then
    for target in ${TARGETS[@]}; do
      echo "BUILDING FOR DEBUG (${target})" >&2
      RUSTFLAGS="$RUSTFLAGS -A dead_code" cargo +nightly build -Zbuild-std --package ${BRIDGE_CRATE} --no-default-features --target "${target}"
    done
  else
    for target in ${TARGETS[@]}; do
      echo "BUILDING FOR DEBUG (${target})" >&2
      cargo build --package ${BRIDGE_CRATE} --target "${target}"
    done
  fi

  UNIVERSAL_BUILD_TARGET="./target/universal/debug"

  declare -a COMPILED_LIBS
  for target in "${TARGETS[@]}"; do
    COMPILED_LIBS+=("./target/${target}/debug/${RUST_LIB}")
  done
fi

echo "BUILDING UNIVERSAL TARGET"
mkdir -p "${UNIVERSAL_BUILD_TARGET}"
cat << EOF
  lipo
    ${COMPILED_LIBS[@]}
    -create -output "${UNIVERSAL_BUILD_TARGET}/${RUST_LIB_NAME}_${PLATFORM_NAME}.a"
EOF

lipo \
  ${COMPILED_LIBS[@]} \
  -create -output "${UNIVERSAL_BUILD_TARGET}/${RUST_LIB_NAME}_${PLATFORM_NAME}.a"

CHECKFILE="${PROJECT_DIR}/Generated/${BRIDGE_CRATE}/${BRIDGE_CRATE}.swift"

if [[ ! -f "${CHECKFILE}" ]]; then
  echo "Failed to find expected generated file after compilation, but did not!" >&2
  echo "  Expected file: ${CHECKFILE}"
  exit 1
fi

In my XCode project, rather than directly linking to the build product (which now includes the PLATFORM_NAME in the file name), I added a new Configuration Settings File with the following contents:

OTHER_LDFLAGS = $(inherited) -l"rust_bridge_$(PLATFORM_NAME)"

I go multiple years between having to configure linkers, so it took me a bit of fighting before I remembered that -l"name" links to libname.a, and I had to exclude lib from the ldflags.

sax commented 3 months ago

One thing I'm unclear on is the best set of targets for watchos and iphoneos. I'm only targeting the latest version of iOS, so aarch64 may be good enough. I have compiled the Rust part of my application for watchOS and visionOS, but have not started the Swift part so don't yet know if this is missing anything.

Also I have my XCode project in a swift/ subdirectory of my repository, which is why my script is changing directory. That may not be needed for others.