swift: make the built xcframework distributable to the App Store - #57
swift: make the built xcframework distributable to the App Store#57prakashrj wants to merge 1 commit into
Conversation
4846b7c to
cf49051
Compare
| done | ||
|
|
||
| # 4. A simulator slice is required for anyone building for the simulator; its | ||
| # absence only shows up as a confusing link error downstream. |
There was a problem hiding this comment.
This looks backwards. A simulator slice in a framework submitted to the appstore is grounds for rejection. The check is probably redundant anyway - there's (purposefully) a separate build target for simulator frameworks for this exact reason, but it's worth checking the opposite
There was a problem hiding this comment.
You are right that the check was not earning its place, and I have added the one you asked for.
The script now reads the build platform of every architecture in every slice and fails if a slice is not what its name claims:
OK: ios-arm64_x86_64-simulator is built for IOSSIMULATOR
OK: ios-arm64 is built for IOS
OK: macos-arm64 is built for MACOS
Verified it fails in the direction that matters, by copying the simulator binary over the device slice of a real bundle:
FAIL: ios-arm64 declares IOS but its binary is built for: IOSSIMULATOR
xcframework validation FAILED — this bundle would be rejected at upload
exit=1
The presence check is now a WARN rather than a failure. Its concern was an adopter who cannot build for the simulator, which is not a rejection cause, and this script's charter is rejection causes only. A device-only bundle is a legitimate output and should not fail validation.
On the underlying point, though, I think a simulator slice inside an xcframework is safe, and I would rather show the evidence than assert it — see my reply on the Makefile comment.
| mkdir -p ./build/Build/Products/Release-all | ||
| xcodebuild -create-xcframework \ | ||
| -framework ./build/Build/Products/Release-iphoneos/TailscaleKit.framework \ | ||
| -framework ./build/Build/Products/Release-iphonesimulator/TailscaleKit.framework \ |
There was a problem hiding this comment.
Also incorrect here AFAIK. You cannot embed simulator binaries in appStore submissions - unless they've changed the rules
There was a problem hiding this comment.
I think this one is safe, and I have a shipped artifact to check it against rather than an opinion.
The distinction is that an xcframework is a container, not the submitted artifact. Xcode resolves it at build time and embeds only the slice matching the destination, so the simulator slice never reaches the archive. The rule you are remembering applied to fat frameworks, where lipo merged device and simulator architectures into a single binary and Xcode had no way to pick one — which is the problem xcframeworks were introduced to solve.
Measured, rather than argued. I ship an app built on exactly this three-slice bundle:
$ ls TailscaleKit.xcframework
Info.plist ios-arm64 ios-arm64_x86_64-simulator macos-arm64
The app archived from it embeds one slice:
$ vtool -show-build Payload/…app/Frameworks/TailscaleKit.framework/TailscaleKit
platform IOS
minos 18.1
$ lipo -info …/TailscaleKit
Non-fat file: … is architecture: arm64
No simulator architecture, and nothing for a stripping phase to remove. That build was uploaded to App Store Connect and finished processing as processingState=VALID — so it cleared Apple's upload validation, which is the gate that rejects simulator architectures. To be exact about what that does and does not show: it has not been through App Review yet, so it is evidence about the upload checks specifically, not about a human reviewer.
If you would still rather the distributable bundle be device + macOS only, I am happy to drop the simulator slice from this target — it costs adopters the ability to build for the simulator against the prebuilt bundle, which is the tradeoff ios-fat currently covers for development. Your call; I did not want to quietly leave it as-is after you had flagged it, but I also did not want to change it on a premise I could show was not load-bearing.
Either way, the mislabelling risk you pointed at is now checked explicitly in validate-xcframework.sh — see the other thread.
`make ios-fat` produces an xcframework that builds and links fine and is then rejected at upload. Apps embedding it fail App Store Connect validation with ITMS-91053 (Missing API declaration), because neither iOS slice carries a PrivacyInfo.xcprivacy. Nothing surfaces at build time; the first signal is a rejection email minutes after submission. TailscaleKit collects no data and does no tracking, but Apple requires the manifest to be present regardless. This adds: - swift/PrivacyInfo.xcprivacy — an all-empty manifest, which is the accurate declaration for this framework. - `make xcframework` — a 3-slice target (ios-arm64, ios-arm64_x86_64-simulator, macos-arm64) intended for distribution. ios-fat is unchanged in shape and still builds the 2-slice iOS bundle; it now injects the manifest too. The macOS slice matters because TailscaleKit builds for macOS today (`make macos`) but never made it into an xcframework, so adopters had to assemble one by hand — the usual result being symlink and code-signing problems that also fail at upload. - swift/script/validate-xcframework.sh — asserts the six things that fail at upload rather than at build. Run it before distributing. Two details that are easy to get wrong and are handled here: - macOS frameworks are versioned bundles. The manifest has to be in Versions/A/Resources so the Versions/A/_CodeSignature seal covers it. At the bundle root it is unsealed and upload fails with error 90238. The target re-signs after injecting so the seal is regenerated. - A stray Info.plist at the macOS versioned-bundle root duplicates Versions/A/Resources/Info.plist and triggers the same 90238; it is removed. The validator also checks that no vendor team identifier is embedded in the signature, which is #15802 — a prebuilt binary carrying Tailscale's team ID collides with the adopter's own signing identity. Verified on macOS 15 / Xcode 26.1.1: make xcframework -> 3 slices written ./script/validate-xcframework.sh -> exit 0, all checks pass (manifest removed from ios-arm64) -> exit 1, reports ITMS-91053 Context: tailscale/tailscale#15410 noted the iOS story needed cleanup, and tailscale/tailscale#13937 asked for first-class Swift support. This closes the remaining gap between "TailscaleKit builds" and "an app embedding TailscaleKit can ship". swift/README.md documented building but not shipping: it listed the make targets and stated the iOS framework "is suitable for app-store submissions", which is the claim this change disproves. It now says the iOS slice is the one you ship, points at `make xcframework` for distribution, and adds a Distribution section covering the four upload-time failures and how to check for them. Updates #20992
cf49051 to
3f46134
Compare
Problem
make ios-fatproduces an xcframework that builds and links fine — and is then rejected at upload. An app embedding it fails App Store Connect validation with:because neither iOS slice carries a
PrivacyInfo.xcprivacy. Nothing surfaces at build time; the first signal is a rejection email minutes after submission.TailscaleKit collects no data and does no tracking, but Apple requires the manifest to be present regardless.
Changes
swift/PrivacyInfo.xcprivacy— an all-empty manifest, which is the accurate declaration for this framework.make xcframework— a 3-slice target (ios-arm64,ios-arm64_x86_64-simulator,macos-arm64) intended for distribution.ios-fatkeeps its existing shape and still builds the 2-slice iOS bundle; it now injects the manifest too.The macOS slice matters because TailscaleKit already builds for macOS (
make macos) but never made it into an xcframework, so adopters wanting both platforms had to assemble one by hand — usually hitting symlink and code-signing problems that also fail at upload.swift/script/validate-xcframework.sh— asserts the things that fail at upload rather than at build. Run before distributing. Only rejection causes fail the script; anything that merely makes the bundle less useful to an adopter is reported asWARN.Among the hard checks: every architecture in every slice must be built for the platform the slice name claims (
ios-arm64→IOS,ios-arm64_x86_64-simulator→IOSSIMULATOR,macos-arm64→MACOS). A simulator slice inside an xcframework is safe, because Xcode embeds only the slice matching the destination — but a slice mislabelled at assembly time would ship simulator code, and that is worth catching.Two details that are easy to get wrong
Versions/A/Resourcesso theVersions/A/_CodeSignatureseal covers it. At the bundle root it is unsealed and upload fails with error 90238. The target re-signs after injecting so the seal is regenerated.Info.plistat the macOS versioned-bundle root duplicatesVersions/A/Resources/Info.plistand triggers the same 90238. It's removed.The validator additionally checks that no vendor team identifier is embedded in the signature — that's #15802, where a prebuilt binary carrying Tailscale's team ID collides with the adopter's own signing identity.
Verification
macOS 15 / Xcode 26.1.1:
make xcframework./script/validate-xcframework.shios-arm64ios-arm64sliceios-arm64 declares IOS but its binary is built for: IOSSIMULATORThe negative case is included deliberately — a validator that only ever passes isn't evidence of anything.
Context
tailscale/tailscale#15410 noted the iOS path needed cleanup, and #13937 asked for first-class Swift support. This closes the remaining gap between "TailscaleKit builds" and "an app embedding TailscaleKit can ship".
For background, I've been running TailscaleKit in userspace (tsnet, no
NEPacketTunnelProvider) on a physical iPhone and on macOS; these are the three things that stood between a working build and a shippable one. Happy to split this into separate PRs if you'd prefer the manifest and the macOS slice reviewed independently.Documentation
swift/README.mddocumented building but not shipping. It listed the make targets and stated the iOS framework "is suitable for app-store submissions" — the claim this PR disproves. It now says the iOS slice is the one you ship, points atmake xcframeworkfor distribution, and adds a Distribution section covering the four upload-time failures and the command to check for them.Tracking issue: tailscale/tailscale#20992 (filed there because issues are disabled on this repo).