Allow library podspec to declare Swift Package Manager dependencies - #44627
Allow library podspec to declare Swift Package Manager dependencies#44627mfazekas wants to merge 3 commits into
Conversation
|
Hi @mfazekas! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
cipolleschi
left a comment
There was a problem hiding this comment.
Thanks for the amazing PR. 👏
I left some comments in the code to simplify it a little bit and to make it easier to use.
I also have a couple of questions:
1.) only works
USE_FRAMEWORKS=dynamic pod install
Does it works with static frameworks?
2.) .xcworkspace needs to be reopened after pod install - this could be worked around by not removing/readding spm dependencies
Do you think there is a way to detect whether dependencies changed so that we can emit a red message to prompt the user to close and reopen the .xcworkspace project?
Alternatively, we can kill Xcode, but if a person has multiple project open, that could be annoying.
| def self.spm_dependency(s, url:, requirement:, products:) | ||
| SPM.dependency(s, url: url, requirement: requirement, products: products) | ||
| end |
There was a problem hiding this comment.
To simplify the usage for 3rd party dependencies, I'd properly move this function from the utils.rb directly to the react_native_pods.
In this way, instead of the check:
if const_defined?(:ReactNativePodsUtils) && ReactNativePodsUtils.respond_to?(:spm_dependency)
ReactNativePodsUtils.spm_dependency(s,
url: 'https://github.com/apple/swift-atomics.git',
requirement: {kind: 'upToNextMajorVersion', minimumVersion: '1.1.0'},
products: ['Atomics']
)
else
raise "Please upgrade React Native to >=0.75.0 to use SPM dependencies."
end
they can just:
if defined?(:spm_dependency)
spm_dependency(s,
url: 'https://github.com/apple/swift-atomics.git',
requirement: {kind: 'upToNextMajorVersion', minimumVersion: '1.1.0'},
products: ['Atomics']
)
else
raise "Please upgrade React Native to >=0.75.0 to use SPM dependencies."
end
I would also consider backporting this to all the RN versions in the supported window (right now: 0.72, 0.73, 0.74) so that they don't even need to if-else.
What do you think?
There was a problem hiding this comment.
Added as spm_dependency.
As noted #44627 (comment) this approach does depend on what linking (static/dynamic) the RN framework uses and what linking does the SPM product uses. This is something to consider with backporting, as it might require some further changes to be usefull in all scenarios
It's a bit complicated. I've tested with Alamofire Swift Package Manager package that has a dynamic and static product.
Now
According to testing in Xcode 15.4, if there is a single swift package manager dependency, the regenerating the project might loose the dependency in the UI. (Not 100% reproducible) spm-reload.movWe can write an osascript that reloads the project if it's open, but it's a bit hacky #!/usr/bin/osascript -l JavaScript
function main() {
const app = Application("Xcode")
const active = app.activeWorkspaceDocument();
console.log("Active", app.activeWorkspaceDocument().file());
const worspacePath = "/Users/boga/Work/OSS/react-native-sim/rn-spm-rfc-poc/example/ios/RnSpmRfcPocExample.xcworkspace"
if (app.activeWorkspaceDocument().file() == filePath) {
app.activeWorkspaceDocument().close();
app.open(filePath);
}
}
main() |
… code update based on review
|
Sorry for the late reply. I'm still catching up after the conferences of the past weeks. One thing we can add is a big warning message that runs only if SPM dependencies are found.
What do you think? |
I've tested this with new Xcode 16.0 beta and close/reopen issue is solved there. So likely some warning that
The SPM might be a simple Swift Macro and in that case it works with all linking. If it's a static library then static linking causes link errors with duplicate symbols. If it's a dyanamic library, then static linking only works if the app also links with the dynamic library, otherwise they'll see undefined symbols. So not sure what's the good message here.
Maybe we also add a url to this issue, or a new one created just for explaining the potential issues with SPM regarding linking?
Yes given that the reload issue seems to get's solved by new Xcode version, it don't think it's worth to mess with osascript workaround, so message is good there. The linking part is a bit complicated, not sure what's a good message there. |
… show warning after install
|
@cipolleschi has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
|
@cipolleschi merged this pull request in f903f34. |
|
This pull request was successfully merged by @mfazekas in f903f34. When will my fix make it into a release? | How to file a pick request? |
…44627) Summary: React-Native uses Cocapods for native dependency management on iOS. While CocoaPods is flexible and popular, Apple's Swift Package Manager is the new standard. Currently consuming packages available only via Swift Package Manager is not possible. This change implements a single extension so .podspec files can declare Swift Package Manager dependencies via ```ruby ReactNativePodsUtils.spm_dependency(s, url: 'https://github.com/apple/swift-atomics.git', requirement: {kind: 'upToNextMajorVersion', minimumVersion: '1.1.0'}, products: ['Atomics'] ) ``` bypass-github-export-checks ## Changelog: [IOS] [ADDED] - libraries can now declare Swift Package Manager dependencies in their .podspec with `ReactNativePodsUtils.spm_dependency` Pull Request resolved: #44627 Test Plan: https://github.com/mfazekas/rn-spm-rfc-poc/ Is a simple demo for the feature: 1. Podspec declare dependency with: ```ruby if const_defined?(:ReactNativePodsUtils) && ReactNativePodsUtils.respond_to?(:spm_dependency) ReactNativePodsUtils.spm_dependency(s, url: 'https://github.com/apple/swift-atomics.git', requirement: {kind: 'upToNextMajorVersion', minimumVersion: '1.1.0'}, products: ['Atomics'] ) else raise "Please upgrade React Native to >=0.75.0 to use SPM dependencies." end ``` 2. [`import Atomics`](https://github.com/mfazekas/rn-spm-rfc-poc/blob/e4eb1034f7498dedee4cb673d327c34a6048bda2/ios/MultiplyInSwift.swift#L1C2-L1C15) and [`ManagedAtomic`](https://github.com/mfazekas/rn-spm-rfc-poc/blob/e4eb1034f7498dedee4cb673d327c34a6048bda2/ios/MultiplyInSwift.swift#L7-L13) is used in the code 3.) `spm_dependency` causes the dependency to be added via `post_install` hook in the workspace <img width="261" alt="image" src="https://github.com/facebook/react-native/assets/52435/ad6aee1c-ac88-4c84-8aa3-50e148c4f5b2"> 4.) `spm_dependecy` causes the library to be linked with `Atomics` library <img width="817" alt="image" src="https://github.com/facebook/react-native/assets/52435/bfc8dfc0-aeb7-4c75-acbd-937eab1cbf80"> Limitations: 1.) only works `USE_FRAMEWORKS=dynamic pod install` otherwise the linker fails [with known Xcode issue - duplicate link issue](https://forums.swift.org/t/objc-flag-causes-duplicate-symbols-with-swift-packages/27926) 2.) .xcworkspace needs to be reopened after `pod install` - this could be worked around by not removing/readding spm dependencies ### See also: react-native-community/discussions-and-proposals#587 (comment) react-native-community/discussions-and-proposals#787 Reviewed By: cortinico Differential Revision: D58947066 Pulled By: cipolleschi fbshipit-source-id: ae3bf955cd36a02cc78472595fa003cc9e843dd5
The iOS build introduced by 9da9063 (expo-mdoc-data-transfer, which declares two spm_dependency calls in its podspec for the EUDI eudi-lib-ios-iso18013-security and eudi-lib-ios-wallet-storage packages) failed pod install with: undefined method `package_product_dependencies' for nil:NilClass node_modules/react-native/scripts/cocoapods/spm.rb:80:in `block in add_spm_to_target' Root cause (confirmed by reading spm.rb's source and cross-referencing openwallet-foundation-labs/maplibre-react-native#1499, which hit the identical crash with the identical UUID prefix in its own logs): xcodeproj's project.new(klass) mints UUIDs from a counter prefixed by SHA256("Pods.xcodeproj") that resets between project generation and this post_install hook, so its first call here can collide with a UUID the PBXProject root object already owns — silently corrupting the in-memory Pods project. With two spm_dependency calls in the same podspec, that corruption from the first call makes project.targets.find return nil on the second, crashing exactly as seen. Already fixed upstream, just not in the react-native@0.81.5 this project pins — facebook/react-native PRs #57576 and #57602 (merged after 0.81.5) replace the implicit-counter UUID generation with an explicit one, checked against project.objects_by_uuid. Applied that same fix via patch-package rather than upgrading react-native (a much bigger, unrelated change) — patches/react-native+0.81.5.patch adds new_object_with_explicit_uuid and routes both project.new(pkg_class) and project.new(ref_class) call sites in spm.rb through it. Also installed expo-build-properties with ios.useFrameworks: "dynamic" — a separate, previously-missing requirement for any spm_dependency-based pod (see react/react-native#44627's own comment thread and expo-mdoc-data-transfer's install docs): without it, static linking of the Swift Package Manager products causes duplicate-symbol linker errors, independent of the UUID crash above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Run 32292108405 disproved the theory behind ff5340a, and the guard that plugin carried is what caught it. Rather than reporting success, the hook printed: [mdoc-spm-nodouble] No SPM products were linked in MdocDataTransfer's Frameworks phase; either CocoaPods/Xcode changed how spm_dependency links products, or the duplicate objects now come from somewhere else. The products were never in that build phase, so there was nothing to unlink and the 15,628 duplicates were untouched. Remove the plugin — it cannot work — and record what the log actually shows. The measurement that settles it. The pod's libtool line carries exactly 23 .o arguments and a -filelist, and the resulting archive holds the same 23 objects at member indices 12-34 and again at 35-57, every duplicate pair separated by exactly -23: libtool -static ... \ -filelist .../MdocDataTransfer.LinkFileList \ <- copy 1 .../Release-iphoneos/WalletStorage.o \ <- copy 2, same objects ... 22 more ... Apple engineers describe this exact mechanism in the Swift Forums thread that react/react-native#44627 links to: "Xcode is adding some object files to the static library twice - if you look at the build log when it runs libtool, the object file is in the file passed to -filelist, and then it's also passed again on the command line separately." No build setting governs those explicit arguments; Xcode's build system emits them between -filelist and -dependency_info. The archive alone would be harmless, since a normal link pulls only the members it needs. The trigger is -ObjC, which is present twice in the app's Ld line and forces the linker to load every member of every static archive - so both copies come in and every symbol collides. React Native itself warns about precisely this during pod install ("using swift package(s) ... with static linking, this might cause linker errors") and offers only USE_FRAMEWORKS=dynamic, which is approach 2 in the table and does not converge here. So the remaining failure is an upstream Xcode bug, not a misconfiguration. That is worth writing down rather than iterating on further: docs/ios-mdl-spm-linking.md records the mechanism, the evidence, all seven approaches tried with why each failed, four concrete ways forward, and the commands to re-derive the diagnosis from a fresh CI log. ios.buildStatic stays in app.json. It is the package's own supported option and it measurably reduced the damage - intra-archive collisions fell from roughly 7,800 to 2 - even though it does not remove the -filelist/explicit double-feed. The podspec remains pristine upstream, which is what keeps module resolution and build ordering working without any plugin of ours. Net effect across this series: pod install succeeds, every module the pod imports resolves, the full tree compiles, and the failure is now one well-understood upstream defect at the final link instead of four overlapping unknowns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Summary:
React-Native uses Cocapods for native dependency management on iOS. While CocoaPods is flexible and popular, Apple's Swift Package Manager is the new standard. Currently consuming packages available only via Swift Package Manager is not possible. This change implements a single extension so .podspec files can declare Swift Package Manager dependencies via
Changelog:
[IOS] [ADDED] - libraries can now declare Swift Package Manager dependencies in their .podspec with
spm_dependencyTest Plan:
https://github.com/mfazekas/rn-spm-rfc-poc/
Is a simple demo for the feature:
Podspec declare dependency with:
import AtomicsandManagedAtomicis used in the code3.)
spm_dependencycauses the dependency to be added viapost_installhook in the workspace4.)
spm_dependecycauses the library to be linked withAtomicslibraryLimitations: #
1.) Some package target types will work with only some USE_FRAMEWORKS settings:
1️⃣: more complex swift macros usually have runtime component - limitation might apply, as table above will apply
2️⃣: caused by a Xcode linking issue ObjC flag - duplicate link issue
3️⃣: can be fixed by linking the library to the app itself
2.) .xcworkspace needs to be reopened after
pod install- this seems to be an Xcode bug and fixed in Xcode 16 beta.See also:
react-native-community/discussions-and-proposals#587 (comment)
react-native-community/discussions-and-proposals#787