Skip to content

Instantly share code, notes, and snippets.

@ara4n
Created December 21, 2022 02:13
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ara4n/320a53ea768aba51afad4c9ed2168536 to your computer and use it in GitHub Desktop.
Save ara4n/320a53ea768aba51afad4c9ed2168536 to your computer and use it in GitHub Desktop.
Compiling rust libraries under Catalyst (macabi) on Ventura (macOS 13.1)

Fixing rust to support compiling libraries for use with Catalyst (Dec 2022)

The problem is that rustc emits synthetic libraries (symbols.o and lib.rmeta) which Apple's ld then chokes on when targetting aarch64-apple-ios-macabi, as they lack an LC_BUILD_VERSION load command to identify them as being intended for use with Catalyst.

The errors produced look like:

ld: in /Users/matthew/workspace/matrix-rust-sdk/target/aarch64-apple-ios-macabi/dbg/deps/libstatic_assertions-fdafb4b8ba800a8a.rlib(lib.rmeta), building for Mac Catalyst, but linking in object file built for , file '/Users/matthew/workspace/matrix-rust-sdk/target/aarch64-apple-ios-macabi/dbg/deps/libstatic_assertions-fdafb4b8ba800a8a.rlib' for architecture arm64

The key thing being that the lib.rmeta file here within the .rlib archive has a blank platform, which ld refuses to link in with the other Catalyst platform objects & libraries.

rustc generates object files using https://github.com/gimli-rs/object, which doesn't expose an API to add an LC_BUILD_VERSION on Mach-O objects. So I added one:

diff --git a/Cargo.toml b/Cargo.toml
index b08eec8..c840cf1 100755
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "object"
-version = "0.30.0"
+version = "0.29.0"
 edition = "2018"
 exclude = ["/.github", "/testfiles"]
 keywords = ["object", "elf", "mach-o", "pe", "coff"]
diff --git a/src/lib.rs b/src/lib.rs
index 40f17c0..968496e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -56,6 +56,10 @@
 #![deny(missing_debug_implementations)]
 #![no_std]
 // Style.
+
+// let us be built directly by rustc as a path dependency
+#![allow(elided_lifetimes_in_paths, rustc::default_hash_types, explicit_outlives_requirements)]
+
 #![allow(clippy::collapsible_if)]
 #![allow(clippy::comparison_chain)]
 #![allow(clippy::match_like_matches_macro)]
diff --git a/src/write/macho.rs b/src/write/macho.rs
index 8ef722f..628a05d 100644
--- a/src/write/macho.rs
+++ b/src/write/macho.rs
@@ -1,4 +1,5 @@
 use core::mem;
+use core::convert::TryInto;
 
 use crate::endian::*;
 use crate::macho;
@@ -211,6 +212,15 @@ impl<'a> Object<'a> {
         let mut ncmds = 0;
         let command_offset = offset;
 
+        // Calculate size of build version
+        let build_version_offset = offset;
+        let mut build_version_len = 0;
+        if self.platform > 0 {
+            build_version_len = mem::size_of::<macho::BuildVersionCommand<Endianness>>();
+            offset += build_version_len;
+            ncmds += 1;
+        }
+
         // Calculate size of segment command and section headers.
         let segment_command_offset = offset;
         let segment_command_len =
@@ -358,6 +368,18 @@ impl<'a> Object<'a> {
             },
         );
 
+        if self.platform > 0 {
+            debug_assert_eq!(build_version_offset, buffer.len());
+            buffer.write(&macho::BuildVersionCommand {
+                cmd: U32::new(endian, macho::LC_BUILD_VERSION),
+                cmdsize: U32::new(endian, build_version_len.try_into().unwrap()),
+                platform: U32::new(endian, self.platform),
+                minos: U32::new(endian, self.minos),
+                sdk: U32::new(endian, self.sdk),
+                ntools: U32::new(endian, 0),
+            });
+        }
+
         // Write segment command.
         debug_assert_eq!(segment_command_offset, buffer.len());
         macho.write_segment_command(
diff --git a/src/write/mod.rs b/src/write/mod.rs
index aa4980b..ebdf7d7 100644
--- a/src/write/mod.rs
+++ b/src/write/mod.rs
@@ -70,6 +70,10 @@ pub struct Object<'a> {
     pub mangling: Mangling,
     /// Mach-O "_tlv_bootstrap" symbol.
     tlv_bootstrap: Option<SymbolId>,
+    /// Mach-O platform details
+    platform: u32,
+    minos: u32,
+    sdk: u32,
 }
 
 impl<'a> Object<'a> {
@@ -88,6 +92,9 @@ impl<'a> Object<'a> {
             flags: FileFlags::None,
             mangling: Mangling::default(format, architecture),
             tlv_bootstrap: None,
+            platform: 0,
+            minos: 0,
+            sdk: 0,
         }
     }
 
@@ -298,6 +305,13 @@ impl<'a> Object<'a> {
         comdat_id
     }
 
+    /// Add a build version load command (Mach-O only); needed for macabi to link.
+    pub fn set_build_version(&mut self, platform: u32, minos: u32, sdk: u32) {
+        self.platform = platform;
+        self.minos = minos;
+        self.sdk = sdk;
+    }
+
     /// Get the `SymbolId` of the symbol with the given name.
     pub fn symbol_id(&self, name: &[u8]) -> Option<SymbolId> {
         self.symbol_map.get(name).cloned()

Then, you need to build a custom rustc toolchain against the local object dependency:

diff --git a/Cargo.toml b/Cargo.toml
index 000c10a1f90..23a5753c1b1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -105,6 +105,7 @@ object.debug = 0
 # See comments in `src/tools/rustc-workspace-hack/README.md` for what's going on
 # here
 rustc-workspace-hack = { path = 'src/tools/rustc-workspace-hack' }
+object = { path = '/Users/matthew/workspace/object' }
 
 # See comments in `library/rustc-std-workspace-core/README.md` for what's going on
 # here
diff --git a/compiler/rustc_codegen_ssa/src/back/metadata.rs b/compiler/rustc_codegen_ssa/src/back/metadata.rs
index 51c5c375d51..953a48a0d9d 100644
--- a/compiler/rustc_codegen_ssa/src/back/metadata.rs
+++ b/compiler/rustc_codegen_ssa/src/back/metadata.rs
@@ -133,6 +133,16 @@ pub(crate) fn create_object_file(sess: &Session) -> Option<write::Object<'static
     };
 
     let mut file = write::Object::new(binary_format, architecture, endianness);
+
+    // macOS ld won't link macabi ABIs which are missing a build_version
+    if sess.target.llvm_target.ends_with("-macabi") {
+        file.set_build_version(
+            object::macho::PLATFORM_MACCATALYST,
+            0x000E0000, // minOS 14.0
+            0x00100200, // SDK 16.2
+        );
+    }
+
     let e_flags = match architecture {
         Architecture::Mips => {
             let arch = match sess.target.options.cpu.as_ref() {
diff --git a/compiler/rustc_target/src/spec/x86_64_apple_ios_macabi.rs b/compiler/rustc_target/src/spec/x86_64_apple_ios_macabi.rs
index 0f3f8519963..5d307ac4f3a 100644
--- a/compiler/rustc_target/src/spec/x86_64_apple_ios_macabi.rs
+++ b/compiler/rustc_target/src/spec/x86_64_apple_ios_macabi.rs
@@ -2,7 +2,7 @@
 use crate::spec::{Cc, LinkerFlavor, Lld, StackProbeType, Target, TargetOptions};
 
 pub fn target() -> Target {
-    let llvm_target = "x86_64-apple-ios13.0-macabi";
+    let llvm_target = "x86_64-apple-ios15.0-macabi";
 
     let arch = Arch::X86_64_macabi;
     let mut base = opts("ios", arch);

N.B. needing to downgrade the local object checkout to 0.29.0 to persuade rustc to use it, as well as tweaking its lint rules to let it build in rustc's strict environment.

To build rustc, you just ./x.py build && ./x.py install

To link the result as a custom toolchain, you rustup toolchain link rust-catalyst /usr/local/rust or wherever.

Then to actually build the library under Catalyst (matrix-rust-sdk in this instance), you have to tell cargo to use the right toolchain (if it's not the default) and build with -Z build-std as there's no prebuilt standard library for our custom toolchain.

For matrix-rust-sdk this poses another problem, because ruma-common depends on indexmap which chokes by default if there's no std. So you have to run a custom ruma with std as an explicit feature:

diff --git a/crates/ruma-common/Cargo.toml b/crates/ruma-common/Cargo.toml
index 83f22461..2aa36f8a 100644
--- a/crates/ruma-common/Cargo.toml
+++ b/crates/ruma-common/Cargo.toml
@@ -54,7 +54,7 @@ form_urlencoded = "1.0.0"
 getrandom = { version = "0.2.6", optional = true }
 html5ever = { version = "0.25.2", optional = true }
 http = { workspace = true, optional = true }
-indexmap = { version = "1.9.1", features = ["serde"] }
+indexmap = { version = "1.9.1", features = ["serde", "std"] }
 itoa = "1.0.1"
 js_int = { workspace = true, features = ["serde"] }
 js_option = "0.1.0"

...and then link that in from matrix-rust-sdk with the tweaked framework build script like so:

diff --git a/Cargo.toml b/Cargo.toml
index 6d2b68fe..4f96eee9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,8 +19,10 @@ resolver = "2"
 rust-version = "1.65"
 
 [workspace.dependencies]
-ruma = { git = "https://github.com/ruma/ruma", rev = "284b797e0513daf56859b64b8c7a506856fb11ec", features = ["client-api-c"] }
-ruma-common = { git = "https://github.com/ruma/ruma", rev = "284b797e0513daf56859b64b8c7a506856fb11ec" }
+#ruma = { git = "https://github.com/ruma/ruma", rev = "284b797e0513daf56859b64b8c7a506856fb11ec", features = ["client-api-c"] }
+ruma = { path = "/Users/matthew/workspace/ruma/crates/ruma", features = ["client-api-c"] }
+#ruma-common = { git = "https://github.com/ruma/ruma", rev = "284b797e0513daf56859b64b8c7a506856fb11ec" }
+ruma-common = { path = "/Users/matthew/workspace/ruma/crates/ruma-common" }
 tracing = { version = "0.1.36", default-features = false, features = ["std"] }
 uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "249a78b6f3f35661f1530e53811134e1bf012608" }
 uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "249a78b6f3f35661f1530e53811134e1bf012608" }
diff --git a/bindings/apple/README.md b/bindings/apple/README.md
index 710e891b..778db18a 100644
--- a/bindings/apple/README.md
+++ b/bindings/apple/README.md
@@ -27,6 +27,10 @@ For development purposes, it will additionally generate a `Package.swift` file i
 
 When building the SDK for release you should pass the `--release` argument to the task, which will strip away any symbols and optimise the created binary.
 
+You can also pass in --only-target to speed things up and build a single target (e.g. aarch64-apple-ios)
+
+The resulting framework can then be added from xcode via the Package.swift found at the root of the repository.
+
 ## Building only the Crypto SDK
 
diff --git a/xtask/src/swift.rs b/xtask/src/swift.rs
index 31dbdb6b..14cf80c1 100644
--- a/xtask/src/swift.rs
+++ b/xtask/src/swift.rs
@@ -115,7 +115,7 @@ fn generate_uniffi(library_file: &Path, ffi_directory: &Path) -> Result<()> {
 }
 
 fn build_for_target(target: &str, profile: &str) -> Result<PathBuf> {
-    cmd!("cargo build -p matrix-sdk-ffi --target {target} --profile {profile}").run()?;
+    cmd!("cargo +rust-catalyst build -p matrix-sdk-ffi -Z build-std --target {target} --profile {profile}").run()?;
 
     // The builtin dev profile has its files stored under target/debug, all
     // other targets have matching directory names

You can then build the catalyst framework with:

cargo xtask swift build-framework --only-target aarch64-apple-ios-macabi

Having imported the matrix-rust-sdk repo as a swift package into XCode, the final tweak needed to build Element X is to switch to using OIDExternalUserAgentCatalyst rather than OIDExternalUserAgentiOS.

@ara4n
Copy link
Author

ara4n commented Feb 27, 2024

Feb 2024 update:

I just built Element X for catalyst again. This time rustc worked (thanks to rust-lang/rust#106021 getting fixed - yay)

However, i hit some other problems; might as well document them here for posterity:

  1. matrix-rust-sdk depends transitively on both indexmap 2.x and 1.x. For 1.x (needed by tower), you need to explicitly enable the std feature if building without a std library (as rust doesn't ship a prebuilt std library for catalyst). You can't enable the std feature at the top level Cargo.toml, as the workspace dependencies are overridden by the explicit crate dependencies, and you can't patch crates.io to enable features. So instead, the fix is to add the dep to a concrete crate under your control such as matrix-sdk-common - and then the feature will be applied to other transitive instances of the crate:
diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml
index 65ca44ea3..b826080a9 100644
--- a/crates/matrix-sdk-common/Cargo.toml
+++ b/crates/matrix-sdk-common/Cargo.toml
@@ -22,6 +22,7 @@ js = ["instant/wasm-bindgen", "wasm-bindgen-futures"]
 async-trait = { workspace = true }
 futures-core = { workspace = true }
 instant = "0.1.12"
+indexmap = { version = "1.9.3", features = ["std"] }
 ruma = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
  1. The builder has to be told about catalyst, and modified to build an explicit std library (which in turn requires nightly toolchain):
diff --git a/xtask/src/swift.rs b/xtask/src/swift.rs
index 567deb17a..a4a8b91fd 100644
--- a/xtask/src/swift.rs
+++ b/xtask/src/swift.rs
@@ -109,12 +109,21 @@ impl Platform {
 const FFI_LIBRARY_NAME: &str = "libmatrix_sdk_ffi.a";
 /// The list of targets supported by the SDK.
 const TARGETS: &[Target] = &[
-    Target { triple: "aarch64-apple-ios", platform: Platform::Ios, description: "iOS" },
+    Target {
+       triple: "aarch64-apple-ios",
+       platform: Platform::Ios,
+       description: "iOS"
+    },
     Target {
         triple: "aarch64-apple-darwin",
         platform: Platform::Macos,
         description: "macOS (Apple Silicon)",
     },
+    Target {
+        triple: "aarch64-apple-ios-macabi",
+        platform: Platform::Macos,
+        description: "macOS (Catalyst)",
+    },
     Target {
         triple: "x86_64-apple-darwin",
         platform: Platform::Macos,
@@ -142,7 +151,7 @@ fn build_library() -> Result<()> {
 
     create_dir_all(ffi_directory.as_path())?;
 
-    cmd!("rustup run stable cargo build -p matrix-sdk-ffi").run()?;
+    cmd!("rustup run nightly cargo build -p matrix-sdk-ffi").run()?;
 
     rename(lib_output_dir.join(FFI_LIBRARY_NAME), ffi_directory.join(FFI_LIBRARY_NAME))?;
     let swift_directory = root_directory.join("bindings/apple/generated/swift");
@@ -264,12 +273,12 @@ fn build_targets(
             let triple = target.triple;
 
             println!("-- Building for {}", target.description);
-            cmd!("rustup run stable cargo build -p matrix-sdk-ffi --target {triple} --profile {profile}")
+            cmd!("rustup run nightly cargo build -p matrix-sdk-ffi -Z build-std --target {triple} --profile {profile}")
                 .run()?;
         }
     } else {
         let triples = &targets.iter().map(|target| target.triple).collect::<Vec<_>>();
-        let mut cmd = cmd!("rustup run stable cargo build -p matrix-sdk-ffi");
+        let mut cmd = cmd!("rustup run nightly cargo build -p matrix-sdk-ffi -Z build-std");
         for triple in triples {
             cmd = cmd.arg("--target").arg(triple);
         }

Then, you can build the sdk for catalyst:

cargo xtask swift build-framework --only-target aarch64-apple-ios-macabi

  1. You have to do the same thing with matrix-rich-text-editor. Tell it to target catalyst:
diff --git a/platforms/ios/tools/Info.plist b/platforms/ios/tools/Info.plist
index b498a3c1..a540258c 100644
--- a/platforms/ios/tools/Info.plist
+++ b/platforms/ios/tools/Info.plist
@@ -6,7 +6,7 @@
        <array>
                <dict>
                        <key>LibraryIdentifier</key>
-                       <string>ios-arm64</string>
+                       <string>ios-arm64-maccatalyst</string>
                        <key>LibraryPath</key>
                        <string>WysiwygComposerFFI.framework</string>
                        <key>SupportedArchitectures</key>
@@ -15,21 +15,8 @@
                        </array>
                        <key>SupportedPlatform</key>
                        <string>ios</string>
-               </dict>
-               <dict>
-                       <key>LibraryIdentifier</key>
-                       <string>ios-arm64_x86_64-simulator</string>
-                       <key>LibraryPath</key>
-                       <string>WysiwygComposerFFI.framework</string>
-                       <key>SupportedArchitectures</key>
-                       <array>
-                               <string>arm64</string>
-                               <string>x86_64</string>
-                       </array>
-                       <key>SupportedPlatform</key>
-                       <string>ios</string>
-                       <key>SupportedPlatformVariant</key>
-                       <string>simulator</string>
+                       <key>SupportedPlatformVariant</key>
+                       <string>maccatalyst</string>
                </dict>
        </array>
        <key>CFBundlePackageType</key>

...and have a script to build the framework for catalyst (derived from the main one, which targets iOS + sim):

#!/usr/bin/env bash

GENERATION_PATH=.generated/ios

UNIFFI_CONFIG_FILE_PATH=bindings/wysiwyg-ffi/uniffi.toml

ARM64_LIB_PATH=target/aarch64-apple-ios-macabi/release/libuniffi_wysiwyg_composer.a

IOS_PATH=platforms/ios
TOOLS_PATH="${IOS_PATH}/tools"

SWIFT_PACKAGE_PATH="${IOS_PATH}/lib/WysiwygComposer"
SWIFT_BINDINGS_FILE_PATH="${SWIFT_PACKAGE_PATH}/Sources/WysiwygComposer/WysiwygComposer.swift"

XCFRAMEWORK_PATH="${SWIFT_PACKAGE_PATH}/WysiwygComposerFFI.xcframework"

XCFRAMEWORK_ARM64_PATH="${XCFRAMEWORK_PATH}/ios-arm64-maccatalyst/WysiwygComposerFFI.framework"
XCFRAMEWORK_ARM64_HEADERS_PATH="${XCFRAMEWORK_ARM64_PATH}/Headers"
XCFRAMEWORK_ARM64_MODULES_PATH="${XCFRAMEWORK_ARM64_PATH}/Modules"
XCFRAMEWORK_ARM64_LIBRARY_PATH="${XCFRAMEWORK_ARM64_PATH}/WysiwygComposerFFI"

cargo +nightly build -p uniffi-wysiwyg-composer -Z build-std --release --target aarch64-apple-ios-macabi

rm -rf $XCFRAMEWORK_PATH
rm -f $SWIFT_BINDINGS_FILE_PATH
rm -rf $GENERATION_PATH

if ! command -v swiftformat &> /dev/null
then
    echo "swiftformat could not be found"
    exit 1
fi
mkdir -p $GENERATION_PATH
cargo uniffi-bindgen generate --library $ARM64_LIB_PATH -l swift --out-dir $GENERATION_PATH

mv "${GENERATION_PATH}/WysiwygComposer.swift" $SWIFT_BINDINGS_FILE_PATH
sed -i "" -e '1h;2,$H;$!d;g' -e 's/) -> ComposerUpdate {\n        return try! FfiConverterTypeComposerUpdate.lift(\n            try!/) throws -> ComposerUpdate {\n        return try FfiConverterTypeComposerUpdate.lift(\n            try/g' $SWIFT_BINDINGS_FILE_PATH
sed -i "" -e '1h;2,$H;$!d;g' -e 's/) -> ComposerUpdate/) throws -> ComposerUpdate/g' $SWIFT_BINDINGS_FILE_PATH

mkdir -p $XCFRAMEWORK_ARM64_HEADERS_PATH
mkdir $XCFRAMEWORK_ARM64_MODULES_PATH

mv $ARM64_LIB_PATH $XCFRAMEWORK_ARM64_LIBRARY_PATH
mv ${GENERATION_PATH}/*.h $XCFRAMEWORK_ARM64_HEADERS_PATH
cp "${TOOLS_PATH}/Info.plist" $XCFRAMEWORK_PATH
cp "${TOOLS_PATH}/module.modulemap" $XCFRAMEWORK_ARM64_MODULES_PATH
  1. Finally, and this is the nasty bit: you can't statically link two different rust libraries into the same application if they have both been built with an explicit std lib, as you'll get duplicate symbols from the overlapping std lib: c.f. rust-lang/rust#44322. Disabling LTO didn't seem to fix it. The Correct Solution is probably to build a single rust library (containing a single stdlib). However, i went for the dirty workaround instead, which was to bluntly rename the symbols in WysiwygEditor static lib:
cd platforms/ios/lib/WysiwygComposer/WysiwygComposerFFI.xcframework/ios-arm64-maccatalyst/WysiwygComposerFFI.framework
mkdir tmp
cd tmp
ar x ../WysiwygComposerFFI.orig
perl -pi -e'sub fix { $_ = shift; s/r/x/; return $_ } s/(___rg_oom|___rust_foreign_exception|___rust_drop_panic|___rdl_alloc|_rust_begin_unwind|___rdl_dealloc|___rdl_oom|___rdl_realloc|_rust_panic|_rust_eh_personality|___rdl_alloc_zeroed|___rust_start_panic)/fix($1)/eg' *
chmod u+r __.SYMDEF
ar -r ../WysiwygComposerFFI.new *
mv ../WysiwygComposerFFI.new ../WysiwygComposerFFI

(you probably don't need to actually expand & recompress the library and could hit it with perl/sed in situ, but i had it expanded anyway to check more carefully what was going on, and just in case ar does any LZ style compression these days).

Then, I yanked out a few other libraries which hadn't been built for catalyst (MapLibreGL and SwiftOgg), and it built.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment