From 1674751f02a1e9cf12b8be13e4a1365e85437cae Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 21 Aug 2026 14:50:45 -0400 Subject: [PATCH] Fix ccache never being invoked on iOS builds CCACHE_BINARY is set as an Xcode build setting by set_ccache_compiler_and_linker_build_settings, and ccache-clang.sh reads it as an environment variable. Xcode does not export build settings into the environment of a compile task, so it is empty during a build and exec $CCACHE_BINARY clang "$@" word-splits down to plain clang. ccache is never invoked, and every translation unit pays for a shell fork that achieves nothing. Verified by logging the variable from inside the launcher during a clean rn-tester build: empty on all 1578 compiler invocations, with 0 cacheable calls. The build setting came from #48257, which fixed a real problem -- builds started from Xcode.app have no Homebrew on PATH. The intent was right, but the delivery mechanism is not available to a compiler launcher: Xcode passes it neither build settings nor arguments (a CC value containing arguments fails to spawn). pod install now generates the launchers into Pods/ with both paths resolved at install time, and points CC/CXX at those. The generated scripts are the ones this removes, with CCACHE_BINARY replaced by the resolved ccache path and the config path absolute rather than derived from $0. Pods/ is regenerated by every pod install and is gitignored in the app templates, so the paths cannot go stale and nothing tracked gains a machine-specific value. Dropping the build setting also stops pod install writing an absolute ccache path into a tracked project.pbxproj. rn-tester, clean Debug simulator build, Xcode 26.6, Apple M4: before 215s, 0 cacheable calls after, cold 210s, 1578/1582 cacheable after, warm 19s, 1578/1578 hits --- packages/react-native/package.json | 2 - .../scripts/cocoapods/__tests__/utils-test.rb | 30 ++++++++++++++ .../react-native/scripts/cocoapods/utils.rb | 40 +++++++++++++++++-- .../scripts/xcode/ccache-clang++.sh | 14 ------- .../scripts/xcode/ccache-clang.sh | 14 ------- 5 files changed, 67 insertions(+), 33 deletions(-) delete mode 100755 packages/react-native/scripts/xcode/ccache-clang++.sh delete mode 100755 packages/react-native/scripts/xcode/ccache-clang.sh diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 6d0af84fc364..bef13959b49d 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -117,8 +117,6 @@ "scripts/setup-apple-spm.js", "scripts/spm", "scripts/xcode/asset-catalog.sh", - "scripts/xcode/ccache-clang.sh", - "scripts/xcode/ccache-clang++.sh", "scripts/xcode/ccache.conf", "scripts/xcode/with-environment.sh", "sdks/.hermesv1version", diff --git a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb index 44e0e3bcaa9c..e265624abb90 100644 --- a/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb +++ b/packages/react-native/scripts/cocoapods/__tests__/utils-test.rb @@ -1155,6 +1155,36 @@ def test_add_ndebug_flag_to_pods_in_release assert_equal("$(inherited) -DNDEBUG", custom_release_config2.build_settings["OTHER_CPLUSPLUSFLAGS"]) assert_equal("$(inherited) -DNDEBUG", custom_release_config3.build_settings["OTHER_CPLUSPLUSFLAGS"]) end + + # ================================ # + # Test - ccache launcher script # + # ================================ # + + def test_ccacheLauncherScript_embedsResolvedCcachePath + # Act + script = ReactNativePodsUtils.ccache_launcher_script("clang", "/opt/homebrew/bin/ccache", "/app/rn/scripts/xcode/ccache.conf") + + # Assert + assert_equal('exec "/opt/homebrew/bin/ccache" clang "$@"', script.lines.last.chomp) + end + + def test_ccacheLauncherScript_embedsResolvedConfigPath + # Act + script = ReactNativePodsUtils.ccache_launcher_script("clang", "/opt/homebrew/bin/ccache", "/app/rn/scripts/xcode/ccache.conf") + + # Assert + assert(script.include?("REACT_NATIVE_CCACHE_CONFIGPATH=/app/rn/scripts/xcode/ccache.conf")) + assert(script.include?('${CCACHE_CONFIGPATH:-$REACT_NATIVE_CCACHE_CONFIGPATH}')) + end + + def test_ccacheLauncherScript_forCpp_execsClangpp + # Act + script = ReactNativePodsUtils.ccache_launcher_script("clang++", "/usr/local/bin/ccache", "/tmp/ccache.conf") + + # Assert + assert_equal('exec "/usr/local/bin/ccache" clang++ "$@"', script.lines.last.chomp) + end + end # ===== # diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index f65d0798fcb5..6833b67d5b34 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -156,6 +156,39 @@ def self.set_build_setting(installer, build_setting:, value:, config_name: nil) end end + # Generated rather than shipped: a compiler launcher gets none of Xcode's + # environment, so both paths have to be baked in at install time. + def self.ccache_launcher_script(compiler, ccache_path, config_path) + <<~SH + #!/bin/sh + # 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. + + # Generated by pod install. Edit react_native_pods.rb, not this file. + + REACT_NATIVE_CCACHE_CONFIGPATH=#{config_path} + # Provide our config file if none is already provided + export CCACHE_CONFIGPATH="${CCACHE_CONFIGPATH:-$REACT_NATIVE_CCACHE_CONFIGPATH}" + + exec "#{ccache_path}" #{compiler} "$@" + SH + end + + def self.generate_ccache_launchers(installer, react_native_path, ccache_path) + pods_root = installer.sandbox.root.to_s + config_path = File.expand_path( + File.join(react_native_path, 'scripts', 'xcode', 'ccache.conf'), + Pod::Config.instance.installation_root.to_s + ) + { 'ccache-clang.sh' => 'clang', 'ccache-clang++.sh' => 'clang++' }.each do |name, compiler| + path = File.join(pods_root, name) + File.write(path, self.ccache_launcher_script(compiler, ccache_path, config_path)) + File.chmod(0755, path) + end + end + def self.set_ccache_compiler_and_linker_build_settings(installer, react_native_path, ccache_enabled) projects = self.extract_projects(installer) @@ -169,8 +202,8 @@ def self.set_ccache_compiler_and_linker_build_settings(installer, react_native_p end # Using scripts wrapping the ccache executable, to allow injection of configurations - ccache_clang_sh = File.join("$(REACT_NATIVE_PATH)", 'scripts', 'xcode', 'ccache-clang.sh') - ccache_clangpp_sh = File.join("$(REACT_NATIVE_PATH)", 'scripts', 'xcode', 'ccache-clang++.sh') + ccache_clang_sh = File.join("$(PODS_ROOT)", 'ccache-clang.sh') + ccache_clangpp_sh = File.join("$(PODS_ROOT)", 'ccache-clang++.sh') if ccache_available and ccache_enabled Pod::UI.puts("#{message_prefix}: Setting CC, LD, CXX & LDPLUSPLUS build settings") @@ -182,11 +215,12 @@ def self.set_ccache_compiler_and_linker_build_settings(installer, react_native_p config.build_settings["LD"] = ccache_clang_sh config.build_settings["CXX"] = ccache_clangpp_sh config.build_settings["LDPLUSPLUS"] = ccache_clangpp_sh - config.build_settings["CCACHE_BINARY"] = ccache_path end project.save() end + + self.generate_ccache_launchers(installer, react_native_path, ccache_path) elsif ccache_available and !ccache_enabled Pod::UI.puts("#{message_prefix}: Pass ':ccache_enabled => true' to 'react_native_post_install' in your Podfile or set environment variable 'USE_CCACHE=1' to increase the speed of subsequent builds") elsif !ccache_available and ccache_enabled diff --git a/packages/react-native/scripts/xcode/ccache-clang++.sh b/packages/react-native/scripts/xcode/ccache-clang++.sh deleted file mode 100755 index 54ff8ba5816d..000000000000 --- a/packages/react-native/scripts/xcode/ccache-clang++.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -# 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. - -# Get the absolute path of this script -SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" - -REACT_NATIVE_CCACHE_CONFIGPATH=$SCRIPT_DIR/ccache.conf -# Provide our config file if none is already provided -export CCACHE_CONFIGPATH="${CCACHE_CONFIGPATH:-$REACT_NATIVE_CCACHE_CONFIGPATH}" - -exec $CCACHE_BINARY clang++ "$@" diff --git a/packages/react-native/scripts/xcode/ccache-clang.sh b/packages/react-native/scripts/xcode/ccache-clang.sh deleted file mode 100755 index 9b1a355c2cce..000000000000 --- a/packages/react-native/scripts/xcode/ccache-clang.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -# 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. - -# Get the absolute path of this script -SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" - -REACT_NATIVE_CCACHE_CONFIGPATH=$SCRIPT_DIR/ccache.conf -# Provide our config file if none is already provided -export CCACHE_CONFIGPATH="${CCACHE_CONFIGPATH:-$REACT_NATIVE_CCACHE_CONFIGPATH}" - -exec $CCACHE_BINARY clang "$@"