feat(shorebird_cli): generate podspec for iOS framework releases#3675
feat(shorebird_cli): generate podspec for iOS framework releases#3675eseidel wants to merge 4 commits into
Conversation
Generate a CocoaPods podspec alongside the existing xcframeworks in the release directory, enabling users to integrate via `pod install` instead of manually configuring Xcode framework search paths and embedding.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
bdero
left a comment
There was a problem hiding this comment.
bdero's review bot:
Overall
Pragmatic shape — wrapping existing xcframeworks in a podspec is the right "Approach A" given customer demand and zero changes to the build pipeline. Three things in the podspec template are hardcoded that probably shouldn't be (s.version, s.platform, s.license), and the test only spot-checks two lines so template-level breakage wouldn't be caught. None of this is blocking.
One reminder unrelated to a specific line: both manual checkboxes in the test plan (shorebird release ios-framework generates the podspec; pod install works in a real iOS app) are unchecked. The unit tests prove we write a podspec; only the manual run proves CocoaPods accepts it. Want this verified before merge — it's the entire customer-visible value of the PR.
| File(podspecPath).writeAsStringSync(''' | ||
| Pod::Spec.new do |s| | ||
| s.name = 'ShorebirdFlutter' | ||
| s.version = '0.0.1' |
There was a problem hiding this comment.
bdero's review bot:
💡 s.version = '0.0.1' is the same string for every release, so Podfile.lock and pod outdated will show stale info regardless of which Shorebird release the user is actually shipping. With :path => integration the version doesn't drive resolution, so nothing breaks — but plumbing through argResults['release-version'] (the same value getReleaseVersion returns) makes the lockfile actually reflect reality. One catch: Flutter's release-version can include +build suffixes, and CocoaPods' SemVer parser is stricter than pub's, so we may need to strip or transform.
| s.license = { :type => 'BSD-3-Clause' } | ||
| s.author = 'Shorebird' | ||
| s.source = { :path => '.' } | ||
| s.platform = :ios, '12.0' |
There was a problem hiding this comment.
bdero's review bot:
💡 s.platform = :ios, '12.0' is hardcoded, but the actual minimum is whatever MinimumOSVersion ends up baked into App.xcframework and ShorebirdFlutter.xcframework — driven by Flutter's minimum, which has been creeping up. CocoaPods doesn't cross-check podspec platform against framework Info.plist, so a future Flutter min-version bump would silently let users link Shorebird into iOS 12 apps where the binaries actually require 13+. Worth either reading from the xcframework's Info.plist or pulling from a Shorebird-side known-minimum constant.
| s.version = '0.0.1' | ||
| s.summary = 'Shorebird Flutter framework for add-to-app integration.' | ||
| s.homepage = 'https://shorebird.dev' | ||
| s.license = { :type => 'BSD-3-Clause' } |
There was a problem hiding this comment.
bdero's review bot:
💡 s.license = { :type => 'BSD-3-Clause' } — not sure this is accurate for the bundled binaries. The Flutter engine portion is BSD-3-Clause, but Shorebird's modifications and proprietary patching code ship in ShorebirdFlutter.xcframework too, and I think those are under different terms. Worth a check with someone who has more licensing context — at minimum, { :file => 'LICENSE' } pointing to a real bundled file is more honest than asserting one license type for the whole thing.
| test('generates podspec in release directory', () async { | ||
| await runWithOverrides(iosFrameworkReleaser.buildReleaseArtifacts); | ||
|
|
||
| final podspecFile = File( | ||
| p.join(projectRoot.path, 'release', 'ShorebirdFlutter.podspec'), | ||
| ); | ||
| expect(podspecFile.existsSync(), isTrue); | ||
| final content = podspecFile.readAsStringSync(); | ||
| expect(content, contains("s.name = 'ShorebirdFlutter'")); | ||
| expect( | ||
| content, | ||
| contains( | ||
| "s.vendored_frameworks = 'App.xcframework', " | ||
| "'ShorebirdFlutter.xcframework'", | ||
| ), | ||
| ); | ||
| }); |
There was a problem hiding this comment.
bdero's review bot:
💡 Test only asserts two contains checks. A heredoc typo (missing end, mismatched quotes) wouldn't be caught — pod install would discover it at runtime in production. A golden-file comparison against the full podspec content would lock down the template; it costs almost nothing and means future template edits show up clearly in diffs. Optional: shelling out to pod ipc spec would catch syntax errors but that adds a CI dependency we probably don't want.
| ${styleBold.wrap('Option A: CocoaPods')} | ||
| Add the following to your app's Podfile: | ||
| ${lightCyan.wrap("pod 'ShorebirdFlutter', :path => '$relativeFrameworkDirectoryPath'")} | ||
| Then run ${lightCyan.wrap('pod install')}. | ||
|
|
||
| Instructions for these steps can be found at https://docs.flutter.dev/add-to-app/ios/project-setup#option-b---embed-frameworks-in-xcode. | ||
| ${styleBold.wrap('Option B: Manual Xcode embedding')} | ||
| 1. Add the relative path to the ${lightCyan.wrap(relativeFrameworkDirectoryPath)} directory to your app's Framework Search Paths in your Xcode build settings. | ||
| 2. Embed the App.xcframework and ShorebirdFlutter.xcframework in your Xcode project. | ||
| Instructions: https://docs.flutter.dev/add-to-app/ios/project-setup#option-b---embed-frameworks-in-xcode |
There was a problem hiding this comment.
bdero's review bot:
🧹 If CocoaPods is the new recommended path, worth labeling it that way ("Option A: CocoaPods (recommended)") so users don't read Option A, glance at Option B, and feel like they need to pick. Otherwise both options read as equally weighted choices and we get more "which one should I use?" support questions.
| Pod::Spec.new do |s| | ||
| s.name = 'ShorebirdFlutter' | ||
| s.version = '0.0.1' | ||
| s.summary = 'Shorebird Flutter framework for add-to-app integration.' |
There was a problem hiding this comment.
bdero's review bot:
🧹 s.summary could mention that the framework is a Shorebird-patched Flutter engine, not stock Flutter — that's the whole point. "Shorebird's patched Flutter engine for add-to-app iOS integration" or similar. Minor.
bdero
left a comment
There was a problem hiding this comment.
This lgtm. Couple of things my review bot suggested that seem to make sense. Plz land at your discretion.
|
Also, IIRC, there isn't an issue we should close when this one lands, right? |
|
@eseidel Looks like this is ready to go pending some clanker reviews. |
|
It's ready to go, I'm just not sure if it solves a real customer problem or not. |
Summary
ShorebirdFlutter.podspecin therelease/directory alongside the existing xcframeworks duringshorebird release ios-frameworkThis enables users to integrate Shorebird's iOS framework add-to-app via CocoaPods (
pod 'ShorebirdFlutter', :path => 'release') instead of manually configuring Xcode framework search paths and embedding.Context: iOS add-to-app integration approaches
Flutter documents two main iOS add-to-app integration methods:
We have customer demand for CocoaPods-based integration. There are two possible approaches:
Approach A (this PR): Wrap existing xcframeworks in a podspec
The build process is unchanged — we still produce
App.xcframeworkandShorebirdFlutter.xcframework. We just additionally generate a podspec that wraps them so users can dopod installinstead of manual Xcode configuration.Pros:
shorebird release ios-frameworkas before)Cons:
shorebird release ios-frameworkseparately when Flutter code changespod install)Approach B (not in this PR): Hook into source-compilation CocoaPods flow
This would be the "real" Option A support where Flutter compiles from source as part of
pod install. This would require:This breaks Shorebird's current "no project modifications" advantage and is significantly more complex. Worth exploring only if Approach A doesn't satisfy customer demand.
Test plan
ios_framework_releaser_test.darttests passshorebird release ios-frameworkand verifyrelease/ShorebirdFlutter.podspecis generatedpod installworks in a native iOS app with the generated podspec