Allow library podspec to declare Swift Package Manager dependencies (#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: https://github.com/facebook/react-native/pull/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:

https://github.com/react-native-community/discussions-and-proposals/issues/587#issuecomment-2117025448
https://github.com/react-native-community/discussions-and-proposals/pull/787

Reviewed By: cortinico

Differential Revision: D58947066

Pulled By: cipolleschi

fbshipit-source-id: ae3bf955cd36a02cc78472595fa003cc9e843dd5
This commit is contained in:
Miklós Fazekas
2024-06-25 03:45:07 -07:00
committed by Blake Friedman
parent 4cec121a7b
commit 2e06efbf5d
2 changed files with 108 additions and 0 deletions
@@ -0,0 +1,94 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
class SPMManager
def initialize()
@dependencies_by_pod = {}
end
def dependency(pod_spec, url:, requirement:, products:)
@dependencies_by_pod[pod_spec.name] ||= []
@dependencies_by_pod[pod_spec.name] << { url: url, requirement: requirement, products: products}
end
def apply_on_post_install(installer)
project = installer.pods_project
log 'Cleaning old SPM dependencies from Pods project'
clean_spm_dependencies_from_target(project, @dependencies_by_pod)
log 'Adding SPM dependencies to Pods project'
@dependencies_by_pod.each do |pod_name, dependencies|
dependencies.each do |spm_spec|
log "Adding SPM dependency on product #{spm_spec[:products]}"
add_spm_to_target(
project,
project.targets.find { |t| t.name == pod_name},
spm_spec[:url],
spm_spec[:requirement],
spm_spec[:products]
)
log " Adding workaround for Swift package not found issue"
target = project.targets.find { |t| t.name == pod_name}
target.build_configurations.each do |config|
target.build_settings(config.name)['SWIFT_INCLUDE_PATHS'] ||= ['$(inherited)']
search_path = '${SYMROOT}/${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}/'
unless target.build_settings(config.name)['SWIFT_INCLUDE_PATHS'].include?(search_path)
target.build_settings(config.name)['SWIFT_INCLUDE_PATHS'].push(search_path)
end
end
end
end
unless @dependencies_by_pod.empty?
log_warning "If you're using Xcode 15 or earlier you might need to close and reopen the Xcode workspace"
unless ENV["USE_FRAMEWORKS"] == "dynamic"
@dependencies_by_pod.each do |pod_name, dependencies|
log_warning "Pod #{pod_name} is using swift package(s) #{dependencies.map{|i| i[:products]}.flatten.uniq.join(", ")} with static linking, this might cause linker errors. Consider using USE_FRAMEOWRKS=dynamic, see https://github.com/facebook/react-native/pull/44627#issuecomment-2123119711 for more information"
end
end
end
end
private
def log(msg)
::Pod::UI.puts "[SPM] #{msg}"
end
def log_warning(msg)
::Pod::UI.puts "\n\n[SPM] WARNING!!! #{msg}\n\n"
end
def clean_spm_dependencies_from_target(project, new_targets)
project.root_object.package_references.delete_if { |pkg| (pkg.class == Xcodeproj::Project::Object::XCRemoteSwiftPackageReference) }
end
def add_spm_to_target(project, target, url, requirement, products)
pkg_class = Xcodeproj::Project::Object::XCRemoteSwiftPackageReference
ref_class = Xcodeproj::Project::Object::XCSwiftPackageProductDependency
pkg = project.root_object.package_references.find { |p| p.class == pkg_class && p.repositoryURL == url }
if !pkg
pkg = project.new(pkg_class)
pkg.repositoryURL = url
pkg.requirement = requirement
log(" Adding package to workspace: #{pkg.inspect}")
project.root_object.package_references << pkg
end
products.each do |product_name|
ref = target.package_product_dependencies.find do |r|
r.class == ref_class && r.package == pkg && r.product_name == product_name
end
next if ref
log(" Adding product dependency #{product_name} to #{target.name}")
ref = project.new(ref_class)
ref.package = pkg
ref.product_name = product_name
target.package_product_dependencies << ref
end
end
end
SPM = SPMManager.new
@@ -17,6 +17,7 @@ require_relative './cocoapods/local_podspec_patch.rb'
require_relative './cocoapods/runtime.rb'
require_relative './cocoapods/helpers.rb'
require_relative './cocoapods/privacy_manifest_utils.rb'
require_relative './cocoapods/spm.rb'
# Importing to expose use_native_modules!
require_relative './cocoapods/autolinking.rb'
@@ -242,6 +243,18 @@ def install_modules_dependencies(spec, new_arch_enabled: NewArchitectureHelper.n
NewArchitectureHelper.install_modules_dependencies(spec, new_arch_enabled, folly_config[:version])
end
# This function can be used by library developer to declare a SwiftPackageManager dependency.
#
# Parameters:
# - spec: The spec the Swift Package Manager dependency has to be added to
# - url: The URL of the Swift Package Manager dependency
# - requirement: The version requirement of the Swift Package Manager dependency (eg. ` {kind: 'upToNextMajorVersion', minimumVersion: '5.9.1'},`)
# - products: The product/target of the Swift Package Manager dependency (eg. AlamofireDynamic)
def spm_dependency(spec, url:, requirement:, products:)
SPM.dependency(spec, url: url, requirement: requirement, products: products)
end
# It returns the default flags.
# deprecated.
def get_default_flags()
@@ -297,6 +310,7 @@ def react_native_post_install(
ReactNativePodsUtils.updateOSDeploymentTarget(installer)
ReactNativePodsUtils.set_dynamic_frameworks_flags(installer)
ReactNativePodsUtils.add_ndebug_flag_to_pods_in_release(installer)
SPM.apply_on_post_install(installer)
if privacy_file_aggregation_enabled
PrivacyManifestUtils.add_aggregated_privacy_manifest(installer)