diff --git a/.buildkite/swift-test.sh b/.buildkite/swift-test.sh index 0d2791107..455da7712 100755 --- a/.buildkite/swift-test.sh +++ b/.buildkite/swift-test.sh @@ -18,12 +18,6 @@ function run_tests() { function build_for_real_device() { local platform; platform=$1 - # See https://github.com/Automattic/wordpress-rs/issues/48 - if [[ $platform == "watchOS" ]]; then - echo "~~~ watchOS is not supported yet" - return - fi - echo "--- :swift: Building for $platform device" export NSUnbufferedIO=YES xcodebuild -destination "generic/platform=$platform" \ diff --git a/Cargo.lock b/Cargo.lock index 2acc9118e..b0919fc1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2991,6 +2991,15 @@ dependencies = [ "uniffi", ] +[[package]] +name = "xcframework" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "serde_json", +] + [[package]] name = "zerocopy" version = "0.7.34" diff --git a/Cargo.toml b/Cargo.toml index 179be8260..8a4aca886 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "wp_api", "wp_contextual", "wp_uniffi_bindgen", + "xcframework", ] resolver = "2" diff --git a/Makefile b/Makefile index 83bb803be..d8e06c87d 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,6 @@ swift_package_platform_ios := $(call swift_package_platform_version,ios) swift_package_platform_watchos := $(call swift_package_platform_version,watchos) swift_package_platform_tvos := $(call swift_package_platform_version,tvos) -cargo_config_library = --config profile.release.debug=true --config 'profile.release.panic="abort"' - # Required for supporting tvOS and watchOS. We can update the nightly toolchain version if needed. rust_nightly_toolchain := nightly-2024-04-30 @@ -51,66 +49,6 @@ docs: docs-archive: docs tar -czvf docs.tar.gz docs -# Builds the library for all the various architectures / systems required in an XCFramework -xcframework-libraries: - # macOS - env MACOSX_DEPLOYMENT_TARGET=$(swift_package_platform_macos) $(MAKE) x86_64-apple-darwin-xcframework-library - env MACOSX_DEPLOYMENT_TARGET=$(swift_package_platform_macos) $(MAKE) aarch64-apple-darwin-xcframework-library - - # iOS - env IPHONEOS_DEPLOYMENT_TARGET=$(swift_package_platform_ios) $(MAKE) aarch64-apple-ios-xcframework-library - env IPHONEOS_DEPLOYMENT_TARGET=$(swift_package_platform_ios) $(MAKE) x86_64-apple-ios-xcframework-library - env IPHONEOS_DEPLOYMENT_TARGET=$(swift_package_platform_ios) $(MAKE) aarch64-apple-ios-sim-xcframework-library - - # tvOS - env TVOS_DEPLOYMENT_TARGET=$(swift_package_platform_tvos) $(MAKE) aarch64-apple-tvos-xcframework-library-with-nightly - env TVOS_DEPLOYMENT_TARGET=$(swift_package_platform_tvos) $(MAKE) aarch64-apple-tvos-sim-xcframework-library-with-nightly - env TVOS_DEPLOYMENT_TARGET=$(swift_package_platform_tvos) $(MAKE) x86_64-apple-tvos-xcframework-library-with-nightly - - # watchOS - env WATCHOS_DEPLOYMENT_TARGET=$(swift_package_platform_watchos) $(MAKE) arm64_32-apple-watchos-xcframework-library-with-nightly - env WATCHOS_DEPLOYMENT_TARGET=$(swift_package_platform_watchos) $(MAKE) aarch64-apple-watchos-sim-xcframework-library-with-nightly - env WATCHOS_DEPLOYMENT_TARGET=$(swift_package_platform_watchos) $(MAKE) x86_64-apple-watchos-sim-xcframework-library-with-nightly - -%-xcframework-library: - cargo $(cargo_config_library) build --target $* --package wp_api --release - $(MAKE) $*-combine-libraries - -%-xcframework-library-with-nightly: - cargo +$(rust_nightly_toolchain) $(cargo_config_library) build --target $* --package wp_api --release -Z build-std=panic_abort,std - $(MAKE) $*-combine-libraries - -# Xcode doesn't properly support multiple XCFrameworks being used by the same target, so we need -# to combine the binaries -%-combine-libraries: - xcrun libtool -static -o target/$*/release/libwordpress.a target/$*/release/libwp_api.a - -# Some libraries need to be created in a multi-binary format, so we combine them here -xcframework-combined-libraries: xcframework-libraries - - rm -rf target/universal-* - mkdir -p target/universal-macos/release target/universal-ios/release target/universal-tvos/release target/universal-watchos/release - - # Combine the macOS Binaries - lipo -create target/aarch64-apple-darwin/release/libwordpress.a target/x86_64-apple-darwin/release/libwordpress.a \ - -output target/universal-macos/release/libwordpress.a - lipo -info target/universal-macos/release/libwordpress.a - - # Combine iOS Simulator Binaries - lipo -create target/aarch64-apple-ios-sim/release/libwordpress.a target/x86_64-apple-ios/release/libwordpress.a \ - -output target/universal-ios/release/libwordpress.a - lipo -info target/universal-ios/release/libwordpress.a - - # Combine tvOS Simulator Binaries - lipo -create target/aarch64-apple-tvos-sim/release/libwordpress.a target/x86_64-apple-tvos/release/libwordpress.a \ - -output target/universal-tvos/release/libwordpress.a - lipo -info target/universal-tvos/release/libwordpress.a - - # Combine watchOS Simulator Binaries - lipo -create target/aarch64-apple-watchos-sim/release/libwordpress.a target/x86_64-apple-watchos-sim/release/libwordpress.a \ - -output target/universal-watchos/release/libwordpress.a - lipo -info target/universal-watchos/release/libwordpress.a - # An XCFramework relies on the .h file and the modulemap to interact with the precompiled binary xcframework-headers: bindings rm -rvf target/swift-bindings/headers @@ -119,33 +57,61 @@ xcframework-headers: bindings cp target/swift-bindings/*.h target/swift-bindings/headers cp target/swift-bindings/libwordpressFFI.modulemap target/swift-bindings/headers/module.modulemap +apple-platform-targets-macos := x86_64-apple-darwin aarch64-apple-darwin +apple-platform-targets-ios := aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim +apple-platform-targets-tvos := aarch64-apple-tvos aarch64-apple-tvos-sim +apple-platform-targets-watchos := arm64_32-apple-watchos x86_64-apple-watchos-sim aarch64-apple-watchos-sim +apple-platform-targets := \ + $(apple-platform-targets-macos) \ + $(apple-platform-targets-ios) \ + $(apple-platform-targets-tvos) \ + $(apple-platform-targets-watchos) + +ifeq ($(BUILDKITE), true) +CARGO_PROFILE ?= release +else +CARGO_PROFILE ?= dev +endif + +cargo_config_library = --config profile.$(CARGO_PROFILE).debug=true --config 'profile.$(CARGO_PROFILE).panic="abort"' + +# Set deployment targets for each platform +_build-apple-%-darwin: export MACOSX_DEPLOYMENT_TARGET=$(swift_package_platform_macos) +_build-apple-%-ios _build-apple-%-ios-sim: export IPHONEOS_DEPLOYMENT_TARGET=$(swift_package_platform_ios) +_build-apple-%-tvos _build-apple-%-tvos-sim: export TVOS_DEPLOYMENT_TARGET=$(swift_package_platform_tvos) +_build-apple-%-watchos _build-apple-%-watchos-sim: export WATCHOS_DEPLOYMENT_TARGET=$(swift_package_platform_watchos) + +# Use nightly toolchain for tvOS and watchOS +_build-apple-%-tvos _build-apple-%-tvos-sim _build-apple-%-watchos _build-apple-%-watchos-sim: \ + CARGO_OPTS = +$(rust_nightly_toolchain) -Z build-std=panic_abort,std + +# Build the library for a specific target +_build-apple-%: xcframework-headers + cargo $(CARGO_OPTS) $(cargo_config_library) build --target $* --package wp_api --profile $(CARGO_PROFILE) + +# Build the library for one single platform, including real device and simulator. +build-apple-platform-macos := $(addprefix _build-apple-,$(apple-platform-targets-macos)) +build-apple-platform-ios := $(addprefix _build-apple-,$(apple-platform-targets-ios)) +build-apple-platform-tvos := $(addprefix _build-apple-,$(apple-platform-targets-tvos)) +build-apple-platform-watchos := $(addprefix _build-apple-,$(apple-platform-targets-watchos)) + +# Creating xcframework for one single platform, including real device and simulator. +xcframework-only-macos: $(build-apple-platform-macos) +xcframework-only-ios: $(build-apple-platform-ios) +xcframework-only-tvos: $(build-apple-platform-tvos) +xcframework-only-watchos: $(build-apple-platform-watchos) +xcframework-only-%: + cargo run --quiet --bin xcframework -- --profile $(CARGO_PROFILE) --targets $(apple-platform-targets-$*) + +# Creating xcframework for all platforms. +xcframework-all: $(build-apple-platform-macos) $(build-apple-platform-ios) $(build-apple-platform-tvos) $(build-apple-platform-watchos) + cargo run --quiet --bin xcframework -- --profile $(CARGO_PROFILE) --targets $(apple-platform-targets) + ifeq ($(SKIP_PACKAGE_WP_API),true) xcframework: @echo "Skip building libwordpressFFI.xcframework" else - -# Generate the xcframework -# -# Run `make setup-rust` to install required rust toolchain. -xcframework: bindings xcframework-combined-libraries xcframework-headers - - rm -rf target/libwordpressFFI.xcframework - - xcodebuild -create-xcframework \ - -library target/aarch64-apple-ios/release/libwordpress.a \ - -headers target/swift-bindings/headers \ - -library target/universal-macos/release/libwordpress.a \ - -headers target/swift-bindings/headers \ - -library target/universal-ios/release/libwordpress.a \ - -headers target/swift-bindings/headers \ - -library target/aarch64-apple-tvos/release/libwordpress.a \ - -headers target/swift-bindings/headers \ - -library target/universal-tvos/release/libwordpress.a \ - -headers target/swift-bindings/headers \ - -library target/universal-watchos/release/libwordpress.a \ - -headers target/swift-bindings/headers \ - -output target/libwordpressFFI.xcframework - +xcframework: xcframework-all endif docker-image-swift: diff --git a/xcframework/Cargo.toml b/xcframework/Cargo.toml new file mode 100644 index 000000000..3e3a80e24 --- /dev/null +++ b/xcframework/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "xcframework" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.0", features = ["derive"] } +serde_json = "1.0" +anyhow = "1.0" diff --git a/xcframework/src/main.rs b/xcframework/src/main.rs new file mode 100644 index 000000000..30ad9173c --- /dev/null +++ b/xcframework/src/main.rs @@ -0,0 +1,332 @@ +use anyhow::{Context, Result}; +use clap::*; +use std::collections::HashMap; +use std::fmt::Display; +use std::path::{Path, PathBuf}; +use std::process::Command; + +const XCFRAMEWORK_OUTPUT_PATH: &str = "target/libwordpressFFI.xcframework"; +const SWIFT_BINDINGS_HEADER_DIR: &str = "target/swift-bindings/headers"; +const LIBRARY_FILENAME: &str = "libwordpress.a"; + +fn main() -> Result<()> { + CreateXCFramework::parse().run() +} + +#[derive(Debug, Parser)] +pub struct CreateXCFramework { + // Non-empty list of targets + #[clap( + long, + num_args = 1.., + required = true, + help = "List of targets whose static libraries should be included in the xcframework" + )] + targets: Vec, + + #[clap( + long, + default_value = "release", + help = "Cargo profile used to build the targets" + )] + profile: String, +} + +impl CreateXCFramework { + fn run(&self) -> Result<()> { + let temp_dir = std::env::temp_dir().join("wp-rs-xcframework"); + recreate_directory(&temp_dir)?; + + XCFramework::new(&self.targets, &self.profile)?.create(&temp_dir)?; + + Ok(()) + } +} + +// Represent a xcframework that contains static libraries for multiple platforms. +// +// Since `xcodebuild -create-xcframework` command requires its `-libraray` not +// having duplicated platform. This type along with `LibraryGroup` and `Slice` +// work together to make it easier to create a xcframework. +struct XCFramework { + libraries: Vec, + headers: PathBuf, +} + +// Represent a group of static libraries that are built for the same platform. +struct LibraryGroup { + id: LibraryGroupId, + slices: Vec, +} + +// Represent a thin static library which is built with `cargo build --target --profile ` +struct Slice { + target: String, + profile: String, +} + +impl XCFramework { + fn new(targets: &Vec, profile: &str) -> Result { + let headers = PathBuf::from(SWIFT_BINDINGS_HEADER_DIR); + if !headers.exists() { + anyhow::bail!("Headers not found: {}", headers.display()) + } + + let mut groups = HashMap::::new(); + for target in targets { + let id = LibraryGroupId::from_target(target)?; + let id_clone = id.clone(); + groups + .entry(id) + .or_insert(LibraryGroup { + id: id_clone, + slices: Vec::new(), + }) + .slices + .push(Slice { + target: target.clone(), + profile: profile.to_owned(), + }); + } + + Ok(Self { + libraries: groups.into_values().collect(), + headers, + }) + } + + fn create(&self, temp_dir: &Path) -> Result { + self.preview(); + + let libraries = self.combine_libraries(temp_dir)?; + let temp_dest = self.create_xcframework(&libraries, temp_dir)?; + + let dest = PathBuf::from(XCFRAMEWORK_OUTPUT_PATH); + recreate_directory(&dest)?; + std::fs::rename(temp_dest, &dest).with_context(|| "Failed to move xcframework")?; + + println!("xcframework created at {}", &dest.display()); + Ok(dest) + } + + fn preview(&self) { + println!("Creating xcframework to include the following targets:"); + for lib in &self.libraries { + println!(" Platform: {}", lib.id); + for slice in &lib.slices { + println!(" - {}", slice.target); + } + } + } + + fn combine_libraries(&self, temp_dir: &Path) -> Result> { + self.libraries + .iter() + .map(|lib| lib.create(temp_dir)) + .collect() + } + + fn create_xcframework(&self, libraries: &[PathBuf], temp_dir: &Path) -> Result { + let temp_dest = temp_dir.join("libwordpressFFI.xcframework"); + std::fs::remove_dir_all(&temp_dest).ok(); + + let library_args = libraries.iter().flat_map(|lib| { + [ + "-library".as_ref(), + lib.as_os_str(), + "-headers".as_ref(), + self.headers.as_os_str(), + ] + }); + Command::new("xcodebuild") + .arg("-create-xcframework") + .args(library_args) + .arg("-output") + .arg(&temp_dest) + .successful_output()?; + + Ok(temp_dest) + } +} + +impl LibraryGroup { + fn create(&self, temp_dir: &Path) -> Result { + let mut libraries: Vec = Vec::new(); + for slice in &self.slices { + libraries.push(slice.create(temp_dir)?); + } + + let dir = temp_dir.join(self.id.to_string()); + recreate_directory(&dir)?; + + let dest = dir.join(LIBRARY_FILENAME); + Command::new("lipo") + .arg("-create") + .args(libraries) + .arg("-output") + .arg(&dest) + .successful_output()?; + + Ok(dest) + } +} + +impl Slice { + fn create(&self, temp_dir: &Path) -> Result { + let libs = self.built_libraries(); + + // If there are more static libraries (a.k.a cargo packages), we'll + // need to bundle them together into one static library. + // At the moment, we only have one libwp_api, so we can just copy it. + assert!( + libs.len() == 1, + "Expected exactly one library for each slice" + ); + + let lib = &libs[0]; + if !lib.exists() { + anyhow::bail!("Library not found: {}", lib.display()) + } + + let dir = temp_dir.join(&self.target); + recreate_directory(&dir)?; + + let dest = dir.join(LIBRARY_FILENAME); + std::fs::copy(lib, &dest) + .with_context(|| format!("Failed to copy {} to {}", lib.display(), dest.display()))?; + + Ok(dest) + } + + fn built_libraries(&self) -> Vec { + let mut target_dir: PathBuf = ["target", &self.target].iter().collect(); + if self.profile == "dev" { + target_dir.push("debug"); + } else { + target_dir.push(&self.profile); + } + + vec![target_dir.join("libwp_api.a")] + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +struct LibraryGroupId { + os: ApplePlatform, + is_sim: bool, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +enum ApplePlatform { + MacOS, + #[allow(clippy::upper_case_acronyms)] + IOS, + TvOS, + WatchOS, +} + +impl TryFrom<&str> for ApplePlatform { + type Error = anyhow::Error; + + fn try_from(s: &str) -> std::result::Result { + match s { + "darwin" => Ok(ApplePlatform::MacOS), + "ios" => Ok(ApplePlatform::IOS), + "tvos" => Ok(ApplePlatform::TvOS), + "watchos" => Ok(ApplePlatform::WatchOS), + _ => anyhow::bail!("Unknown Apple platform: {}", s), + } + } +} + +impl Display for ApplePlatform { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match self { + ApplePlatform::MacOS => "macos", + ApplePlatform::IOS => "ios", + ApplePlatform::TvOS => "tvos", + ApplePlatform::WatchOS => "watchos", + }; + write!(f, "{}", name) + } +} + +impl LibraryGroupId { + fn from_target(target: &str) -> Result { + let mut parts = target.split('-'); + _ /* arch */= parts.next(); + if parts.next() != Some("apple") { + anyhow::bail!("{} is not an Apple platform", target) + } + + let os: ApplePlatform = parts + .next() + .with_context(|| format!("No OS in target: {}", target))? + .try_into()?; + + let output = Command::new("rustc") + .env("RUSTC_BOOTSTRAP", "1") + .args([ + "-Z", + "unstable-options", + "--print", + "target-spec-json", + "--target", + ]) + .arg(target) + .successful_output()?; + let json = serde_json::from_slice::(&output.stdout) + .with_context(|| "Failed to parse command output as JSON")?; + let llvm_target = json + .get("llvm-target") + .and_then(|t| t.as_str()) + .with_context(|| "No llvm-target in command output")?; + + Ok(Self { + os, + is_sim: llvm_target.ends_with("-simulator"), + }) + } +} + +impl Display for LibraryGroupId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.os)?; + + if self.is_sim { + write!(f, "-sim") + } else { + Ok(()) + } + } +} + +trait ExecuteCommand { + fn successful_output(&mut self) -> Result; +} + +impl ExecuteCommand for Command { + fn successful_output(&mut self) -> Result { + let output = self + .output() + .with_context(|| format!("Command failed: $ {:?}", self))?; + if output.status.success() { + Ok(output) + } else { + anyhow::bail!( + "Command failed with exit code: {}\n$ {:?}", + output.status, + self + ) + } + } +} + +fn recreate_directory(dir: &PathBuf) -> Result<()> { + if dir.exists() { + std::fs::remove_dir_all(dir) + .with_context(|| format!("Failed to remove directory at {:?}", dir))?; + } + + std::fs::create_dir_all(dir).with_context(|| format!("Failed to create directory: {:?}", dir)) +}