Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 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 XcodebuildMock < Xcodebuild
@@version = ""
@@version_invocation_count = 0

def self.set_version=(v)
@@version = v
end

def self.version
@@version_invocation_count += 1
@@version
end

def self.version_invocation_count
@@version_invocation_count
end

def self.reset()
@@version_invocation_count = 0
end
end
131 changes: 114 additions & 17 deletions packages/react-native/scripts/cocoapods/__tests__/utils-test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
require_relative "./test_utils/PathnameMock.rb"
require_relative "./test_utils/TargetDefinitionMock.rb"
require_relative "./test_utils/XcodeprojMock.rb"
require_relative "./test_utils/XcodebuildMock.rb"

class UtilsTests < Test::Unit::TestCase
def setup
Expand All @@ -30,6 +31,7 @@ def teardown
SysctlChecker.reset()
Environment.reset()
Xcodeproj::Plist.reset()
XcodebuildMock.reset()
ENV['RCT_NEW_ARCH_ENABLED'] = '0'
ENV['USE_HERMES'] = '1'
ENV['USE_FRAMEWORKS'] = nil
Expand Down Expand Up @@ -526,9 +528,56 @@ def test_applyMacCatalystPatches_correctlyAppliesNecessaryPatches
# ================================= #
# Test - Apply Xcode 15 Patch #
# ================================= #
def test_applyXcode15Patch_whenXcodebuild14_correctlyAppliesNecessaryPatch
# Arrange
XcodebuildMock.set_version = "Xcode 14.3"
first_target = prepare_target("FirstTarget")
second_target = prepare_target("SecondTarget")
third_target = TargetMock.new("ThirdTarget", [
BuildConfigurationMock.new("Debug", {
"GCC_PREPROCESSOR_DEFINITIONS" => '$(inherited) "SomeFlag=1" '
}),
BuildConfigurationMock.new("Release", {
"GCC_PREPROCESSOR_DEFINITIONS" => '$(inherited) "SomeFlag=1" '
}),
], nil)

def test_applyXcode15Patch_correctlyAppliesNecessaryPatch
user_project_mock = UserProjectMock.new("/a/path", [
prepare_config("Debug"),
prepare_config("Release"),
],
:native_targets => [
first_target,
second_target
]
)
pods_projects_mock = PodsProjectMock.new([], {"hermes-engine" => {}}, :native_targets => [
third_target
])
installer = InstallerMock.new(pods_projects_mock, [
AggregatedProjectMock.new(user_project_mock)
])

# Act
user_project_mock.build_configurations.each do |config|
assert_nil(config.build_settings["OTHER_LDFLAGS"])
end

ReactNativePodsUtils.apply_xcode_15_patch(installer, :xcodebuild_manager => XcodebuildMock)

# Assert
user_project_mock.build_configurations.each do |config|
assert_equal("$(inherited) _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION", config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"])
assert_equal("$(inherited) ", config.build_settings["OTHER_LDFLAGS"])
end

# User project and Pods project
assert_equal(2, XcodebuildMock.version_invocation_count)
end

def test_applyXcode15Patch_whenXcodebuild15_correctlyAppliesNecessaryPatch
# Arrange
XcodebuildMock.set_version = "Xcode 15.0"
first_target = prepare_target("FirstTarget")
second_target = prepare_target("SecondTarget")
third_target = TargetMock.new("ThirdTarget", [
Expand Down Expand Up @@ -557,24 +606,70 @@ def test_applyXcode15Patch_correctlyAppliesNecessaryPatch
])

# Act
ReactNativePodsUtils.apply_xcode_15_patch(installer)
user_project_mock.build_configurations.each do |config|
assert_nil(config.build_settings["OTHER_LDFLAGS"])
end

ReactNativePodsUtils.apply_xcode_15_patch(installer, :xcodebuild_manager => XcodebuildMock)

# Assert
first_target.build_configurations.each do |config|
assert_equal(config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"].strip,
'$(inherited) "_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION"'
)
user_project_mock.build_configurations.each do |config|
assert_equal("$(inherited) _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION", config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"])
assert_equal("$(inherited) -Wl -ld_classic ", config.build_settings["OTHER_LDFLAGS"])
end
second_target.build_configurations.each do |config|
assert_equal(config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"].strip,
'$(inherited) "_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION"'
)

# User project and Pods project
assert_equal(2, XcodebuildMock.version_invocation_count)
end

def test_applyXcode15Patch_whenXcodebuild14ButProjectHasSettings_correctlyRemovesNecessaryPatch
# Arrange
XcodebuildMock.set_version = "Xcode 14.3"
first_target = prepare_target("FirstTarget")
second_target = prepare_target("SecondTarget")
third_target = TargetMock.new("ThirdTarget", [
BuildConfigurationMock.new("Debug", {
"GCC_PREPROCESSOR_DEFINITIONS" => '$(inherited) "SomeFlag=1" '
}),
BuildConfigurationMock.new("Release", {
"GCC_PREPROCESSOR_DEFINITIONS" => '$(inherited) "SomeFlag=1" '
}),
], nil)

debug_config = prepare_config("Debug", {"OTHER_LDFLAGS" => "$(inherited) -Wl -ld_classic "})
release_config = prepare_config("Release", {"OTHER_LDFLAGS" => "$(inherited) -Wl -ld_classic "})

user_project_mock = UserProjectMock.new("/a/path", [
debug_config,
release_config,
],
:native_targets => [
first_target,
second_target
]
)
pods_projects_mock = PodsProjectMock.new([debug_config.clone, release_config.clone], {"hermes-engine" => {}}, :native_targets => [
third_target
])
installer = InstallerMock.new(pods_projects_mock, [
AggregatedProjectMock.new(user_project_mock)
])

# Act
user_project_mock.build_configurations.each do |config|
assert_equal("$(inherited) -Wl -ld_classic ", config.build_settings["OTHER_LDFLAGS"])
end
third_target.build_configurations.each do |config|
assert_equal(config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"].strip,
'$(inherited) "SomeFlag=1" "_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION"'
)

ReactNativePodsUtils.apply_xcode_15_patch(installer, :xcodebuild_manager => XcodebuildMock)

# Assert
user_project_mock.build_configurations.each do |config|
assert_equal("$(inherited) _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION", config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"])
assert_equal("$(inherited) ", config.build_settings["OTHER_LDFLAGS"])
end

# User project and Pods project
assert_equal(2, XcodebuildMock.version_invocation_count)
end

# ==================================== #
Expand Down Expand Up @@ -923,12 +1018,14 @@ def prepare_user_project_mock_with_plists
])
end

def prepare_config(config_name)
return BuildConfigurationMock.new(config_name, {"LIBRARY_SEARCH_PATHS" => [
def prepare_config(config_name, extra_config = {})
config = {"LIBRARY_SEARCH_PATHS" => [
"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)",
"\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
"another/path",
]})
]}.merge(extra_config)

return BuildConfigurationMock.new(config_name, config)
end

def prepare_target(name, product_type = nil, dependencies = [])
Expand Down
16 changes: 16 additions & 0 deletions packages/react-native/scripts/cocoapods/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ def call_sysctl_arm64
end
end

# Helper class that is used to easily send commands to Xcodebuild
# And that can be subclassed for testing purposes.
class Xcodebuild
def self.version
`xcodebuild -version`
end
end

# Helper object to wrap system properties like RUBY_PLATFORM
# This makes it easier to mock the behaviour in tests
class Environment
Expand All @@ -26,3 +34,11 @@ def self.find_codegen_file(path)
return `find #{path} -type f \\( #{js_files} -or #{ts_files} \\)`.split("\n").sort()
end
end

module Helpers
class Constants
def self.min_ios_version_supported
return '13.4'
end
end
end
105 changes: 97 additions & 8 deletions packages/react-native/scripts/cocoapods/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,16 +152,31 @@ def self.apply_mac_catalyst_patches(installer)
end
end

def self.apply_xcode_15_patch(installer)
installer.target_installation_results.pod_target_installation_results
.each do |pod_name, target_installation_result|
target_installation_result.native_target.build_configurations.each do |config|
# unary_function and binary_function are no longer provided in c++20 and newer standard modes as part of Xcode 15. They can be re-enabled with setting _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION
# Ref: https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes#Deprecations
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= '$(inherited) '
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '"_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION" '
def self.apply_xcode_15_patch(installer, xcodebuild_manager: Xcodebuild)
projects = self.extract_projects(installer)

gcc_preprocessor_definition_key = 'GCC_PREPROCESSOR_DEFINITIONS'
other_ld_flags_key = 'OTHER_LDFLAGS'
libcpp_cxx17_fix = '_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION'
xcode15_compatibility_flags = '-Wl -ld_classic '

projects.each do |project|
project.build_configurations.each do |config|
# fix for unary_function and binary_function
self.safe_init(config, gcc_preprocessor_definition_key)
self.add_value_to_setting_if_missing(config, gcc_preprocessor_definition_key, libcpp_cxx17_fix)

# fix for weak linking
self.safe_init(config, other_ld_flags_key)
if self.is_using_xcode15_or_greter(:xcodebuild_manager => xcodebuild_manager)
self.add_value_to_setting_if_missing(config, other_ld_flags_key, xcode15_compatibility_flags)
else
self.remove_value_to_setting_if_present(config, other_ld_flags_key, xcode15_compatibility_flags)
end
end
project.save()
end

end

def self.apply_flags_for_fabric(installer, fabric_enabled: false)
Expand Down Expand Up @@ -281,6 +296,37 @@ def self.update_search_paths(installer)
end
end

def self.updateIphoneOSDeploymentTarget(installer)
pod_to_update = Set.new([
"boost",
"CocoaAsyncSocket",
"Flipper",
"Flipper-DoubleConversion",
"Flipper-Fmt",
"Flipper-Boost-iOSX",
"Flipper-Folly",
"Flipper-Glog",
"Flipper-PeerTalk",
"FlipperKit",
"fmt",
"libevent",
"OpenSSL-Universal",
"RCT-Folly",
"SocketRocket",
"YogaKit"
])

installer.target_installation_results.pod_target_installation_results
.each do |pod_name, target_installation_result|
unless pod_to_update.include?(pod_name)
next
end
target_installation_result.native_target.build_configurations.each do |config|
config.build_settings["IPHONEOS_DEPLOYMENT_TARGET"] = Helpers::Constants.min_ios_version_supported
end
end
end

# ========= #
# Utilities #
# ========= #
Expand All @@ -292,6 +338,49 @@ def self.extract_projects(installer)
.push(installer.pods_project)
end

def self.safe_init(config, setting_name)
old_config = config.build_settings[setting_name]
if old_config == nil
config.build_settings[setting_name] ||= '$(inherited) '
end
end

def self.add_value_to_setting_if_missing(config, setting_name, value)
old_config = config.build_settings[setting_name]
if !old_config.include?(value)
config.build_settings[setting_name] << value
end
end

def self.remove_value_to_setting_if_present(config, setting_name, value)
old_config = config.build_settings[setting_name]
if old_config.include?(value)
# Old config can be either an Array or a String
if old_config.is_a?(Array)
old_config = old_config.join(" ")
end
new_config = old_config.gsub(value, "")
config.build_settings[setting_name] = new_config
end
end

def self.is_using_xcode15_or_greter(xcodebuild_manager: Xcodebuild)
xcodebuild_version = xcodebuild_manager.version

# The output of xcodebuild -version is something like
# Xcode 15.0
# or
# Xcode 14.3.1
# We want to capture the version digits
regex = /(\d+)\.(\d+)(?:\.(\d+))?/
if match_data = xcodebuild_version.match(regex)
major = match_data[1].to_i
return major >= 15
end

return false
end

def self.add_compiler_flag_to_project(installer, flag, configuration: nil)
projects = self.extract_projects(installer)

Expand Down
5 changes: 4 additions & 1 deletion packages/react-native/scripts/react_native_pods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require_relative './cocoapods/new_architecture.rb'
require_relative './cocoapods/local_podspec_patch.rb'
require_relative './cocoapods/runtime.rb'
require_relative './cocoapods/helpers.rb'

$CODEGEN_OUTPUT_DIR = 'build/generated/ios'
$CODEGEN_COMPONENT_DIR = 'react/renderer/components'
Expand All @@ -37,7 +38,7 @@


def min_ios_version_supported
return '13.4'
return Helpers::Constants.min_ios_version_supported
end

# This function returns the min supported OS versions supported by React Native
Expand Down Expand Up @@ -272,10 +273,12 @@ def react_native_post_install(
ReactNativePodsUtils.apply_flags_for_fabric(installer, fabric_enabled: fabric_enabled)
ReactNativePodsUtils.apply_xcode_15_patch(installer)
ReactNativePodsUtils.apply_ats_config(installer)
ReactNativePodsUtils.updateIphoneOSDeploymentTarget(installer)

NewArchitectureHelper.set_clang_cxx_language_standard_if_needed(installer)
NewArchitectureHelper.modify_flags_for_new_architecture(installer, NewArchitectureHelper.new_arch_enabled)


Pod::UI.puts "Pod install took #{Time.now.to_i - $START_TIME} [s] to run".green
end

Expand Down