build: Add XCFramework binary distribution support#266
Conversation
Generate the vendored SnapshotPreviews frameworks with a Bash-based Xcode archive flow and a build-only package manifest that forces dynamic products for packaging. Add a binary-framework DemoApp example, document the manual integration requirements, and update release packaging to publish the generated XCFramework artifacts from the XCFrameworks directory. Co-Authored-By: Codex <noreply@openai.com>
NicoHinderling
left a comment
There was a problem hiding this comment.
hopefully @noahsmartin can take a look as well
| run: bash build.sh | ||
| - name: Zip xcframeworks | ||
| run: | | ||
| rm -f *.xcframework.zip |
There was a problem hiding this comment.
I'll fix that. Bit pointless as the runner will be torn down.
| sanitize_swift_interfaces "$modules_path" "$framework" | ||
| } | ||
|
|
||
| sanitize_swift_interfaces() { |
There was a problem hiding this comment.
What's the motiviation for this logic? Seems it's not new, but just curious
There was a problem hiding this comment.
I actually had to ask Claude Code this as I didn't have context from the original implementation:
This is carried over from the existing XCFramework build flow. The motivation is to make the emitted
.swiftinterfacefiles importable by binary consumers.The generated interfaces can include private interfaces,
NSInvocationreferences from XCTest/ObjC interop, awkwardXCTest.qualification, and self-module-qualified names such asPreviewGallery.PreviewGallery. Those either are not useful to consumers or can break module import for the vendored frameworks.So this is not new behavior conceptually; it preserves the existing interface cleanup while expanding the framework set/platform matrix.
The runner is ephemeral and starts fresh each time, so cleaning up *.xcframework.zip files before zipping is unnecessary. Co-authored-by: Cameron Cooke <web@cameroncooke.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Snapshotting interfaces skip sanitization
- Modified copy_swift_module to sanitize archived Modules folder even when source swiftmodule is missing, ensuring Swift interfaces from dependencies are properly cleaned.
Or push these changes by commenting:
@cursor push 647219353e
Preview (647219353e)
diff --git a/build.sh b/build.sh
--- a/build.sh
+++ b/build.sh
@@ -96,16 +96,6 @@
configuration="Release"
fi
- local source_module_path="$DERIVED_DATA_DIR/$framework/Build/Intermediates.noindex/ArchiveIntermediates/$framework/BuildProductsPath/$configuration/$framework.swiftmodule"
- if [ ! -d "$source_module_path" ]; then
- if requires_swift_module "$framework"; then
- echo "Missing Swift module output for $framework at $source_module_path" >&2
- exit 1
- fi
-
- return
- fi
-
local framework_path="$archive_path/Products/Library/Frameworks/$framework.framework"
local modules_path="$framework_path/Modules"
@@ -117,6 +107,19 @@
ln -s "Versions/Current/Modules" "$framework_path/Modules"
fi
+ local source_module_path="$DERIVED_DATA_DIR/$framework/Build/Intermediates.noindex/ArchiveIntermediates/$framework/BuildProductsPath/$configuration/$framework.swiftmodule"
+ if [ ! -d "$source_module_path" ]; then
+ if requires_swift_module "$framework"; then
+ echo "Missing Swift module output for $framework at $source_module_path" >&2
+ exit 1
+ fi
+
+ if [ -d "$modules_path" ]; then
+ sanitize_swift_interfaces "$modules_path" "$framework"
+ fi
+ return
+ fi
+
mkdir -p "$modules_path"
if [ -d "$modules_path/$framework.swiftmodule" ]; then
rm -r "$modules_path/$framework.swiftmodule"You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 5df5671. Configure here.
… module is missing Ensure sanitize_swift_interfaces runs on Modules folder in the archived framework when the source swiftmodule path is absent but the archived Modules directory exists (e.g., from Swift dependencies like SnapshottingSwift). Applied via @cursor push command
| for platform in "${PLATFORMS[@]}"; do | ||
| sdk="${platform%%|*}" | ||
| destination="${platform#*|}" | ||
| build_framework_archive "$framework" "$sdk" "$destination" | ||
| done |
There was a problem hiding this comment.
Bug: The build script will fail when compiling SnapshottingTests for watchOS because EMGInvocationCreator.mm uses NSInvocation, which is unavailable on that platform.
Severity: CRITICAL
Suggested Fix
Wrap the usage of NSInvocation in EMGInvocationCreator.mm with an #if !TARGET_OS_WATCH preprocessor directive to exclude it from watchOS builds. Alternatively, exclude the SnapshottingTestsObjc source from the watchOS target in the package manifest files (Package.swift, XCFrameworkPackage.swift).
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: build.sh#L183-L187
Potential issue: The `build.sh` script now attempts to build and archive
`SnapshottingTests` for watchOS and watchOS Simulator. However, a dependency,
`EMGInvocationCreator.mm`, unconditionally uses `NSInvocation` in its `create:` method.
Since `NSInvocation` is not available in the watchOS SDK, the compilation will fail.
Because the build script has `set -euo pipefail` enabled, this compilation error will
cause the entire script to abort, preventing the generation of any XCFrameworks. This
issue was not present before as builds were only performed for iOS targets.
Did we get this right? 👍 / 👎 to inform future reviews.




Adds XCFramework binary distribution support for SnapshotPreviews while keeping the standard Swift Package integration source-first.
The release build now produces the full manually consumable framework set under
XCFrameworks/, packages those artifacts for GitHub releases, and includes a separate example project that integrates the generated frameworks without Swift Package Manager.Swift Package vs binary distribution
Package.swiftremains the public source-package manifest. SwiftPM is allowed to choose linkage for regular products, whileSnapshottingstays explicitly dynamic because it is the inserted/runtime dylib product.XCFrameworkPackage.swiftis a build-only manifest used bybuild.sh. It forces the currently built product to dynamic for binary packaging and wires already-built SnapshotPreviews dependencies back in as binary targets, so shared support modules are dynamically linked instead of being repeatedly embedded into parent frameworks.Vended artifact layout
Generated binary artifacts are written to
XCFrameworks/:SnapshotSharedModels.xcframeworkSnapshotPreviewsCore.xcframeworkSnapshotPreferences.xcframeworkPreviewGallery.xcframeworkSnapshottingTests.xcframeworkSnapshotting.xcframeworkPreviewsSupport.xcframeworkPreviewsSupportremains checked in atPreviewsSupport/PreviewsSupport.xcframeworkfor SwiftPM usage. The distribution build copies it intoXCFrameworks/so binary consumers and release assets use one artifact folder.Dynamic dependency model
Manual XCFramework integration does not get SwiftPM dependency resolution automatically, so consumers must link and embed the documented framework closure for each target.
The generated frameworks keep shared SnapshotPreviews dependencies as dynamic framework dependencies. This avoids duplicating shared support modules across frameworks when multiple SnapshotPreviews products are loaded in the same process.
Binary example project
Examples/DemoAppremains the normal local Swift Package example.Examples/DemoApp-XCFrameworksis the manual binary integration example. It shares source files from../DemoApp/..., links against../../XCFrameworks/*.xcframework, and includes a small wrapper script:cd Examples/DemoApp-XCFrameworks ./generate-xcframeworks.sh open DemoApp-XCFrameworks.xcodeprojShared demo source that depends on optional external packages uses
canImport, so the Swift Package example can keep accessibility snapshot rendering while the XCFramework example builds without that external package.Manual integration matrix
For an app target using per-preview rendering preferences:
SnapshotPreferences.xcframeworkSnapshotSharedModels.xcframeworkFor an app target using the gallery UI:
PreviewGallery.xcframeworkSnapshotPreviewsCore.xcframeworkSnapshotPreferences.xcframeworkSnapshotSharedModels.xcframeworkPreviewsSupport.xcframeworkFor an XCTest snapshot/layout target:
SnapshottingTests.xcframeworkSnapshotPreviewsCore.xcframeworkSnapshotSharedModels.xcframeworkPreviewsSupport.xcframeworkFor the inserted-dylib/accessibility runtime flow, also include:
Snapshotting.xcframeworkRefs EME-1168