Skip to content

Commit

Permalink
Fix Xcode 15 RC issues (#39474)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #39474

When it comes to Xcode 15 RC, we are aware of two issues:
1. `unary_function` and `binary_function` not available in Cxx17
2. [Weak linking](https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes#Linking) is not supported anymore.

This change should fix both of the issues, adding the flags to allow for `unary_function`and `binary_function` to be called and adding the `-Wl -ld_classic` flag to `OTHER_LDFLAGS` in case Xcode 15 is detected.

## Changelog:
[Internal] - add the `_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION` and the `-Wl -ld_classic` flags to projects when needed

Reviewed By: dmytrorykun

Differential Revision: D49319256

fbshipit-source-id: cd93f0ad5c798cb9239e3454d0e9c38164ccc3e4
  • Loading branch information
cipolleschi authored and facebook-github-bot committed Sep 18, 2023
1 parent 5384733 commit 9a9b824
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 25 deletions.
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
8 changes: 8 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 Down
74 changes: 66 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 @@ -323,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

0 comments on commit 9a9b824

Please sign in to comment.