forked from kellyvv/StableAudio3-IOS
-
Notifications
You must be signed in to change notification settings - Fork 0
Add XCFramework build script with C-ABI wrapper #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
olilarkin
wants to merge
1
commit into
main
Choose a base branch
from
claude/swift-xcframework-script-lVAIw
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,261 @@ | ||
| #!/usr/bin/env bash | ||
| # Builds StableAudioKit.xcframework with all transitive dependencies (mlx-swift, | ||
| # swift-sentencepiece) statically bundled into a single static framework binary. | ||
| # Produces one slice per platform/destination and combines them with | ||
| # xcodebuild -create-xcframework. | ||
| # | ||
| # Usage: | ||
| # ./Scripts/build-xcframework.sh [output_dir] | ||
| # | ||
| # Output: <output_dir>/StableAudioKit.xcframework (default: build/xcframework) | ||
| # | ||
| # Requirements: macOS with Xcode 15+ command line tools. The script must be run | ||
| # from a clean checkout — it temporarily rewrites Package.swift to swap the | ||
| # local ../mlx-swift dependency for the upstream git URL. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| if [[ "$(uname -s)" != "Darwin" ]]; then | ||
| echo "error: This script must run on macOS." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| PACKAGE_DIR="$(dirname "$SCRIPT_DIR")" | ||
| RESOURCES_DIR="$SCRIPT_DIR/xcframework-resources" | ||
| COMPILE_METALLIB="$SCRIPT_DIR/compile-mlx-metallib.sh" | ||
|
|
||
| OUTPUT_DIR="${1:-"$PACKAGE_DIR/build/xcframework"}" | ||
| WORK_DIR="$PACKAGE_DIR/build/xcframework-work" | ||
| SLICES_DIR="$WORK_DIR/slices" | ||
| XCFRAMEWORK_PATH="$OUTPUT_DIR/StableAudioKit.xcframework" | ||
|
|
||
| # Mlx-swift fork used in place of the ../mlx-swift path dependency. | ||
| MLXSWIFT_URL="https://github.com/olilarkin/mlx-swift" | ||
| MLXSWIFT_BRANCH="${MLXSWIFT_BRANCH:-main}" | ||
|
|
||
| # The package's Sources/StableAudioKit module name as exposed to Swift consumers. | ||
| MODULE_NAME="StableAudioKit" | ||
| FRAMEWORK_NAME="StableAudioKit" | ||
|
|
||
| # (slice_id, scheme_destination, sdk, min_os, swift_triple_prefix, metal_target_prefix) | ||
| # We build StableAudioKit (the SPM library product) per slice via xcodebuild. | ||
| SLICES=( | ||
| "macos|generic/platform=macOS|macosx|14.0|arm64-apple-macosx|air64-apple-macosx" | ||
| "ios|generic/platform=iOS|iphoneos|17.0|arm64-apple-ios|air64-apple-ios" | ||
| "ios-simulator|generic/platform=iOS Simulator|iphonesimulator|17.0|arm64-apple-ios-simulator|air64-apple-ios-simulator" | ||
| "xros|generic/platform=visionOS|xros|1.0|arm64-apple-xros|air64-apple-xros" | ||
| "xros-simulator|generic/platform=visionOS Simulator|xrsimulator|1.0|arm64-apple-xros-simulator|air64-apple-xros-simulator" | ||
| ) | ||
|
|
||
| if [[ ! -x "$COMPILE_METALLIB" ]]; then | ||
| echo "error: $COMPILE_METALLIB not found or not executable." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| mkdir -p "$WORK_DIR" "$SLICES_DIR" "$OUTPUT_DIR" | ||
|
|
||
| # --- Prep: rewrite Package.swift to use the public mlx-swift fork --- | ||
|
|
||
| PACKAGE_FILE="$PACKAGE_DIR/Package.swift" | ||
| PACKAGE_BACKUP="$WORK_DIR/Package.swift.bak" | ||
| RESOLVED_FILE="$PACKAGE_DIR/Package.resolved" | ||
| RESOLVED_BACKUP="$WORK_DIR/Package.resolved.bak" | ||
|
|
||
| cp "$PACKAGE_FILE" "$PACKAGE_BACKUP" | ||
| if [[ -f "$RESOLVED_FILE" ]]; then | ||
| cp "$RESOLVED_FILE" "$RESOLVED_BACKUP" | ||
| fi | ||
|
|
||
| restore_package() { | ||
| if [[ -f "$PACKAGE_BACKUP" ]]; then | ||
| mv "$PACKAGE_BACKUP" "$PACKAGE_FILE" | ||
| fi | ||
| if [[ -f "$RESOLVED_BACKUP" ]]; then | ||
| mv "$RESOLVED_BACKUP" "$RESOLVED_FILE" | ||
| fi | ||
| } | ||
| trap restore_package EXIT | ||
|
|
||
| python3 - "$PACKAGE_FILE" "$MLXSWIFT_URL" "$MLXSWIFT_BRANCH" <<'PY' | ||
| import re, sys, pathlib | ||
| path, url, branch = sys.argv[1], sys.argv[2], sys.argv[3] | ||
| src = pathlib.Path(path).read_text() | ||
| new = re.sub( | ||
| r'\.package\(\s*path:\s*"\.\./mlx-swift"\s*\)', | ||
| f'.package(url: "{url}", branch: "{branch}")', | ||
| src, | ||
| ) | ||
| if new == src: | ||
| print("warn: no ../mlx-swift path dependency found in Package.swift to rewrite", | ||
| file=sys.stderr) | ||
| pathlib.Path(path).write_text(new) | ||
| PY | ||
|
|
||
| # --- Build each slice --- | ||
|
|
||
| build_slice() { | ||
| local id="$1" dest="$2" sdk="$3" min_os="$4" triple_prefix="$5" metal_target_prefix="$6" | ||
|
|
||
| echo "" | ||
| echo "==> Building slice: $id ($dest)" | ||
|
|
||
| local derived="$WORK_DIR/derived-$id" | ||
| rm -rf "$derived" | ||
|
|
||
| xcodebuild build \ | ||
| -scheme "$MODULE_NAME" \ | ||
| -destination "$dest" \ | ||
| -derivedDataPath "$derived" \ | ||
| -configuration Release \ | ||
| -skipPackagePluginValidation \ | ||
| BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ | ||
| SKIP_INSTALL=NO \ | ||
| SWIFT_OPTIMIZATION_LEVEL="-O" \ | ||
| OTHER_SWIFT_FLAGS="-no-verify-emitted-module-interface" \ | ||
| ONLY_ACTIVE_ARCH=NO | ||
|
|
||
| # Locate the build products directory for this slice. | ||
| local products | ||
| products="$(find "$derived/Build/Products" -maxdepth 1 -type d -name "Release-*" | head -n1)" | ||
| if [[ -z "$products" ]]; then | ||
| echo "error: Could not find Release-* products directory for slice $id." >&2 | ||
| exit 1 | ||
| fi | ||
| echo " products: $products" | ||
|
|
||
| # Collect every static archive produced by the package build. | ||
| # SPM Swift/C/C++ targets each emit lib<Target>.a here; xcodebuild puts | ||
| # mlx-swift, sentencepiece, sentencepiece C++, and our own targets together. | ||
| local merge_input | ||
| merge_input="$WORK_DIR/merge-$id" | ||
| rm -rf "$merge_input" | ||
| mkdir -p "$merge_input" | ||
|
|
||
| local archives=() | ||
| while IFS= read -r -d '' lib; do | ||
| archives+=("$lib") | ||
| done < <(find "$products" -maxdepth 2 -name "*.a" -print0) | ||
|
|
||
| # Some SPM builds bury archives under PackageFrameworks; pick those up too. | ||
| while IFS= read -r -d '' lib; do | ||
| archives+=("$lib") | ||
| done < <(find "$derived/Build/Intermediates.noindex" -name "*.a" -print0 2>/dev/null) | ||
|
|
||
| if [[ ${#archives[@]} -eq 0 ]]; then | ||
| echo "error: No .a archives found for slice $id." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo " merging ${#archives[@]} static archive(s)" | ||
| local merged_binary="$merge_input/$FRAMEWORK_NAME" | ||
| xcrun libtool -static -no_warning_for_no_symbols -o "$merged_binary" "${archives[@]}" | ||
|
|
||
| # Assemble the .framework bundle. | ||
| local slice_root="$SLICES_DIR/$id" | ||
| local fw="$slice_root/$FRAMEWORK_NAME.framework" | ||
| rm -rf "$slice_root" | ||
| mkdir -p "$fw/Headers" "$fw/Modules/$MODULE_NAME.swiftmodule" | ||
|
|
||
| cp "$merged_binary" "$fw/$FRAMEWORK_NAME" | ||
| cp "$RESOURCES_DIR/StableAudioKit.h" "$fw/Headers/StableAudioKit.h" | ||
| cp "$RESOURCES_DIR/module.modulemap" "$fw/Modules/module.modulemap" | ||
|
|
||
| # Find the StableAudioKit swiftmodule directory produced for this slice and | ||
| # copy its arch-specific files into the framework. xcodebuild typically | ||
| # writes it under Build/Intermediates.noindex/.../$MODULE_NAME.swiftmodule/. | ||
| local found_modules=() | ||
| while IFS= read -r -d '' f; do | ||
| found_modules+=("$f") | ||
| done < <(find "$derived" \ | ||
| -type d -name "$MODULE_NAME.swiftmodule" -print0 2>/dev/null) | ||
|
|
||
| if [[ ${#found_modules[@]} -eq 0 ]]; then | ||
| echo "error: No $MODULE_NAME.swiftmodule directory found for slice $id." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Prefer a directory that contains a .swiftinterface (BUILD_LIBRARY_FOR_DISTRIBUTION output). | ||
| local best="" | ||
| for d in "${found_modules[@]}"; do | ||
| if compgen -G "$d/*.swiftinterface" > /dev/null; then | ||
| best="$d" | ||
| break | ||
| fi | ||
| done | ||
| if [[ -z "$best" ]]; then | ||
| best="${found_modules[0]}" | ||
| fi | ||
| echo " swiftmodule: $best" | ||
|
|
||
| # Copy every per-arch artifact (swiftmodule/swiftdoc/swiftinterface/abi.json). | ||
| find "$best" -maxdepth 1 -type f \ | ||
| \( -name "*.swiftmodule" -o -name "*.swiftdoc" \ | ||
| -o -name "*.swiftinterface" -o -name "*.private.swiftinterface" \ | ||
| -o -name "*.abi.json" -o -name "*.swiftsourceinfo" \) \ | ||
| -exec cp {} "$fw/Modules/$MODULE_NAME.swiftmodule/" \; | ||
|
|
||
| # Write the per-slice Info.plist with the right CFBundleSupportedPlatforms / | ||
| # MinimumOSVersion. We use a single hand-rolled file; macOS frameworks | ||
| # normally use Versions/A but the flat layout works for create-xcframework | ||
| # consumption on every Apple platform. | ||
| local platform_value | ||
| case "$sdk" in | ||
| macosx) platform_value="MacOSX" ;; | ||
| iphoneos) platform_value="iPhoneOS" ;; | ||
| iphonesimulator) platform_value="iPhoneSimulator" ;; | ||
| xros) platform_value="XROS" ;; | ||
| xrsimulator) platform_value="XRSimulator" ;; | ||
| *) echo "error: unknown sdk $sdk" >&2; exit 1 ;; | ||
| esac | ||
| sed -e "s/__SUPPORTED_PLATFORM__/$platform_value/" \ | ||
| -e "s/__MIN_OS_VERSION__/$min_os/" \ | ||
| "$RESOURCES_DIR/Info.plist.tmpl" > "$fw/Info.plist" | ||
|
|
||
| # Compile mlx.metallib for this slice and embed it in the framework. | ||
| # The compile script targets macOS by default — invoke it with the | ||
| # right Metal target so the resulting metallib loads at runtime. | ||
| local metal_target | ||
| case "$sdk" in | ||
| macosx) metal_target="${metal_target_prefix}${min_os}" ;; | ||
| iphoneos) metal_target="${metal_target_prefix}${min_os}" ;; | ||
| iphonesimulator) metal_target="${metal_target_prefix}${min_os}" ;; | ||
| xros) metal_target="${metal_target_prefix}${min_os}" ;; | ||
| xrsimulator) metal_target="${metal_target_prefix}${min_os}" ;; | ||
| esac | ||
|
|
||
| local mlx_checkout="$derived/SourcePackages/checkouts/mlx-swift" | ||
| METAL_TARGET="$metal_target" MLX_SWIFT_DIR="$mlx_checkout" \ | ||
| "$COMPILE_METALLIB" "$fw" >/dev/null | ||
|
|
||
| # Copy any *.bundle resources produced by SPM dependencies (e.g. sentencepiece). | ||
| while IFS= read -r -d '' bundle; do | ||
| cp -R "$bundle" "$fw/" | ||
| done < <(find "$products" -maxdepth 2 -type d -name "*.bundle" -print0) | ||
|
|
||
| echo " slice ready: $fw" | ||
| } | ||
|
|
||
| for entry in "${SLICES[@]}"; do | ||
| IFS='|' read -r id dest sdk min_os triple_prefix metal_target_prefix <<<"$entry" | ||
| build_slice "$id" "$dest" "$sdk" "$min_os" "$triple_prefix" "$metal_target_prefix" | ||
| done | ||
|
|
||
| # --- Combine slices into a single XCFramework --- | ||
|
|
||
| echo "" | ||
| echo "==> Creating XCFramework" | ||
| rm -rf "$XCFRAMEWORK_PATH" | ||
|
|
||
| CREATE_ARGS=() | ||
| for entry in "${SLICES[@]}"; do | ||
| IFS='|' read -r id _rest <<<"$entry" | ||
| CREATE_ARGS+=(-framework "$SLICES_DIR/$id/$FRAMEWORK_NAME.framework") | ||
| done | ||
|
|
||
| xcodebuild -create-xcframework \ | ||
| "${CREATE_ARGS[@]}" \ | ||
| -output "$XCFRAMEWORK_PATH" | ||
|
|
||
| echo "" | ||
| echo "Done: $XCFRAMEWORK_PATH" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>CFBundleDevelopmentRegion</key> | ||
| <string>en</string> | ||
| <key>CFBundleExecutable</key> | ||
| <string>StableAudioKit</string> | ||
| <key>CFBundleIdentifier</key> | ||
| <string>com.olilarkin.StableAudioKit</string> | ||
| <key>CFBundleInfoDictionaryVersion</key> | ||
| <string>6.0</string> | ||
| <key>CFBundleName</key> | ||
| <string>StableAudioKit</string> | ||
| <key>CFBundlePackageType</key> | ||
| <string>FMWK</string> | ||
| <key>CFBundleShortVersionString</key> | ||
| <string>1.0.0</string> | ||
| <key>CFBundleVersion</key> | ||
| <string>1</string> | ||
| <key>CFBundleSupportedPlatforms</key> | ||
| <array> | ||
| <string>__SUPPORTED_PLATFORM__</string> | ||
| </array> | ||
| <key>MinimumOSVersion</key> | ||
| <string>__MIN_OS_VERSION__</string> | ||
| </dict> | ||
| </plist> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For Swift consumers, copying only
StableAudioKit.swiftmoduleleaves the generated.swiftinterfacewith unresolved public dependency types:AudioWriter.write(_ audio: MLXArray, ...)is public, so the interface imports/references MLX, but this script only libtool-merges the dependency binaries and never packages the dependency.swiftmodule/.swiftinterfacefiles. A client that adds only this supposedly self-contained XCFramework and runsimport StableAudioKitwill fail to compile because it cannot load the MLX module; either hide those dependency types from the public API or include the required Swift modules alongside the framework.Useful? React with 👍 / 👎.