From 41e65361e92dc5520e59ab939ed34ba3b1f31cee Mon Sep 17 00:00:00 2001 From: lazerg Date: Tue, 27 Oct 2026 19:22:05 +0500 Subject: [PATCH 1/3] Recreate .xcode.env.local on pod install when its node no longer exists --- .../scripts/cocoapods/__tests__/utils-test.rb | 21 ++++++++++++++++--- .../react-native/scripts/cocoapods/utils.rb | 8 ++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb index 2f338430009c..fddab85193fc 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb @@ -747,15 +747,30 @@ def test_setBuildSettingDebug_addTheUserSetting # =================================== # def test_createXcodeEnvIfMissing_whenTheyArePresent_doNothing # Arrange - FileMock.mocked_existing_files("/.xcode.env") - FileMock.mocked_existing_files("/.xcode.env.local") + FileMock.mocked_existing_files(["/.xcode.env", "/.xcode.env.local", "/usr/local/bin/node"]) + FileMock.files_to_read({ + "/.xcode.env.local" => "export NODE_BINARY=/usr/local/bin/node\n", + }) # Act ReactNativePodsUtils.create_xcode_env_if_missing(file_manager: FileMock) # Assert - assert_equal(["/.xcode.env", "/.xcode.env.local"], FileMock.exist_invocation_params) + assert_equal(["/.xcode.env", "/.xcode.env.local", "/usr/local/bin/node"], FileMock.exist_invocation_params) assert_equal([], $collected_commands) end + def test_createXcodeEnvIfMissing_whenLocalNodeIsMissing_recreatesLocal + # Arrange + FileMock.mocked_existing_files(["/.xcode.env", "/.xcode.env.local"]) + FileMock.files_to_read({ + "/.xcode.env.local" => "export NODE_BINARY=/Users/me/.nvm/versions/node/v24.18.1/bin/node\n", + }) + # Act + ReactNativePodsUtils.create_xcode_env_if_missing(file_manager: FileMock) + # Assert + assert_equal(1, $collected_commands.length) + assert_match(/^echo 'export NODE_BINARY=.*' > \/\.xcode\.env\.local$/, $collected_commands[0]) + end + def test_createXcodeEnvIfMissing_whenTheyAreNotPresent_createsThem # Arrange diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index 6473278a2e7e..93dffaa29311 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -306,7 +306,7 @@ def self.create_xcode_env_if_missing(file_manager: File) system("echo 'export NODE_BINARY=$(command -v node)' > #{file_path}") end - if !file_manager.exist?("#{file_path}.local") + if !file_manager.exist?("#{file_path}.local") || !self.local_node_binary_exists?("#{file_path}.local", file_manager) # When installing pods with a yarn alias, yarn creates a fake yarn and node executables # in a temporary folder. # Using `node --print "process.argv[0]";` we are able to retrieve the actual path from which node is running. @@ -316,6 +316,12 @@ def self.create_xcode_env_if_missing(file_manager: File) end end + def self.local_node_binary_exists?(local_file_path, file_manager) + node_binary = file_manager.read(local_file_path)[/^export NODE_BINARY=(.+)$/, 1] + return true if node_binary.nil? || !node_binary.start_with?("/") + return file_manager.exist?(Shellwords.split(node_binary).first) + end + # It examines the target_definition property and sets the appropriate value for # ENV['USE_FRAMEWORKS'] variable. # From bcc9d3283b41ea4ced7f436456d08c4add9d494c Mon Sep 17 00:00:00 2001 From: lazerg Date: Tue, 27 Oct 2026 19:24:38 +0500 Subject: [PATCH 2/3] Keep the rest of .xcode.env.local when replacing a removed node --- .../cocoapods/__tests__/test_utils/FileMock.rb | 10 ++++++++++ .../scripts/cocoapods/__tests__/utils-test.rb | 9 +++++---- packages/react-native/scripts/cocoapods/utils.rb | 14 ++++++++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/react-native/scripts/cocoapods/__tests__/test_utils/FileMock.rb b/packages/react-native/scripts/cocoapods/__tests__/test_utils/FileMock.rb index 8b9796cd951f..cc85741e2a00 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/test_utils/FileMock.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/test_utils/FileMock.rb @@ -16,6 +16,7 @@ class FileMock < File @@open_files = [] @@files_to_read = {} + @@written_files = {} attr_reader :collected_write attr_reader :fsync_invocation_count @@ -98,6 +99,14 @@ def self.read(filepath) return @@files_to_read[filepath] end + def self.write(filepath, content) + @@written_files[filepath] = content + end + + def self.written_files + return @@written_files + end + # Resets all the settings for the File mock def self.reset() @@delete_invocation_count = 0 @@ -109,5 +118,6 @@ def self.reset() @@file_invocation_params = [] @@exist_invocation_params = [] @@files_to_read = {} + @@written_files = {} end end diff --git a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb index fddab85193fc..c2ec8f76f9ca 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb @@ -758,17 +758,18 @@ def test_createXcodeEnvIfMissing_whenTheyArePresent_doNothing assert_equal([], $collected_commands) end - def test_createXcodeEnvIfMissing_whenLocalNodeIsMissing_recreatesLocal + def test_createXcodeEnvIfMissing_whenLocalNodeIsMissing_updatesNodeBinary # Arrange FileMock.mocked_existing_files(["/.xcode.env", "/.xcode.env.local"]) FileMock.files_to_read({ - "/.xcode.env.local" => "export NODE_BINARY=/Users/me/.nvm/versions/node/v24.18.1/bin/node\n", + "/.xcode.env.local" => "export FOO=bar\nexport NODE_BINARY=/old/bin/node\n", }) # Act ReactNativePodsUtils.create_xcode_env_if_missing(file_manager: FileMock) # Assert - assert_equal(1, $collected_commands.length) - assert_match(/^echo 'export NODE_BINARY=.*' > \/\.xcode\.env\.local$/, $collected_commands[0]) + assert_equal(["/.xcode.env", "/.xcode.env.local", "/old/bin/node"], FileMock.exist_invocation_params) + assert_equal([], $collected_commands) + assert_match(/\Aexport FOO=bar\nexport NODE_BINARY=(?!\/old\/bin\/node).+\n\z/, FileMock.written_files["/.xcode.env.local"]) end def test_createXcodeEnvIfMissing_whenTheyAreNotPresent_createsThem diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index 93dffaa29311..93af0cc131c6 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -306,18 +306,24 @@ def self.create_xcode_env_if_missing(file_manager: File) system("echo 'export NODE_BINARY=$(command -v node)' > #{file_path}") end - if !file_manager.exist?("#{file_path}.local") || !self.local_node_binary_exists?("#{file_path}.local", file_manager) + local_file_exists = file_manager.exist?("#{file_path}.local") + if !local_file_exists || !self.local_node_binary_exists?("#{file_path}.local", file_manager: file_manager) # When installing pods with a yarn alias, yarn creates a fake yarn and node executables # in a temporary folder. # Using `node --print "process.argv[0]";` we are able to retrieve the actual path from which node is running. # see https://github.com/facebook/react-native/issues/43285 for more info. We've tweaked this slightly. node_binary = Shellwords.escape(`node --print "process.argv[0]"`.strip) - system("echo 'export NODE_BINARY=#{node_binary}' > #{file_path}.local") + if local_file_exists + local_env = file_manager.read("#{file_path}.local") + file_manager.write("#{file_path}.local", local_env.gsub(/^export NODE_BINARY=.+$/) { "export NODE_BINARY=#{node_binary}" }) + else + system("echo 'export NODE_BINARY=#{node_binary}' > #{file_path}.local") + end end end - def self.local_node_binary_exists?(local_file_path, file_manager) - node_binary = file_manager.read(local_file_path)[/^export NODE_BINARY=(.+)$/, 1] + def self.local_node_binary_exists?(local_file_path, file_manager: File) + node_binary = file_manager.read(local_file_path).scan(/^export NODE_BINARY=(.+)$/).flatten.last return true if node_binary.nil? || !node_binary.start_with?("/") return file_manager.exist?(Shellwords.split(node_binary).first) end From c951f796354bbea9132b5b476bf50391ac5fcd2b Mon Sep 17 00:00:00 2001 From: lazerg Date: Wed, 28 Oct 2026 07:02:05 +0500 Subject: [PATCH 3/3] Treat an empty NODE_BINARY in .xcode.env.local as broken --- .../scripts/cocoapods/__tests__/utils-test.rb | 14 ++++++++++++++ packages/react-native/scripts/cocoapods/utils.rb | 11 +++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb index c2ec8f76f9ca..5b2e28090002 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb @@ -772,6 +772,20 @@ def test_createXcodeEnvIfMissing_whenLocalNodeIsMissing_updatesNodeBinary assert_match(/\Aexport FOO=bar\nexport NODE_BINARY=(?!\/old\/bin\/node).+\n\z/, FileMock.written_files["/.xcode.env.local"]) end + def test_createXcodeEnvIfMissing_whenLocalNodeIsEmpty_updatesNodeBinary + # Arrange + FileMock.mocked_existing_files(["/.xcode.env", "/.xcode.env.local"]) + FileMock.files_to_read({ + "/.xcode.env.local" => "export FOO=bar\nexport NODE_BINARY=\n", + }) + # Act + ReactNativePodsUtils.create_xcode_env_if_missing(file_manager: FileMock) + # Assert + assert_equal(["/.xcode.env", "/.xcode.env.local"], FileMock.exist_invocation_params) + assert_equal([], $collected_commands) + assert_match(/\Aexport FOO=bar\nexport NODE_BINARY=.+\n\z/, FileMock.written_files["/.xcode.env.local"]) + end + def test_createXcodeEnvIfMissing_whenTheyAreNotPresent_createsThem # Arrange diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index 93af0cc131c6..b8e6263d9fc5 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -315,7 +315,7 @@ def self.create_xcode_env_if_missing(file_manager: File) node_binary = Shellwords.escape(`node --print "process.argv[0]"`.strip) if local_file_exists local_env = file_manager.read("#{file_path}.local") - file_manager.write("#{file_path}.local", local_env.gsub(/^export NODE_BINARY=.+$/) { "export NODE_BINARY=#{node_binary}" }) + file_manager.write("#{file_path}.local", local_env.gsub(/^export NODE_BINARY=.*$/) { "export NODE_BINARY=#{node_binary}" }) else system("echo 'export NODE_BINARY=#{node_binary}' > #{file_path}.local") end @@ -323,9 +323,12 @@ def self.create_xcode_env_if_missing(file_manager: File) end def self.local_node_binary_exists?(local_file_path, file_manager: File) - node_binary = file_manager.read(local_file_path).scan(/^export NODE_BINARY=(.+)$/).flatten.last - return true if node_binary.nil? || !node_binary.start_with?("/") - return file_manager.exist?(Shellwords.split(node_binary).first) + node_binary = file_manager.read(local_file_path).scan(/^export NODE_BINARY=(.*)$/).flatten.last + return true if node_binary.nil? + node_binary = Shellwords.split(node_binary).first + return false if node_binary.nil? || node_binary.empty? + return true if !node_binary.start_with?("/") + return file_manager.exist?(node_binary) end # It examines the target_definition property and sets the appropriate value for