From 2e06efbf5dfffd054e05fee8dad8068abcf2caa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Tue, 25 Jun 2024 03:45:07 -0700 Subject: [PATCH] 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 image 4.) `spm_dependecy` causes the library to be linked with `Atomics` library image 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 --- .../react-native/scripts/cocoapods/spm.rb | 94 +++++++++++++++++++ .../react-native/scripts/react_native_pods.rb | 14 +++ 2 files changed, 108 insertions(+) create mode 100644 packages/react-native/scripts/cocoapods/spm.rb diff --git a/packages/react-native/scripts/cocoapods/spm.rb b/packages/react-native/scripts/cocoapods/spm.rb new file mode 100644 index 00000000000..78ca3c31e08 --- /dev/null +++ b/packages/react-native/scripts/cocoapods/spm.rb @@ -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 diff --git a/packages/react-native/scripts/react_native_pods.rb b/packages/react-native/scripts/react_native_pods.rb index 7c158908c99..66e90e55599 100644 --- a/packages/react-native/scripts/react_native_pods.rb +++ b/packages/react-native/scripts/react_native_pods.rb @@ -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)