Skip to content

build: Add XCFramework binary distribution support#265

Closed
cameroncooke wants to merge 1 commit into
mainfrom
cameroncooke/eme-1168-investigate-open-table-snapshotpreviews-issues
Closed

build: Add XCFramework binary distribution support#265
cameroncooke wants to merge 1 commit into
mainfrom
cameroncooke/eme-1168-investigate-open-table-snapshotpreviews-issues

Conversation

@cameroncooke
Copy link
Copy Markdown
Contributor

@cameroncooke cameroncooke commented Jun 2, 2026

Adds XCFramework binary distribution support for SnapshotPreviews while keeping the standard Swift Package integration source-first.

The binary distribution produces a documented framework set under XCFrameworks/, validates that those frameworks form the expected dynamic dependency graph, and includes a separate example project that consumes the generated frameworks manually.

Swift Package vs binary distribution

Package.swift keeps the normal Swift Package linkage model separate from binary distribution. Regular library products remain explicit static products for the source-package path, matching the existing package behavior and Xcode 15 CI expectations. Snapshotting remains explicitly dynamic because it is the inserted/runtime dylib product.

Binary artifact generation is handled separately by build.sh and the Ruby distribution tooling. That tooling creates temporary SwiftPM distribution packages and forces dynamic framework output only for the XCFramework build path.

Vended artifact layout

Generated binary artifacts are written to XCFrameworks/:

  • SnapshotSharedModels.xcframework
  • SnapshotPreviewsCore.xcframework
  • SnapshotPreferences.xcframework
  • PreviewGallery.xcframework
  • SnapshottingTests.xcframework
  • Snapshotting.xcframework
  • PreviewsSupport.xcframework

PreviewsSupport remains checked in at PreviewsSupport/PreviewsSupport.xcframework for SwiftPM usage. The distribution build copies it into XCFrameworks/ so binary consumers and GitHub release assets have one artifact folder.

Dynamic dependency model

Manual XCFramework integration does not get SwiftPM's dependency resolution automatically, so binary consumers must link the documented framework closure for the product they use.

The build validates that shared SnapshotPreviews dependencies remain real dynamic framework dependencies instead of being embedded repeatedly into parent frameworks. This avoids loading duplicate copies of shared modules when consumers link multiple SnapshotPreviews frameworks in the same process.

Binary example project

Examples/DemoApp is the normal local Swift Package example.

Examples/DemoApp-XCFrameworks is 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.xcodeproj

Shared demo source that depends on optional external packages uses canImport, so the SPM example can keep accessibility snapshot rendering while the XCFramework example builds without the external accessibility package.

Manual integration matrix

For an app target using snapshot preferences:

  • SnapshotPreferences.xcframework
  • SnapshotSharedModels.xcframework

For an app target using PreviewGallery:

  • PreviewGallery.xcframework
  • SnapshotPreviewsCore.xcframework
  • SnapshotPreferences.xcframework
  • SnapshotSharedModels.xcframework
  • PreviewsSupport.xcframework

For an XCTest target using SnapshottingTests:

  • SnapshottingTests.xcframework
  • SnapshotPreviewsCore.xcframework
  • SnapshotSharedModels.xcframework
  • PreviewsSupport.xcframework

For the inserted-dylib/accessibility runtime flow, also include:

  • Snapshotting.xcframework

Validation

The distribution validation checks expected artifacts, supported slices, public Swift interface imports, macOS Swift module placement, missing nested frameworks, expected dynamic load commands, and PreviewsSupport packaging.

The iOS binary smoke test builds a temporary binary-only app/test project and verifies app-target imports, test-target imports, manual framework loading, and snapshot PNG/JSON export.

Refs EME-1168

@linear-code
Copy link
Copy Markdown

linear-code Bot commented Jun 2, 2026

EME-1168

@cameroncooke cameroncooke marked this pull request as ready for review June 2, 2026 14:11
Comment thread build.sh Outdated
Comment thread build.sh Outdated
@cameroncooke cameroncooke requested a review from noahsmartin June 2, 2026 14:26
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: macOS Swift module path incorrect, silent skip
    • Updated copy_swift_module to use 'Release/' instead of 'Release-macosx/' for macOS SDK, matching the empty EFFECTIVE_PLATFORM_NAME behavior.

Create PR

Or push these changes by commenting:

@cursor push e92ce5d1a4
Preview (e92ce5d1a4)
diff --git a/build.sh b/build.sh
--- a/build.sh
+++ b/build.sh
@@ -55,7 +55,14 @@
   local sdk="$2"
   local archive_path="$3"
 
-  local source_module_path="$XCODEBUILD_DERIVED_DATA_PATH/Build/Intermediates.noindex/ArchiveIntermediates/$scheme/BuildProductsPath/Release-$sdk/$scheme.swiftmodule"
+  local platform_suffix
+  if [[ "$sdk" == "macosx" ]]; then
+    platform_suffix=""
+  else
+    platform_suffix="-$sdk"
+  fi
+
+  local source_module_path="$XCODEBUILD_DERIVED_DATA_PATH/Build/Intermediates.noindex/ArchiveIntermediates/$scheme/BuildProductsPath/Release${platform_suffix}/$scheme.swiftmodule"
   if [[ ! -d "$source_module_path" ]]; then
     return
   fi

You can send follow-ups to the cloud agent here.

Comment thread build.sh Outdated
Comment thread scripts/smoke_binary_integration_ios.rb Fixed
Comment thread scripts/validate_xcframework_distribution.rb Fixed
@cameroncooke cameroncooke changed the title build: Vend complete SnapshotPreviews XCFramework set build: Add XCFramework binary distribution support Jun 3, 2026
Comment thread scripts/build_xcframework_distribution.rb Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Demo accessibility snapshots render incorrectly
    • Restored setupA11y() override with AccessibilitySnapshotCore to enable accessibility rendering for previews using .snapshotAccessibility(true).
  • ✅ Fixed: Unused CGSize helper remains
    • Removed the unused CGSize.requiresCoreAnimationSnapshot extension that was left behind after the setupA11y implementation was deleted.

Create PR

Or push these changes by commenting:

@cursor push da1dd06254
Preview (da1dd06254)
diff --git a/Examples/DemoApp/DemoAppTests/DemoAppSnapshotTest.swift b/Examples/DemoApp/DemoAppTests/DemoAppSnapshotTest.swift
--- a/Examples/DemoApp/DemoAppTests/DemoAppSnapshotTest.swift
+++ b/Examples/DemoApp/DemoAppTests/DemoAppSnapshotTest.swift
@@ -10,6 +10,7 @@
 import UIKit
 #endif
 import SnapshottingTests
+import AccessibilitySnapshotCore
 
 class DemoAppSnapshotTest: SnapshotTest {
   override class func snapshotPreviews() -> [String]? {
@@ -27,10 +28,31 @@
   override class func excludedSnapshotPreviewModules() -> [String]? {
     return nil
   }
-}
+  
+  #if canImport(UIKit) && !os(watchOS) && !os(visionOS) && !os(tvOS)
+  override open class func setupA11y() -> ((UIViewController, UIWindow, PreviewLayout) -> UIView)? {
+    return { (controller: UIViewController, window: UIWindow, layout: PreviewLayout) in
+      let containerVC = controller.parent
+      let containedView: UIView
+      switch layout {
+      case .device:
+        containedView = containerVC?.view ?? controller.view
+      default:
+        containedView = controller.view
+      }
+      let a11yView = AccessibilitySnapshotView(
+        containedView: containedView,
+        viewRenderingMode: .drawHierarchyInRect,
+        activationPointDisplayMode: .never,
+        showUserInputLabels: true)
+    
+      a11yView.center = window.center
+      window.addSubview(a11yView)
 
-extension CGSize {
-  var requiresCoreAnimationSnapshot: Bool {
-    height >= UIScreen.main.bounds.size.height * 2
+      _ = try? a11yView.parseAccessibility(useMonochromeSnapshot: false)
+      a11yView.sizeToFit()
+      return a11yView
+    }
   }
+  #endif
 }

You can send follow-ups to the cloud agent here.

Comment thread Examples/DemoApp/DemoAppTests/DemoAppSnapshotTest.swift
Comment thread Examples/DemoApp/DemoAppTests/DemoAppSnapshotTest.swift
@cameroncooke cameroncooke force-pushed the cameroncooke/eme-1168-investigate-open-table-snapshotpreviews-issues branch from f793a7b to 2d6c5a9 Compare June 3, 2026 14:49
@cameroncooke
Copy link
Copy Markdown
Contributor Author

Follow-up on the earlier Bugbot summary about macOS Swift module paths: that issue is already addressed in the current branch. The distribution build uses the macOS Release build-products path and validation checks macOS versioned framework module placement. No additional code change was needed for that stale summary.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Watch UI tests missing Snapshotting
    • Added Snapshotting.xcframework to DemoWatchTests target's Frameworks and Embed Frameworks phases to enable AccessibilityPreviewTest dylib discovery.
  • ✅ Fixed: DemoModule embeds duplicate dylibs
    • Removed SnapshotPreferences and SnapshotSharedModels from DemoModule's Embed Frameworks phase to prevent duplicate dylib loading since DemoApp already embeds them.

Create PR

Or push these changes by commenting:

@cursor push f6a202f395
Preview (f6a202f395)
diff --git a/Examples/DemoApp-XCFrameworks/DemoApp-XCFrameworks.xcodeproj/project.pbxproj b/Examples/DemoApp-XCFrameworks/DemoApp-XCFrameworks.xcodeproj/project.pbxproj
--- a/Examples/DemoApp-XCFrameworks/DemoApp-XCFrameworks.xcodeproj/project.pbxproj
+++ b/Examples/DemoApp-XCFrameworks/DemoApp-XCFrameworks.xcodeproj/project.pbxproj
@@ -16,7 +16,6 @@
 		8B2B49222FD01DAA003221F3 /* SnapshottingTests.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49172FD01D81003221F3 /* SnapshottingTests.xcframework */; };
 		8B2B49232FD01DAA003221F3 /* SnapshottingTests.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49172FD01D81003221F3 /* SnapshottingTests.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		8B2B49252FD01DB3003221F3 /* SnapshotPreferences.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49132FD01D81003221F3 /* SnapshotPreferences.xcframework */; };
-		8B2B49262FD01DB3003221F3 /* SnapshotPreferences.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49132FD01D81003221F3 /* SnapshotPreferences.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		8B2B49282FD01DBD003221F3 /* PreviewGallery.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49122FD01D81003221F3 /* PreviewGallery.xcframework */; };
 		8B2B49292FD01DBD003221F3 /* PreviewGallery.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49122FD01D81003221F3 /* PreviewGallery.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		8B2B492A2FD01DC1003221F3 /* SnapshotPreferences.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49132FD01D81003221F3 /* SnapshotPreferences.xcframework */; };
@@ -27,8 +26,9 @@
 		8B2B492F2FD01E23003221F3 /* SnapshotSharedModels.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49152FD01D81003221F3 /* SnapshotSharedModels.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		8B2B49312FD01E8B003221F3 /* PreviewsSupport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49302FD01E8B003221F3 /* PreviewsSupport.xcframework */; };
 		8B2B494E2FD09E01003221F3 /* PreviewsSupport.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49302FD01E8B003221F3 /* PreviewsSupport.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		8B2B494F2FD01D88003221F3 /* Snapshotting.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49162FD01D81003221F3 /* Snapshotting.xcframework */; };
+		8B2B49502FD01D88003221F3 /* Snapshotting.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49162FD01D81003221F3 /* Snapshotting.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		8B2B49322FD01EC1003221F3 /* SnapshotSharedModels.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49152FD01D81003221F3 /* SnapshotSharedModels.xcframework */; };
-		8B2B49332FD01EC1003221F3 /* SnapshotSharedModels.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49152FD01D81003221F3 /* SnapshotSharedModels.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		8B2B49342FD01EDD003221F3 /* PreviewsSupport.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49302FD01E8B003221F3 /* PreviewsSupport.xcframework */; };
 		8B2B49352FD01EDD003221F3 /* PreviewsSupport.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49302FD01E8B003221F3 /* PreviewsSupport.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		8B2B49362FD01EDD003221F3 /* SnapshotPreferences.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2B49132FD01D81003221F3 /* SnapshotPreferences.xcframework */; };
@@ -184,18 +184,16 @@
 			name = "Embed Frameworks";
 			runOnlyForDeploymentPostprocessing = 0;
 		};
-		8B2B49272FD01DB3003221F3 /* Embed Frameworks */ = {
-			isa = PBXCopyFilesBuildPhase;
-			buildActionMask = 2147483647;
-			dstPath = "";
-			dstSubfolderSpec = 10;
-			files = (
-				8B2B49332FD01EC1003221F3 /* SnapshotSharedModels.xcframework in Embed Frameworks */,
-				8B2B49262FD01DB3003221F3 /* SnapshotPreferences.xcframework in Embed Frameworks */,
-			);
-			name = "Embed Frameworks";
-			runOnlyForDeploymentPostprocessing = 0;
-		};
+	8B2B49272FD01DB3003221F3 /* Embed Frameworks */ = {
+		isa = PBXCopyFilesBuildPhase;
+		buildActionMask = 2147483647;
+		dstPath = "";
+		dstSubfolderSpec = 10;
+		files = (
+		);
+		name = "Embed Frameworks";
+		runOnlyForDeploymentPostprocessing = 0;
+	};
 		FA1671E22A5399F700A42DB0 /* Embed Frameworks */ = {
 			isa = PBXCopyFilesBuildPhase;
 			buildActionMask = 2147483647;
@@ -223,20 +221,21 @@
 			name = "Embed Watch Content";
 			runOnlyForDeploymentPostprocessing = 0;
 		};
-		FA309EE42C38D7A900C85FF4 /* Embed Frameworks */ = {
-			isa = PBXCopyFilesBuildPhase;
-			buildActionMask = 2147483647;
-			dstPath = "";
-			dstSubfolderSpec = 10;
-			files = (
-				8B2B491B2FD01D8B003221F3 /* SnapshottingTests.xcframework in Embed Frameworks */,
-				8B2B493D2FD01F1B003221F3 /* PreviewsSupport.xcframework in Embed Frameworks */,
-				8B2B493F2FD01F1B003221F3 /* SnapshotPreviewsCore.xcframework in Embed Frameworks */,
-				8B2B49412FD01F1B003221F3 /* SnapshotSharedModels.xcframework in Embed Frameworks */,
-			);
-			name = "Embed Frameworks";
-			runOnlyForDeploymentPostprocessing = 0;
-		};
+	FA309EE42C38D7A900C85FF4 /* Embed Frameworks */ = {
+		isa = PBXCopyFilesBuildPhase;
+		buildActionMask = 2147483647;
+		dstPath = "";
+		dstSubfolderSpec = 10;
+		files = (
+			8B2B49502FD01D88003221F3 /* Snapshotting.xcframework in Embed Frameworks */,
+			8B2B491B2FD01D8B003221F3 /* SnapshottingTests.xcframework in Embed Frameworks */,
+			8B2B493D2FD01F1B003221F3 /* PreviewsSupport.xcframework in Embed Frameworks */,
+			8B2B493F2FD01F1B003221F3 /* SnapshotPreviewsCore.xcframework in Embed Frameworks */,
+			8B2B49412FD01F1B003221F3 /* SnapshotSharedModels.xcframework in Embed Frameworks */,
+		);
+		name = "Embed Frameworks";
+		runOnlyForDeploymentPostprocessing = 0;
+	};
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
@@ -336,17 +335,18 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
-		FA309ED32C38D77C00C85FF4 /* Frameworks */ = {
-			isa = PBXFrameworksBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				8B2B491A2FD01D8B003221F3 /* SnapshottingTests.xcframework in Frameworks */,
-				8B2B493C2FD01F1B003221F3 /* PreviewsSupport.xcframework in Frameworks */,
-				8B2B493E2FD01F1B003221F3 /* SnapshotPreviewsCore.xcframework in Frameworks */,
-				8B2B49402FD01F1B003221F3 /* SnapshotSharedModels.xcframework in Frameworks */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
+	FA309ED32C38D77C00C85FF4 /* Frameworks */ = {
+		isa = PBXFrameworksBuildPhase;
+		buildActionMask = 2147483647;
+		files = (
+			8B2B494F2FD01D88003221F3 /* Snapshotting.xcframework in Frameworks */,
+			8B2B491A2FD01D8B003221F3 /* SnapshottingTests.xcframework in Frameworks */,
+			8B2B493C2FD01F1B003221F3 /* PreviewsSupport.xcframework in Frameworks */,
+			8B2B493E2FD01F1B003221F3 /* SnapshotPreviewsCore.xcframework in Frameworks */,
+			8B2B49402FD01F1B003221F3 /* SnapshotSharedModels.xcframework in Frameworks */,
+		);
+		runOnlyForDeploymentPostprocessing = 0;
+	};
 		FA8F8B782C66C4C4007CEA33 /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 2d6c5a9. Configure here.

Comment thread Examples/DemoApp-XCFrameworks/DemoApp-XCFrameworks.xcodeproj/project.pbxproj Outdated
Comment thread scripts/validate_xcframework_distribution.rb
Comment thread scripts/build_xcframework_distribution.rb
@cameroncooke cameroncooke force-pushed the cameroncooke/eme-1168-investigate-open-table-snapshotpreviews-issues branch 2 times, most recently from ffffd9b to 2cd49ae Compare June 3, 2026 17:10
Comment thread scripts/build_xcframework_distribution.rb
@cameroncooke cameroncooke force-pushed the cameroncooke/eme-1168-investigate-open-table-snapshotpreviews-issues branch from 2cd49ae to d0e17e5 Compare June 3, 2026 17:19
Comment thread scripts/xcframework_distribution.rb
Comment thread scripts/build_xcframework_distribution.rb
@cameroncooke cameroncooke force-pushed the cameroncooke/eme-1168-investigate-open-table-snapshotpreviews-issues branch from d0e17e5 to d49e585 Compare June 3, 2026 17:35
Comment thread scripts/validate_xcframework_distribution.rb
Add a reproducible XCFramework distribution flow that builds the documented SnapshotPreviews binary framework set into XCFrameworks/ and validates the resulting dynamic dependency graph, Swift interface imports, macOS module layout, and PreviewsSupport packaging.

Add a binary-only iOS smoke test that builds a temporary consumer app/test project, manually links the generated frameworks, and verifies snapshot export. Keep the normal Swift Package integration separate from the binary distribution path by generating temporary dynamic-package manifests only for framework vending.

Add a separate DemoApp-XCFrameworks example that shares the existing DemoApp sources while leaving the standard DemoApp project as the local Swift Package example.

Refs EME-1168
Co-Authored-By: Codex <noreply@openai.com>
@cameroncooke cameroncooke force-pushed the cameroncooke/eme-1168-investigate-open-table-snapshotpreviews-issues branch from d49e585 to 39ac598 Compare June 3, 2026 18:44
@cameroncooke
Copy link
Copy Markdown
Contributor Author

Closing this PR in favor of a clean replacement branch with the same final change represented as one commit.

@cameroncooke cameroncooke deleted the cameroncooke/eme-1168-investigate-open-table-snapshotpreviews-issues branch June 4, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants