From 7afd574f31d7ffb390e71847ae8211919e59f85b Mon Sep 17 00:00:00 2001 From: Jan-Erik Rediger Date: Mon, 2 Jan 2023 13:00:49 +0100 Subject: [PATCH] iOS Example: Fix the build to make it work again This includes: * Correctly calling `uniffi-bindgen` through cargo. * Ditching lipo for individual static libraries per target, that way it easily works on all Mac hardware (x86_64 or arm64) as well as for iOS hardware targets. * Build a `staticlib` using cargo's new `--crate-type` option --- .../app/ios/IOSApp.xcodeproj/project.pbxproj | 14 ++-- examples/app/ios/README.md | 60 ++++++++------- examples/app/ios/xc-universal-binary.sh | 77 ++++++++++--------- 3 files changed, 81 insertions(+), 70 deletions(-) diff --git a/examples/app/ios/IOSApp.xcodeproj/project.pbxproj b/examples/app/ios/IOSApp.xcodeproj/project.pbxproj index 9a138b5f16..d8142100f8 100644 --- a/examples/app/ios/IOSApp.xcodeproj/project.pbxproj +++ b/examples/app/ios/IOSApp.xcodeproj/project.pbxproj @@ -34,7 +34,7 @@ "$(SRCROOT)/Generated/$(INPUT_FILE_BASE)FFI.h", "$(SRCROOT)/Generated/$(INPUT_FILE_BASE).swift", ); - script = "# Generate swift bindings for the todolist rust library.\necho \"Generating files for $INPUT_FILE_PATH\"\n\"$SRCROOT/../../../target/debug/uniffi-bindgen\" generate \"$INPUT_FILE_PATH\" --language swift --out-dir \"$SRCROOT/Generated\"\necho \"Generated files for $INPUT_FILE_BASE in $SRCROOT/Generated\"\n"; + script = "# Generate swift bindings for the todolist rust library.\necho \"Generating files for $INPUT_FILE_PATH\"\n$HOME/.cargo/bin/cargo run -p uniffi_bindgen -- \\\n generate \"$INPUT_FILE_PATH\" \\\n --language swift \\\n --out-dir \"$SRCROOT/Generated\"\necho \"Generated files for $INPUT_FILE_BASE in $SRCROOT/Generated\"\n"; }; /* End PBXBuildRule section */ @@ -346,7 +346,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "bash $SRCROOT/xc-universal-binary.sh libarithmetical.a uniffi-example-arithmetic $SRCROOT/../../.. $CONFIGURATION\n"; + shellScript = "bash $SRCROOT/xc-universal-binary.sh uniffi-example-arithmetic $SRCROOT/../../.. $CONFIGURATION\n"; }; CEEB59EB25263ECE003C87D1 /* Build Universal Binary for todolist */ = { isa = PBXShellScriptBuildPhase; @@ -364,7 +364,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "bash $SRCROOT/xc-universal-binary.sh libuniffi_todolist.a uniffi-example-todolist $SRCROOT/../../.. $CONFIGURATION\n"; + shellScript = "bash $SRCROOT/xc-universal-binary.sh uniffi-example-todolist $SRCROOT/../../.. $CONFIGURATION\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -543,7 +543,9 @@ "$(inherited)", "@executable_path/Frameworks", ); - LIBRARY_SEARCH_PATHS = "$(SRCROOT)/../../../target/universal/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "../../../target/aarch64-apple-ios/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "../../../target/aarch64-apple-ios-sim/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "../../../target/x86_64-apple-ios/debug"; PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.example.uniffi.IOSApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "IOSApp-Bridging-Header.h"; @@ -567,7 +569,9 @@ "$(inherited)", "@executable_path/Frameworks", ); - LIBRARY_SEARCH_PATHS = "$(SRCROOT)/../../../target/universal/release"; + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "../../../target/aarch64-apple-ios/release"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "../../../target/aarch64-apple-ios-sim/release"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "../../../target/x86_64-apple-ios/release"; PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.example.uniffi.IOSApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "IOSApp-Bridging-Header.h"; diff --git a/examples/app/ios/README.md b/examples/app/ios/README.md index 421dee02ac..7d3cf1c23c 100644 --- a/examples/app/ios/README.md +++ b/examples/app/ios/README.md @@ -9,7 +9,13 @@ This will not be a complete tutorial on how to use `uniffi`, just the bits to ge ## Install Rust compiler targets with `rustup` ```sh -% rustup target add x86_64-apple-ios aarch64-apple-ios +rustup target add x86_64-apple-ios aarch64-apple-ios aarch64-apple-ios-sim +``` + +## Install `uniffi-bindgen` + +```sh +cargo install uniffi_bindgen ``` ## Associate the iOS project with the Rust project @@ -32,8 +38,8 @@ $HOME/.cargo/bin/uniffi-bindgen generate "$INPUT_FILE_PATH" --language swift --o These will output two files Xcode is interested in. ```sh +$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE)FFI.h $(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE).swift -$(DERIVED_FILE_DIR)/uniffi_$(INPUT_FILE_BASE)-Bridging-Header.h ``` The header file is a descriptor of the C API to a library that hasn't been built yet. The Swift file is a Swift facade that calls that C API. @@ -49,26 +55,25 @@ The header file is a descriptor of the C API to a library that hasn't been built #ifndef IOSApp_Bridging_Header_h #define IOSApp_Bridging_Header_h -#import "todolist-Bridging-Header.h" +#import "todolistFFI.h" #endif /* IOSApp_Bridging_Header_h */ ``` -The `#import` directive points to the header file generated by `uniffi-bindgen generate` above— `$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE)-Bridging-Header.h` +The `#import` directive points to the header file generated by `uniffi-bindgen generate` above— `$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE)FFI.h` ```h -#import "todolist-Bridging-Header.h" +#import "todolistFFI.h" ``` This `IOSApp-Bridging-Header.h` tells Xcode to look for the header file; because it's in `DERIVED_FILE_DIR`, Xcode should know where to look. ## Configure `cargo` to build the crate as a static library -1. In the `Cargo.toml` file of the Rust project, add `staticlib` to the `crate-type` list. +1. The build script automatically builds a static library (note: this requires rustc 1.64 or newer) ```toml [lib] -crate-type = ["staticlib", "cdylib"] name = "uniffi_todolist" ``` @@ -76,64 +81,63 @@ The `package` `name` and the `lib` `name` will be used below. In this case, the ## Tell Xcode how to build the Rust project. -#### TODO use a better explanation of what is going on here - 1. In Xcode, click on the project in the Project Navigator. 2. In the main window, select the app's main target, and then select "Build Phases". 3. Add a new `Run Script` build phase and move it to the top. 4. Add the script that will build a universal binary for the Rust project. -For this project, we've used a script adapted from the #mozilla/application-services project to build a universal binary with `lipo`. +`uniffi` comes with a script suitable to build a static library for all iOS targets: ```sh -bash $SRCROOT/xc-universal-binary.sh libuniffi_todolist.a uniffi-example-todolist $SRCROOT/../../../ $CONFIGURATION +bash $SRCROOT/xc-universal-binary.sh uniffi-example-todolist $SRCROOT/../../../ $CONFIGURATION ``` In this case we constructed the command: ```sh -xc-universal-binary.sh " +xc-universal-binary.sh " ``` by making: - * `STATIC_LIB_NAME` from the `lib` `name` above: `uniffi_todolist` --> `libuniffi_todolist.a` * `FFI_TARGET` from the `package` `name` above: `uniffi-example-todolist`. The workspace path is where the `Cargo.toml` will resolve the Rust project, and also determine the target directory that `cargo build` and `lipo` will put its artifacts. -This script performs a few steps: - -1. Runs `cargo build` to compile the Rust project for the `x86_64-apple-ios` and `aarch64-apple-ios` targets. - * This includes using `uniffi-bindgen` to generate the Rust scaffolding so we can go from C to Rust. -2. Runs `lipo` to combine these libs in to a universal binary. -3. Puts the universal binary in to the `$WORKSPACE_PATH/target/universal` directory. +This script runs `cargo build` to compile the Rust project for the `x86_64-apple-ios`, `aarch64-apple-ios` and `aarch64-apple-ios-sim` targets. +This includes using `uniffi-bindgen` to generate the Rust scaffolding so we can go from C to Rust. ## Tell Xcode where the universal library is -Finally, we need to tell Xcode to look for the universal binary `libuniffi_todolist.a` is, so it can tie it together with the header file `todolist-Bridging-Header.h`. +Finally, we need to tell Xcode to look for the libraries based on the target, so it can tie it together with the header file `todolistFFI.h`. 1. In Xcode, click on the project in the Project Navigator. 2. In the main window, select the app's main target, and then select "Build Settings". 3. Search for `Library Search Paths`. -4. Add paths to where lipo constructed the universal binaries for each of `Debug` and `Release`. +4. Add paths per OS and architecture. + The OS specific part can be configured in the Xcode project editor, + but the architecture specific part has to be added by manually editing the `project.pbxproj` file. + Open `project.pbxproj` in your project direcotry and add the following in the `Debug` section: -```sh -$(SRCROOT)/../../../target/universal/debug -$(SRCROOT)/../../../target/universal/release -``` + ``` + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "../../../target/aarch64-apple-ios/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "../../../target/aarch64-apple-ios-sim/debug"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "../../../target/x86_64-apple-ios/debug"; + ``` + + The path should be a relative path pointing into the `target` directory created by `cargo`. + Add a similar block in the `Release` section and adjust the paths accordingly. ## Back to code. -By now, we should've +By now, we should've 1. Used `uniffi-bindgen` to generate a Swift file to call a C API header and put them in a place Xcode can find them. 2. Configured Xcode to find the header. 3. Configured cargo to build a static library version of the crate. 4. Used `uniffi-bindgen` to generate a C API for the Rust crate. 5. Used cargo to cross-compile the crate for different targets. -6. Used lipo to combine those crates for different target into a universal binary. -7. Configured Xcode to find the universal binary. +7. Configured Xcode to find the different static libraries. By now, we should be able to hit Run in Xcode and build something. diff --git a/examples/app/ios/xc-universal-binary.sh b/examples/app/ios/xc-universal-binary.sh index a3474ddb53..b21e17d5f1 100755 --- a/examples/app/ios/xc-universal-binary.sh +++ b/examples/app/ios/xc-universal-binary.sh @@ -12,54 +12,57 @@ trap error_help ERR PATH="$(bash -l -c 'echo $PATH')" # This should be invoked from inside xcode, not manually -if [[ "${#}" -ne 4 ]] +if [[ "${#}" -ne 3 ]] then echo "Usage (note: only call inside xcode!):" - echo "path/to/build-scripts/xc-universal-binary.sh " + echo "path/to/build-scripts/xc-universal-binary.sh " exit 1 fi -# e.g. liblogins_ffi.a -STATIC_LIB_NAME=${1} # what to pass to cargo build -p, e.g. logins_ffi -FFI_TARGET=${2} -# path to app services root -SRC_ROOT=${3} -# buildvariant from our xcconfigs -BUILDVARIANT=$(echo "${4}" | tr '[:upper:]' '[:lower:]') +FFI_TARGET=${1} +# path to source code root +SRC_ROOT=${2} +# buildvariant from our xcconfigs +BUILDVARIANT=$(echo "${3}" | tr '[:upper:]' '[:lower:]') RELFLAG= -RELDIR="debug" if [[ "${BUILDVARIANT}" != "debug" ]]; then RELFLAG=--release - RELDIR=release fi -TARGETDIR=${SRC_ROOT}/target +if [[ -n "${SDK_DIR:-}" ]]; then + # Assume we're in Xcode, which means we're probably cross-compiling. + # In this case, we need to add an extra library search path for build scripts and proc-macros, + # which run on the host instead of the target. + # (macOS Big Sur does not have linkable libraries in /usr/lib/.) + export LIBRARY_PATH="${SDK_DIR}/usr/lib:${LIBRARY_PATH:-}" +fi -# We can't use cargo lipo because we can't link to universal libraries :( -# /~https://github.com/rust-lang/rust/issues/55235 -LIBS_ARCHS=("x86_64" "arm64") -IOS_TRIPLES=("x86_64-apple-ios" "aarch64-apple-ios") -for i in "${!LIBS_ARCHS[@]}"; do - env -i PATH="${PATH}" \ - "${HOME}"/.cargo/bin/cargo build --locked -p "${FFI_TARGET}" --lib ${RELFLAG} --target "${IOS_TRIPLES[${i}]}" -done +IS_SIMULATOR=0 +if [ "${LLVM_TARGET_TRIPLE_SUFFIX-}" = "-simulator" ]; then + IS_SIMULATOR=1 +fi -UNIVERSAL_BINARY=${TARGETDIR}/universal/${RELDIR}/${STATIC_LIB_NAME} -NEED_LIPO= +for arch in $ARCHS; do + case "$arch" in + x86_64) + if [ $IS_SIMULATOR -eq 0 ]; then + echo "Building for x86_64, but not a simulator build. What's going on?" >&2 + exit 2 + fi -# if the universal binary doesnt exist, or if it's older than the static libs, -# we need to run `lipo` again. -if [[ ! -f "${UNIVERSAL_BINARY}" ]]; then - NEED_LIPO=1 -elif [[ "$(stat -f "%m" "${TARGETDIR}/x86_64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}")" -gt "$(stat -f "%m" "${UNIVERSAL_BINARY}")" ]]; then - NEED_LIPO=1 -elif [[ "$(stat -f "%m" "${TARGETDIR}/aarch64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}")" -gt "$(stat -f "%m" "${UNIVERSAL_BINARY}")" ]]; then - NEED_LIPO=1 -fi -if [[ "${NEED_LIPO}" = "1" ]]; then - mkdir -p "${TARGETDIR}/universal/${RELDIR}" - lipo -create -output "${UNIVERSAL_BINARY}" \ - "${TARGETDIR}/x86_64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}" \ - "${TARGETDIR}/aarch64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}" -fi + # Intel iOS simulator + export CFLAGS_x86_64_apple_ios="-target x86_64-apple-ios" + $HOME/.cargo/bin/cargo rustc -p "${FFI_TARGET}" --lib --crate-type staticlib $RELFLAG --target x86_64-apple-ios + ;; + + arm64) + if [ $IS_SIMULATOR -eq 0 ]; then + # Hardware iOS targets + $HOME/.cargo/bin/cargo rustc -p "${FFI_TARGET}" --lib --crate-type staticlib $RELFLAG --target aarch64-apple-ios + else + # M1 iOS simulator -- currently in Nightly only and requires to build `libstd` + $HOME/.cargo/bin/cargo rustc -p "${FFI_TARGET}" --lib --crate-type staticlib $RELFLAG --target aarch64-apple-ios-sim + fi + esac +done