diff --git a/packages/react-native/scripts/cocoapods/__tests__/rndependencies-test.rb b/packages/react-native/scripts/cocoapods/__tests__/rndependencies-test.rb new file mode 100644 index 000000000000..854a3ecefa60 --- /dev/null +++ b/packages/react-native/scripts/cocoapods/__tests__/rndependencies-test.rb @@ -0,0 +1,89 @@ +# 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. + +require "test/unit" +require "shellwords" +require_relative "../rndependencies.rb" +require_relative "./test_utils/SpecMock.rb" + +class RNDependenciesTests < Test::Unit::TestCase + + # A pod that exports a Swift compatibility header ships this path, and the + # directory name contains spaces. + SWIFT_HEADER = "${PODS_CONFIGURATION_BUILD_DIR}/MyPod/Swift Compatibility Header" + + def teardown + ReactNativeDependenciesUtils.class_variable_set(:@@build_from_source, true) + end + + # Xcode joins an array setting with spaces, then splits it back on + # whitespace while honouring quotes. This is what the compiler ends up with. + def resolved_paths(xcconfig) + value = xcconfig["HEADER_SEARCH_PATHS"] + Shellwords.shellsplit(value.is_a?(Array) ? value.join(" ") : value) + end + + # ================================== # + # TEST - append_header_search_paths # + # ================================== # + + def test_appendHeaderSearchPaths_whenUnset_quotesTheAddedPaths + xcconfig = {} + + ReactNativeDependenciesUtils.append_header_search_paths(xcconfig, ["$(PODS_ROOT)/glog"]) + + assert_equal(["\"$(PODS_ROOT)/glog\""], xcconfig["HEADER_SEARCH_PATHS"]) + end + + def test_appendHeaderSearchPaths_whenStringHasQuotedPathWithSpaces_keepsItIntact + xcconfig = {"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/DoubleConversion\" \"#{SWIFT_HEADER}\""} + + ReactNativeDependenciesUtils.append_header_search_paths(xcconfig, ["$(PODS_ROOT)/glog"]) + + assert_equal(["$(PODS_ROOT)/DoubleConversion", SWIFT_HEADER, "$(PODS_ROOT)/glog"], resolved_paths(xcconfig)) + end + + def test_appendHeaderSearchPaths_whenArrayHasQuotedPathWithSpaces_keepsItIntact + xcconfig = {"HEADER_SEARCH_PATHS" => ["\"$(PODS_ROOT)/DoubleConversion\"", "\"#{SWIFT_HEADER}\""]} + + ReactNativeDependenciesUtils.append_header_search_paths(xcconfig, ["$(PODS_ROOT)/glog"]) + + assert_equal(["$(PODS_ROOT)/DoubleConversion", SWIFT_HEADER, "$(PODS_ROOT)/glog"], resolved_paths(xcconfig)) + end + + def test_appendHeaderSearchPaths_whenCalledTwice_doesNotDuplicateEntries + xcconfig = {"HEADER_SEARCH_PATHS" => "\"#{SWIFT_HEADER}\""} + + ReactNativeDependenciesUtils.append_header_search_paths(xcconfig, ["$(PODS_ROOT)/glog"]) + ReactNativeDependenciesUtils.append_header_search_paths(xcconfig, ["$(PODS_ROOT)/glog"]) + + assert_equal([SWIFT_HEADER, "$(PODS_ROOT)/glog"], resolved_paths(xcconfig)) + end + + # ======================================= # + # TEST - add_rn_third_party_dependencies # + # ======================================= # + + def test_addRNThirdPartyDependencies_whenBuildingFromSource_keepsQuotedPathWithSpaces + spec = SpecMock.new + spec.pod_target_xcconfig = {"HEADER_SEARCH_PATHS" => "\"#{SWIFT_HEADER}\""} + + add_rn_third_party_dependencies(spec) + + paths = resolved_paths(spec.pod_target_xcconfig) + assert_equal(SWIFT_HEADER, paths.first) + assert(paths.include?("$(PODS_ROOT)/RCT-Folly")) + end + + def test_addRNThirdPartyDependencies_whenUsingPrebuiltDeps_keepsQuotedPathWithSpaces + ReactNativeDependenciesUtils.class_variable_set(:@@build_from_source, false) + spec = SpecMock.new + spec.pod_target_xcconfig = {"HEADER_SEARCH_PATHS" => "\"#{SWIFT_HEADER}\""} + + add_rn_third_party_dependencies(spec) + + assert_equal([SWIFT_HEADER, "$(PODS_ROOT)/ReactNativeDependencies/Headers"], resolved_paths(spec.pod_target_xcconfig)) + end +end diff --git a/packages/react-native/scripts/cocoapods/rndependencies.rb b/packages/react-native/scripts/cocoapods/rndependencies.rb index 963290e5fdca..c38755263963 100644 --- a/packages/react-native/scripts/cocoapods/rndependencies.rb +++ b/packages/react-native/scripts/cocoapods/rndependencies.rb @@ -6,7 +6,6 @@ require "json" require 'net/http' require 'rexml/document' -require 'shellwords' require_relative './utils.rb' @@ -34,38 +33,26 @@ def add_rn_third_party_dependencies(s) s.dependency "RCT-Folly/Fabric" end - header_search_paths = current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] || [] - - if header_search_paths.is_a?(String) - header_search_paths = Shellwords.shellsplit(header_search_paths) - end - - header_search_paths << "$(PODS_ROOT)/glog" - header_search_paths << "$(PODS_ROOT)/boost" - header_search_paths << "$(PODS_ROOT)/DoubleConversion" - header_search_paths << "$(PODS_ROOT)/fast_float/include" - header_search_paths << "$(PODS_ROOT)/fmt/include" - header_search_paths << "$(PODS_ROOT)/SocketRocket" - header_search_paths << "$(PODS_ROOT)/RCT-Folly" - - # uniq so a second call on the same spec can't duplicate entries. - current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] = header_search_paths.uniq + ReactNativeDependenciesUtils.append_header_search_paths(current_pod_target_xcconfig, [ + "$(PODS_ROOT)/glog", + "$(PODS_ROOT)/boost", + "$(PODS_ROOT)/DoubleConversion", + "$(PODS_ROOT)/fast_float/include", + "$(PODS_ROOT)/fmt/include", + "$(PODS_ROOT)/SocketRocket", + "$(PODS_ROOT)/RCT-Folly", + ]) else # Prebuilt-deps mode: this pod SELF-SERVES the third-party headers from its # own xcframework (incl. SocketRocket - sole supplier in this mode). See # scripts/cocoapods/__docs__/prebuilt-deps.md for the full contract. s.dependency "ReactNativeDependencies" - header_search_paths = current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] || [] - if header_search_paths.is_a?(String) - header_search_paths = Shellwords.shellsplit(header_search_paths) - end # Artifact headers are flattened into the pod-local Headers/ by the podspec # prepare_command (see __docs__/prebuilt-deps.md). - header_search_paths << "$(PODS_ROOT)/ReactNativeDependencies/Headers" - - # uniq so a second call on the same spec can't duplicate entries. - current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] = header_search_paths.uniq + ReactNativeDependenciesUtils.append_header_search_paths(current_pod_target_xcconfig, [ + "$(PODS_ROOT)/ReactNativeDependencies/Headers", + ]) end s.pod_target_xcconfig = current_pod_target_xcconfig @@ -148,6 +135,25 @@ def self.setup_react_native_dependencies(react_native_path, react_native_version end end + # Xcode splits HEADER_SEARCH_PATHS on whitespace, so every path we add is + # quoted - PODS_ROOT can expand to a directory with spaces in its name. + # Paths already in the xcconfig are left untouched: they carry the podspec + # author's own quoting, and re-quoting them would break it. + def self.append_header_search_paths(xcconfig, paths) + quoted = paths.map { |path| "\"#{path}\"" } + existing = xcconfig["HEADER_SEARCH_PATHS"] + + # reject so a second call on the same spec can't duplicate entries. + case existing + when nil + xcconfig["HEADER_SEARCH_PATHS"] = quoted + when Array + xcconfig["HEADER_SEARCH_PATHS"] = existing + quoted.reject { |path| existing.include?(path) } + else + xcconfig["HEADER_SEARCH_PATHS"] = ([existing] + quoted.reject { |path| existing.include?(path) }).join(" ") + end + end + def self.abort_if_use_local_rndeps_with_no_file() if !File.exist?(ENV["RCT_USE_LOCAL_RN_DEP"]) abort("RCT_USE_LOCAL_RN_DEP is set to #{ENV["RCT_USE_LOCAL_RN_DEP"]} but the file does not exist!")