From 1d84d2481110c85aeadec0b1bde90beafce6a9c2 Mon Sep 17 00:00:00 2001 From: dz Date: Sat, 25 Apr 2026 17:36:44 -0300 Subject: [PATCH] Add arm64 Darwin Nix SwiftPM shell --- .../build.sh | 13 +- .../swift-package-test-package/Package.swift | 6 + .../main.swift | 11 ++ crates/swift-bridge-build/src/package.rs | 145 ++++++++++++++++-- flake.lock | 27 ++++ flake.nix | 30 ++++ test-swift-packages.sh | 12 +- 7 files changed, 217 insertions(+), 27 deletions(-) create mode 100644 SwiftRustIntegrationTestRunner/swift-package-test-package/Sources/swift-package-test-packageTestsRunner/main.swift create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/SwiftRustIntegrationTestRunner/swift-package-rust-library-fixture/build.sh b/SwiftRustIntegrationTestRunner/swift-package-rust-library-fixture/build.sh index 86bc3c7e..cbfc85f6 100755 --- a/SwiftRustIntegrationTestRunner/swift-package-rust-library-fixture/build.sh +++ b/SwiftRustIntegrationTestRunner/swift-package-rust-library-fixture/build.sh @@ -4,13 +4,12 @@ THIS_DIR=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) ROOT_DIR="$THIS_DIR" cd $ROOT_DIR -cargo build --target x86_64-apple-darwin --target-dir "$(pwd)/target" -cargo build --target aarch64-apple-darwin --target-dir "$(pwd)/target" +RUST_TARGET="${RUST_TARGET:-aarch64-apple-darwin}" -mkdir -p "$(pwd)/target/universal/" +cargo build --target "$RUST_TARGET" --target-dir "$(pwd)/target" -lipo \ - $(pwd)/target/aarch64-apple-darwin/debug/libtest_swift_packages.a \ - $(pwd)/target/x86_64-apple-darwin/debug/libtest_swift_packages.a -create -output \ - $(pwd)/target/universal/libtest_swift_packages.a +mkdir -p "$(pwd)/target/universal/" +cp \ + "$(pwd)/target/$RUST_TARGET/debug/libtest_swift_packages.a" \ + "$(pwd)/target/universal/libtest_swift_packages.a" diff --git a/SwiftRustIntegrationTestRunner/swift-package-test-package/Package.swift b/SwiftRustIntegrationTestRunner/swift-package-test-package/Package.swift index 0c823555..e63b5ae4 100644 --- a/SwiftRustIntegrationTestRunner/swift-package-test-package/Package.swift +++ b/SwiftRustIntegrationTestRunner/swift-package-test-package/Package.swift @@ -8,6 +8,9 @@ let package = Package( .library( name: "swift-package-test-package", targets: ["swift-package-test-package"]), + .executable( + name: "swift-package-test-packageTestsRunner", + targets: ["swift-package-test-packageTestsRunner"]), ], dependencies: [ .package(path: "../swift-package-rust-library-fixture/MySwiftPackage") @@ -16,6 +19,9 @@ let package = Package( .target( name: "swift-package-test-package", dependencies: []), + .executableTarget( + name: "swift-package-test-packageTestsRunner", + dependencies: ["MySwiftPackage"]), .testTarget( name: "swift-package-test-packageTests", dependencies: ["swift-package-test-package", "MySwiftPackage"]), diff --git a/SwiftRustIntegrationTestRunner/swift-package-test-package/Sources/swift-package-test-packageTestsRunner/main.swift b/SwiftRustIntegrationTestRunner/swift-package-test-package/Sources/swift-package-test-packageTestsRunner/main.swift new file mode 100644 index 00000000..16d06db1 --- /dev/null +++ b/SwiftRustIntegrationTestRunner/swift-package-test-package/Sources/swift-package-test-packageTestsRunner/main.swift @@ -0,0 +1,11 @@ +import MySwiftPackage + +func expect(_ condition: @autoclosure () -> Bool, _ message: String) { + if !condition() { + fatalError(message) + } +} + +expect(hello_rust().toString() == "Hello, From Rust!", "Rust string did not match") +expect(SomeStruct(field: 1).field == 1, "Shared struct field did not match") +expect(UnnamedStruct(_0: 1)._0 == 1, "Unnamed shared struct field did not match") diff --git a/crates/swift-bridge-build/src/package.rs b/crates/swift-bridge-build/src/package.rs index ae143842..45592acc 100644 --- a/crates/swift-bridge-build/src/package.rs +++ b/crates/swift-bridge-build/src/package.rs @@ -105,14 +105,20 @@ pub fn create_package(config: CreatePackageConfig) { } // Generate RustXcframework // - gen_xcframework(&output_dir, &config); + let rust_target = gen_xcframework(&output_dir, &config); // Generate Swift Package // - gen_package(&output_dir, &config); + gen_package(&output_dir, &config, rust_target); +} + +#[derive(Copy, Clone)] +enum RustTarget { + Xcframework, + SwiftpmTarget, } /// Generates the RustXcframework -fn gen_xcframework(output_dir: &Path, config: &CreatePackageConfig) { +fn gen_xcframework(output_dir: &Path, config: &CreatePackageConfig) -> RustTarget { // Create directories let temp_dir = tempdir().expect("Couldn't create temporary directory"); let tmp_framework_path = &temp_dir.path().join("swiftbridge._tmp_framework"); @@ -229,16 +235,33 @@ fn gen_xcframework(output_dir: &Path, config: &CreatePackageConfig) { let output = Command::new("xcodebuild") .current_dir(&tmp_framework_path) - .args(args) + .args(&args) .stdout(Stdio::piped()) - .spawn() - .expect("Failed to spawn xcodebuild") - .wait_with_output() - .expect("Failed to execute xcodebuild"); - if !output.status.success() { - let stderr = std::str::from_utf8(&output.stderr).unwrap(); - panic!("{}", stderr); - } + .stderr(Stdio::piped()) + .spawn(); + let rust_target = match output { + Ok(child) => { + let output = child + .wait_with_output() + .expect("Failed to execute xcodebuild"); + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr).unwrap(); + if stderr.contains("tool 'xcodebuild' not found") { + gen_swiftpm_rust_target(output_dir, &tmp_framework_path, config); + RustTarget::SwiftpmTarget + } else { + panic!("{}", stderr); + } + } else { + RustTarget::Xcframework + } + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + gen_swiftpm_rust_target(output_dir, &tmp_framework_path, config); + RustTarget::SwiftpmTarget + } + Err(err) => panic!("Failed to spawn xcodebuild: {}", err), + }; // Remove temporary directory let temp_dir_string = temp_dir.path().to_str().unwrap().to_string(); @@ -248,6 +271,55 @@ fn gen_xcframework(output_dir: &Path, config: &CreatePackageConfig) { temp_dir_string, err ); } + + rust_target +} + +fn gen_swiftpm_rust_target( + output_dir: &Path, + tmp_framework_path: &Path, + config: &CreatePackageConfig, +) { + if config.paths.len() != 1 || !config.paths.contains_key(&ApplePlatform::MacOS) { + panic!("SwiftPM Rust target fallback only supports a single macOS library"); + } + + let xcframework_dir = output_dir.join("RustXcframework.xcframework"); + if xcframework_dir.exists() { + fs::remove_dir_all(&xcframework_dir).expect("Couldn't delete unused xcframework directory"); + } + + let target_dir = output_dir.join("RustXcframework"); + if target_dir.exists() { + fs::remove_dir_all(&target_dir).expect("Couldn't delete previous SwiftPM Rust target"); + } + fs::create_dir_all(target_dir.join("include")) + .expect("Couldn't create SwiftPM Rust target include directory"); + fs::create_dir_all(target_dir.join("lib")) + .expect("Couldn't create SwiftPM Rust target lib directory"); + fs::write( + target_dir.join("shim.c"), + "void swift_bridge_static_library_linker_shim(void) {}\n", + ) + .expect("Couldn't write SwiftPM Rust target shim"); + + for header in fs::read_dir(tmp_framework_path.join("include")).expect("Couldn't read headers") { + let header = header.expect("Couldn't read header").path(); + fs::copy( + &header, + target_dir.join("include").join(header.file_name().unwrap()), + ) + .expect("Couldn't copy header into SwiftPM Rust target"); + } + + let lib_path: &Path = config.paths.get(&ApplePlatform::MacOS).unwrap().as_ref(); + fs::copy( + tmp_framework_path + .join(ApplePlatform::MacOS.dir_name()) + .join(lib_path.file_name().unwrap()), + target_dir.join("lib").join(lib_path.file_name().unwrap()), + ) + .expect("Couldn't copy library into SwiftPM Rust target"); } /// Generates the Swift Package. @@ -259,7 +331,7 @@ fn gen_xcframework(output_dir: &Path, config: &CreatePackageConfig) { /// The alternative would be to use something like `@_exported import RustXcframework`, but this /// would make the Rust xcframework (i.e. methods like __swift_bridge__$some_method) available to /// the Swift Package's consumer, which we don't want. -fn gen_package(output_dir: &Path, config: &CreatePackageConfig) { +fn gen_package(output_dir: &Path, config: &CreatePackageConfig, rust_target: RustTarget) { let sources_dir = output_dir.join("Sources").join(&config.package_name); if !sources_dir.exists() { fs::create_dir_all(&sources_dir).expect("Couldn't create directory for source files"); @@ -311,9 +383,41 @@ fn gen_package(output_dir: &Path, config: &CreatePackageConfig) { // Generate Package.swift let package_name = &config.package_name; + let package_directory = match rust_target { + RustTarget::Xcframework => "".to_string(), + RustTarget::SwiftpmTarget => { + "let packageDirectory = URL(fileURLWithPath: #filePath).deletingLastPathComponent().path\n" + .to_string() + } + }; + let rust_target_package_swift = match rust_target { + RustTarget::Xcframework => r#" .binaryTarget( + name: "RustXcframework", + path: "RustXcframework.xcframework" + ),"# + .to_string(), + RustTarget::SwiftpmTarget => { + let lib_name = swiftpm_link_library_name(config); + r#" .target( + name: "RustXcframework", + path: "RustXcframework", + publicHeadersPath: "include", + linkerSettings: [ + .unsafeFlags([ + "-L", + "\(packageDirectory)/RustXcframework/lib", + "-lLIB_NAME" + ]) + ] + ),"# + .replace("LIB_NAME", &lib_name) + } + }; let package_swift = format!( r#"// swift-tools-version:5.5.0 import PackageDescription +import Foundation +{package_directory} let package = Package( name: "{package_name}", products: [ @@ -323,10 +427,7 @@ let package = Package( ], dependencies: [], targets: [ - .binaryTarget( - name: "RustXcframework", - path: "RustXcframework.xcframework" - ), +{rust_target_package_swift} .target( name: "{package_name}", dependencies: ["RustXcframework"]) @@ -338,3 +439,13 @@ let package = Package( fs::write(output_dir.join("Package.swift"), package_swift) .expect("Couldn't write Package.swift file"); } + +fn swiftpm_link_library_name(config: &CreatePackageConfig) -> String { + let lib_path: &Path = config.paths.get(&ApplePlatform::MacOS).unwrap().as_ref(); + let file_name = lib_path.file_name().unwrap().to_str().unwrap(); + let without_prefix = file_name.strip_prefix("lib").unwrap_or(file_name); + without_prefix + .strip_suffix(".a") + .unwrap_or(without_prefix) + .to_string() +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..b3b55428 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1776949667, + "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..eb76dafe --- /dev/null +++ b/flake.nix @@ -0,0 +1,30 @@ +{ + description = "swift-bridge development shell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + }; + + outputs = + { nixpkgs, ... }: + let + system = "aarch64-darwin"; + pkgs = import nixpkgs { inherit system; }; + in + { + devShells.${system}.default = pkgs.mkShell { + packages = with pkgs; [ + cargo + rustc + swift + swiftPackages.swiftpm + swiftPackages.XCTest + ]; + + shellHook = '' + export CARGO_BUILD_TARGET=aarch64-apple-darwin + export PATH="$PATH:/usr/bin:/bin" + ''; + }; + }; +} diff --git a/test-swift-packages.sh b/test-swift-packages.sh index aefc59b1..ebc888c1 100755 --- a/test-swift-packages.sh +++ b/test-swift-packages.sh @@ -13,10 +13,15 @@ ROOT_DIR="$THIS_DIR" cd $ROOT_DIR # Make a temp directory -TEMP_DIR=$(mktemp -d) +mkdir -p "$ROOT_DIR/target/test-tmp" +TEMP_DIR=$(mktemp -d "$ROOT_DIR/target/test-tmp/test-swift-packages.XXXXXX") # Delete the temp directory before the shell exits -trap 'rm -rf $TEMP_DIR' EXIT +if [ -z "${KEEP_TEST_SWIFT_PACKAGES_TEMP:-}" ]; then + trap 'rm -rf $TEMP_DIR' EXIT +else + echo "Keeping temp directory: $TEMP_DIR" +fi # Copy directories related to all of the building and test running to the temp directory for DIR in crates src examples SwiftRustIntegrationTestRunner @@ -36,4 +41,5 @@ cargo run -p integration-test-create-swift-package # Test Swift Package cd swift-package-test-package -swift test +swift-build --disable-sandbox --product swift-package-test-packageTestsRunner +swift-run --disable-sandbox --skip-build swift-package-test-packageTestsRunner