Skip to content

Commit 5a671c1

Browse files
cipolleschiTitozzz
authored andcommitted
Fix Xcode 15 RC issues (#39474)
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. [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: bb895f1e60db915db79684f71fa436ce80b42111
1 parent f6107b6 commit 5a671c1

File tree

4 files changed

+225
-26
lines changed

4 files changed

+225
-26
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
class XcodebuildMock < Xcodebuild
7+
@@version = ""
8+
@@version_invocation_count = 0
9+
10+
def self.set_version=(v)
11+
@@version = v
12+
end
13+
14+
def self.version
15+
@@version_invocation_count += 1
16+
@@version
17+
end
18+
19+
def self.version_invocation_count
20+
@@version_invocation_count
21+
end
22+
23+
def self.reset()
24+
@@version_invocation_count = 0
25+
end
26+
end

scripts/cocoapods/__tests__/utils-test.rb

+115-18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
require_relative "./test_utils/systemUtils.rb"
1515
require_relative "./test_utils/PathnameMock.rb"
1616
require_relative "./test_utils/TargetDefinitionMock.rb"
17+
require_relative "./test_utils/XcodebuildMock.rb"
1718

1819
class UtilsTests < Test::Unit::TestCase
1920
def setup
@@ -29,6 +30,7 @@ def teardown
2930
Pod::Config.reset()
3031
SysctlChecker.reset()
3132
Environment.reset()
33+
XcodebuildMock.reset()
3234
ENV['RCT_NEW_ARCH_ENABLED'] = '0'
3335
ENV['USE_HERMES'] = '1'
3436
ENV['USE_FRAMEWORKS'] = nil
@@ -438,9 +440,9 @@ def test_applyMacCatalystPatches_correctlyAppliesNecessaryPatches
438440
# ================================= #
439441
# Test - Apply Xcode 15 Patch #
440442
# ================================= #
441-
442-
def test_applyXcode15Patch_correctlyAppliesNecessaryPatch
443+
def test_applyXcode15Patch_whenXcodebuild14_correctlyAppliesNecessaryPatch
443444
# Arrange
445+
XcodebuildMock.set_version = "Xcode 14.3"
444446
first_target = prepare_target("FirstTarget")
445447
second_target = prepare_target("SecondTarget")
446448
third_target = TargetMock.new("ThirdTarget", [
@@ -469,24 +471,117 @@ def test_applyXcode15Patch_correctlyAppliesNecessaryPatch
469471
])
470472

471473
# Act
472-
ReactNativePodsUtils.apply_xcode_15_patch(installer)
474+
user_project_mock.build_configurations.each do |config|
475+
assert_nil(config.build_settings["OTHER_LDFLAGS"])
476+
end
477+
478+
ReactNativePodsUtils.apply_xcode_15_patch(installer, :xcodebuild_manager => XcodebuildMock)
473479

474480
# Assert
475-
first_target.build_configurations.each do |config|
476-
assert_equal(config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"].strip,
477-
'$(inherited) "_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION"'
478-
)
481+
user_project_mock.build_configurations.each do |config|
482+
assert_equal("$(inherited) _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION", config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"])
483+
assert_equal("$(inherited) ", config.build_settings["OTHER_LDFLAGS"])
479484
end
480-
second_target.build_configurations.each do |config|
481-
assert_equal(config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"].strip,
482-
'$(inherited) "_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION"'
483-
)
485+
486+
# User project and Pods project
487+
assert_equal(2, XcodebuildMock.version_invocation_count)
488+
end
489+
490+
def test_applyXcode15Patch_whenXcodebuild15_correctlyAppliesNecessaryPatch
491+
# Arrange
492+
XcodebuildMock.set_version = "Xcode 15.0"
493+
first_target = prepare_target("FirstTarget")
494+
second_target = prepare_target("SecondTarget")
495+
third_target = TargetMock.new("ThirdTarget", [
496+
BuildConfigurationMock.new("Debug", {
497+
"GCC_PREPROCESSOR_DEFINITIONS" => '$(inherited) "SomeFlag=1" '
498+
}),
499+
BuildConfigurationMock.new("Release", {
500+
"GCC_PREPROCESSOR_DEFINITIONS" => '$(inherited) "SomeFlag=1" '
501+
}),
502+
], nil)
503+
504+
user_project_mock = UserProjectMock.new("/a/path", [
505+
prepare_config("Debug"),
506+
prepare_config("Release"),
507+
],
508+
:native_targets => [
509+
first_target,
510+
second_target
511+
]
512+
)
513+
pods_projects_mock = PodsProjectMock.new([], {"hermes-engine" => {}}, :native_targets => [
514+
third_target
515+
])
516+
installer = InstallerMock.new(pods_projects_mock, [
517+
AggregatedProjectMock.new(user_project_mock)
518+
])
519+
520+
# Act
521+
user_project_mock.build_configurations.each do |config|
522+
assert_nil(config.build_settings["OTHER_LDFLAGS"])
484523
end
485-
third_target.build_configurations.each do |config|
486-
assert_equal(config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"].strip,
487-
'$(inherited) "SomeFlag=1" "_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION"'
488-
)
524+
525+
ReactNativePodsUtils.apply_xcode_15_patch(installer, :xcodebuild_manager => XcodebuildMock)
526+
527+
# Assert
528+
user_project_mock.build_configurations.each do |config|
529+
assert_equal("$(inherited) _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION", config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"])
530+
assert_equal("$(inherited) -Wl -ld_classic ", config.build_settings["OTHER_LDFLAGS"])
489531
end
532+
533+
# User project and Pods project
534+
assert_equal(2, XcodebuildMock.version_invocation_count)
535+
end
536+
537+
def test_applyXcode15Patch_whenXcodebuild14ButProjectHasSettings_correctlyRemovesNecessaryPatch
538+
# Arrange
539+
XcodebuildMock.set_version = "Xcode 14.3"
540+
first_target = prepare_target("FirstTarget")
541+
second_target = prepare_target("SecondTarget")
542+
third_target = TargetMock.new("ThirdTarget", [
543+
BuildConfigurationMock.new("Debug", {
544+
"GCC_PREPROCESSOR_DEFINITIONS" => '$(inherited) "SomeFlag=1" '
545+
}),
546+
BuildConfigurationMock.new("Release", {
547+
"GCC_PREPROCESSOR_DEFINITIONS" => '$(inherited) "SomeFlag=1" '
548+
}),
549+
], nil)
550+
551+
debug_config = prepare_config("Debug", {"OTHER_LDFLAGS" => "$(inherited) -Wl -ld_classic "})
552+
release_config = prepare_config("Release", {"OTHER_LDFLAGS" => "$(inherited) -Wl -ld_classic "})
553+
554+
user_project_mock = UserProjectMock.new("/a/path", [
555+
debug_config,
556+
release_config,
557+
],
558+
:native_targets => [
559+
first_target,
560+
second_target
561+
]
562+
)
563+
pods_projects_mock = PodsProjectMock.new([debug_config.clone, release_config.clone], {"hermes-engine" => {}}, :native_targets => [
564+
third_target
565+
])
566+
installer = InstallerMock.new(pods_projects_mock, [
567+
AggregatedProjectMock.new(user_project_mock)
568+
])
569+
570+
# Act
571+
user_project_mock.build_configurations.each do |config|
572+
assert_equal("$(inherited) -Wl -ld_classic ", config.build_settings["OTHER_LDFLAGS"])
573+
end
574+
575+
ReactNativePodsUtils.apply_xcode_15_patch(installer, :xcodebuild_manager => XcodebuildMock)
576+
577+
# Assert
578+
user_project_mock.build_configurations.each do |config|
579+
assert_equal("$(inherited) _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION", config.build_settings["GCC_PREPROCESSOR_DEFINITIONS"])
580+
assert_equal("$(inherited) ", config.build_settings["OTHER_LDFLAGS"])
581+
end
582+
583+
# User project and Pods project
584+
assert_equal(2, XcodebuildMock.version_invocation_count)
490585
end
491586

492587
# ==================================== #
@@ -597,12 +692,14 @@ def prepare_empty_user_project_mock
597692
])
598693
end
599694

600-
def prepare_config(config_name)
601-
return BuildConfigurationMock.new(config_name, {"LIBRARY_SEARCH_PATHS" => [
695+
def prepare_config(config_name, extra_config = {})
696+
config = {"LIBRARY_SEARCH_PATHS" => [
602697
"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)",
603698
"\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
604699
"another/path",
605-
]})
700+
]}.merge(extra_config)
701+
702+
return BuildConfigurationMock.new(config_name, config)
606703
end
607704

608705
def prepare_target(name, product_type = nil)

scripts/cocoapods/helpers.rb

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ def call_sysctl_arm64
1111
end
1212
end
1313

14+
# Helper class that is used to easily send commands to Xcodebuild
15+
# And that can be subclassed for testing purposes.
16+
class Xcodebuild
17+
def self.version
18+
`xcodebuild -version`
19+
end
20+
end
21+
1422
# Helper object to wrap system properties like RUBY_PLATFORM
1523
# This makes it easier to mock the behaviour in tests
1624
class Environment

scripts/cocoapods/utils.rb

+76-8
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,29 @@ def self.apply_mac_catalyst_patches(installer)
131131
end
132132
end
133133

134-
def self.apply_xcode_15_patch(installer)
135-
installer.target_installation_results.pod_target_installation_results
136-
.each do |pod_name, target_installation_result|
137-
target_installation_result.native_target.build_configurations.each do |config|
138-
# unary_function and binary_function are no longer provided in C++17 and newer standard modes as part of Xcode 15. They can be re-enabled with setting _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION
139-
# Ref: https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes#Deprecations
140-
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= '$(inherited) '
141-
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '"_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION" '
134+
def self.apply_xcode_15_patch(installer, xcodebuild_manager: Xcodebuild)
135+
projects = self.extract_projects(installer)
136+
137+
gcc_preprocessor_definition_key = 'GCC_PREPROCESSOR_DEFINITIONS'
138+
other_ld_flags_key = 'OTHER_LDFLAGS'
139+
libcpp_cxx17_fix = '_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION'
140+
xcode15_compatibility_flags = '-Wl -ld_classic '
141+
142+
projects.each do |project|
143+
project.build_configurations.each do |config|
144+
# fix for unary_function and binary_function
145+
self.safe_init(config, gcc_preprocessor_definition_key)
146+
self.add_value_to_setting_if_missing(config, gcc_preprocessor_definition_key, libcpp_cxx17_fix)
147+
148+
# fix for weak linking
149+
self.safe_init(config, other_ld_flags_key)
150+
if self.is_using_xcode15_or_greter(:xcodebuild_manager => xcodebuild_manager)
151+
self.add_value_to_setting_if_missing(config, other_ld_flags_key, xcode15_compatibility_flags)
152+
else
153+
self.remove_value_to_setting_if_present(config, other_ld_flags_key, xcode15_compatibility_flags)
154+
end
142155
end
156+
project.save()
143157
end
144158
end
145159

@@ -228,4 +242,58 @@ def self.updateIphoneOSDeploymentTarget(installer)
228242
end
229243
end
230244
end
245+
246+
# ========= #
247+
# Utilities #
248+
# ========= #
249+
250+
def self.extract_projects(installer)
251+
return installer.aggregate_targets
252+
.map{ |t| t.user_project }
253+
.uniq{ |p| p.path }
254+
.push(installer.pods_project)
255+
end
256+
257+
def self.safe_init(config, setting_name)
258+
old_config = config.build_settings[setting_name]
259+
if old_config == nil
260+
config.build_settings[setting_name] ||= '$(inherited) '
261+
end
262+
end
263+
264+
def self.add_value_to_setting_if_missing(config, setting_name, value)
265+
old_config = config.build_settings[setting_name]
266+
if !old_config.include?(value)
267+
config.build_settings[setting_name] << value
268+
end
269+
end
270+
271+
def self.remove_value_to_setting_if_present(config, setting_name, value)
272+
old_config = config.build_settings[setting_name]
273+
if old_config.include?(value)
274+
# Old config can be either an Array or a String
275+
if old_config.is_a?(Array)
276+
old_config = old_config.join(" ")
277+
end
278+
new_config = old_config.gsub(value, "")
279+
config.build_settings[setting_name] = new_config
280+
end
281+
end
282+
283+
def self.is_using_xcode15_or_greter(xcodebuild_manager: Xcodebuild)
284+
xcodebuild_version = xcodebuild_manager.version
285+
286+
# The output of xcodebuild -version is something like
287+
# Xcode 15.0
288+
# or
289+
# Xcode 14.3.1
290+
# We want to capture the version digits
291+
regex = /(\d+)\.(\d+)(?:\.(\d+))?/
292+
if match_data = xcodebuild_version.match(regex)
293+
major = match_data[1].to_i
294+
return major >= 15
295+
end
296+
297+
return false
298+
end
231299
end

0 commit comments

Comments
 (0)