From 2092992c45ea3da6cef26ac757ba41507375d051 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Fri, 15 May 2026 14:30:36 +0100 Subject: [PATCH 1/4] Cache prebuilt iOS binaries in ~/Library/Caches/ReactNative Currently, Hermes, ReactNativeDependencies, and ReactNativeCore tarballs are cached only inside the Pods/ directory. This means every clean `pod install` (or deleting the Pods folder) triggers a full re-download from Maven, even if the same version was already downloaded before. This adds a shared cache layer at ~/Library/Caches/ReactNative/. On first download, tarballs are saved to the shared cache. On subsequent pod installs, if the local Pods cache is empty but the shared cache has the tarball, it is copied locally instead of re-downloaded. This benefits: - Clean installs after deleting Pods/ - Multiple projects using the same React Native version - CI environments with a persistent home directory Added descriptive log messages for each scenario (local hit, shared cache hit, cache miss + download) to aid debugging. --- .../react-native/scripts/cocoapods/rncore.rb | 34 ++++++++++++++----- .../scripts/cocoapods/rndependencies.rb | 31 ++++++++++++++--- .../sdks/hermes-engine/hermes-utils.rb | 32 ++++++++++++++--- 3 files changed, 79 insertions(+), 18 deletions(-) diff --git a/packages/react-native/scripts/cocoapods/rncore.rb b/packages/react-native/scripts/cocoapods/rncore.rb index aaa4e5d33250..5fdddb616d50 100644 --- a/packages/react-native/scripts/cocoapods/rncore.rb +++ b/packages/react-native/scripts/cocoapods/rncore.rb @@ -405,18 +405,36 @@ def self.download_nightly_rncore(react_native_path, version, configuration, dsym download_rncore_tarball(react_native_path, tarball_url, version, configuration, dsyms) end + def self.shared_cache_dir() + return File.join(Dir.home, "Library", "Caches", "ReactNative") + end + def self.download_rncore_tarball(react_native_path, tarball_url, version, configuration, dsyms = false) - destination_path = configuration == nil ? - "#{artifacts_dir()}/reactnative-core-#{version}#{dsyms ? "-dSYM" : ""}.tar.gz" : - "#{artifacts_dir()}/reactnative-core-#{version}#{dsyms ? "-dSYM" : ""}-#{configuration}.tar.gz" + filename = configuration == nil ? + "reactnative-core-#{version}#{dsyms ? "-dSYM" : ""}.tar.gz" : + "reactnative-core-#{version}#{dsyms ? "-dSYM" : ""}-#{configuration}.tar.gz" + destination_path = "#{artifacts_dir()}/#{filename}" + + if File.exist?(destination_path) + rncore_log("Tarball #{filename} already exists in Pods. Skipping download.") + return destination_path + end - unless File.exist?(destination_path) + `mkdir -p "#{artifacts_dir()}"` + + cached_path = File.join(shared_cache_dir(), filename) + if File.exist?(cached_path) + rncore_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})") + FileUtils.cp(cached_path, destination_path) + else + rncore_log("Cache miss: downloading #{filename} from #{tarball_url}") # Download to a temporary file first so we don't cache incomplete downloads. - rncore_log("Downloading ReactNativeCore-prebuilt #{dsyms ? "dSYMs " : ""}#{configuration ? configuration.to_s : ""} tarball from #{tarball_url} to #{Pathname.new(destination_path).relative_path_from(Pathname.pwd).to_s}") tmp_file = "#{artifacts_dir()}/reactnative-core.download" - `mkdir -p "#{artifacts_dir()}" && curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` - else - rncore_log("Using downloaded ReactNativeCore-prebuilt #{dsyms ? "dSYMs " : ""}#{configuration ? configuration.to_s : ""} tarball at #{Pathname.new(destination_path).relative_path_from(Pathname.pwd).to_s}") + `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` + # Save to shared cache for future use + `mkdir -p "#{shared_cache_dir()}"` + FileUtils.cp(destination_path, cached_path) + rncore_log("Saved #{filename} to shared cache (#{shared_cache_dir()})") end return destination_path diff --git a/packages/react-native/scripts/cocoapods/rndependencies.rb b/packages/react-native/scripts/cocoapods/rndependencies.rb index 2000e316cf73..6e1e336b9637 100644 --- a/packages/react-native/scripts/cocoapods/rndependencies.rb +++ b/packages/react-native/scripts/cocoapods/rndependencies.rb @@ -231,15 +231,36 @@ def self.podspec_source_download_prebuilt_nightly_tarball(version) return {:http => url } end + def self.shared_cache_dir() + return File.join(Dir.home, "Library", "Caches", "ReactNative") + end + def self.download_rndeps_tarball(react_native_path, tarball_url, version, configuration) - destination_path = configuration == nil ? - "#{artifacts_dir()}/reactnative-dependencies-#{version}.tar.gz" : - "#{artifacts_dir()}/reactnative-dependencies-#{version}-#{configuration}.tar.gz" + filename = configuration == nil ? + "reactnative-dependencies-#{version}.tar.gz" : + "reactnative-dependencies-#{version}-#{configuration}.tar.gz" + destination_path = "#{artifacts_dir()}/#{filename}" + + if File.exist?(destination_path) + rndeps_log("Tarball #{filename} already exists in Pods. Skipping download.") + return destination_path + end - unless File.exist?(destination_path) + `mkdir -p "#{artifacts_dir()}"` + + cached_path = File.join(shared_cache_dir(), filename) + if File.exist?(cached_path) + rndeps_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})") + FileUtils.cp(cached_path, destination_path) + else + rndeps_log("Cache miss: downloading #{filename} from #{tarball_url}") # Download to a temporary file first so we don't cache incomplete downloads. tmp_file = "#{artifacts_dir()}/reactnative-dependencies.download" - `mkdir -p "#{artifacts_dir()}" && curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` + `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` + # Save to shared cache for future use + `mkdir -p "#{shared_cache_dir()}"` + FileUtils.cp(destination_path, cached_path) + rndeps_log("Saved #{filename} to shared cache (#{shared_cache_dir()})") end return destination_path diff --git a/packages/react-native/sdks/hermes-engine/hermes-utils.rb b/packages/react-native/sdks/hermes-engine/hermes-utils.rb index cdacf7e830f6..b984655216e6 100644 --- a/packages/react-native/sdks/hermes-engine/hermes-utils.rb +++ b/packages/react-native/sdks/hermes-engine/hermes-utils.rb @@ -206,16 +206,38 @@ def download_stable_hermes(react_native_path, version, configuration) download_hermes_tarball(react_native_path, tarball_url, version, configuration) end +def shared_cache_dir() + return File.join(Dir.home, "Library", "Caches", "ReactNative") +end + def download_hermes_tarball(react_native_path, tarball_url, version, configuration) - destination_path = configuration == nil ? - "#{artifacts_dir()}/hermes-ios-#{version}.tar.gz" : - "#{artifacts_dir()}/hermes-ios-#{version}-#{configuration}.tar.gz" + filename = configuration == nil ? + "hermes-ios-#{version}.tar.gz" : + "hermes-ios-#{version}-#{configuration}.tar.gz" + destination_path = "#{artifacts_dir()}/#{filename}" + + if File.exist?(destination_path) + hermes_log("Tarball #{filename} already exists in Pods. Skipping download.", :info) + return destination_path + end - unless File.exist?(destination_path) + `mkdir -p "#{artifacts_dir()}"` + + cached_path = File.join(shared_cache_dir(), filename) + if File.exist?(cached_path) + hermes_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})", :info) + FileUtils.cp(cached_path, destination_path) + else + hermes_log("Cache miss: downloading #{filename} from #{tarball_url}", :info) # Download to a temporary file first so we don't cache incomplete downloads. tmp_file = "#{artifacts_dir()}/hermes-ios.download" - `mkdir -p "#{artifacts_dir()}" && curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` + `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` + # Save to shared cache for future use + `mkdir -p "#{shared_cache_dir()}"` + FileUtils.cp(destination_path, cached_path) + hermes_log("Saved #{filename} to shared cache (#{shared_cache_dir()})", :info) end + return destination_path end From ce18d835963b0c4974f1225fc00954382bf14fd1 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Fri, 15 May 2026 16:03:30 +0100 Subject: [PATCH 2/4] Add SHA1 validation for cached prebuilt iOS binaries Fetches the .sha1 checksum from Maven for each downloaded tarball and validates file integrity at two points: 1. When reading from the shared cache: if the cached file's SHA1 doesn't match Maven's, it is treated as corrupted and re-downloaded, replacing the stale cache entry. 2. After a fresh download: validates the download succeeded correctly before saving to the shared cache. If verification fails, the file is not cached (but still used locally, as CocoaPods will re-extract it). If Maven doesn't serve a .sha1 for a given artifact (e.g. some nightly builds), validation is skipped gracefully. --- .../react-native/scripts/cocoapods/rncore.rb | 42 +++++++++++++++--- .../scripts/cocoapods/rndependencies.rb | 42 +++++++++++++++--- .../sdks/hermes-engine/hermes-utils.rb | 43 ++++++++++++++++--- 3 files changed, 109 insertions(+), 18 deletions(-) diff --git a/packages/react-native/scripts/cocoapods/rncore.rb b/packages/react-native/scripts/cocoapods/rncore.rb index 5fdddb616d50..49172c5a87ed 100644 --- a/packages/react-native/scripts/cocoapods/rncore.rb +++ b/packages/react-native/scripts/cocoapods/rncore.rb @@ -6,6 +6,7 @@ require 'json' require 'net/http' require 'rexml/document' +require 'digest' require_relative './utils.rb' @@ -409,6 +410,27 @@ def self.shared_cache_dir() return File.join(Dir.home, "Library", "Caches", "ReactNative") end + def self.fetch_maven_sha1(tarball_url) + sha1 = `curl -sL "#{tarball_url}.sha1"`.strip + return sha1.downcase if $?.success? && sha1.match?(/\A[a-fA-F0-9]{40}\z/) + nil + end + + def self.validate_tarball(path, tarball_url) + expected_sha1 = fetch_maven_sha1(tarball_url) + if expected_sha1.nil? + rncore_log("SHA1 not available from Maven for #{File.basename(path)}. Skipping validation.") + return true + end + actual_sha1 = Digest::SHA1.file(path).hexdigest + if actual_sha1 == expected_sha1 + rncore_log("SHA1 verified for #{File.basename(path)}") + return true + end + rncore_log("SHA1 mismatch for #{File.basename(path)}: expected #{expected_sha1}, got #{actual_sha1}", :error) + return false + end + def self.download_rncore_tarball(react_native_path, tarball_url, version, configuration, dsyms = false) filename = configuration == nil ? "reactnative-core-#{version}#{dsyms ? "-dSYM" : ""}.tar.gz" : @@ -423,18 +445,26 @@ def self.download_rncore_tarball(react_native_path, tarball_url, version, config `mkdir -p "#{artifacts_dir()}"` cached_path = File.join(shared_cache_dir(), filename) - if File.exist?(cached_path) + if File.exist?(cached_path) && validate_tarball(cached_path, tarball_url) rncore_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})") FileUtils.cp(cached_path, destination_path) else - rncore_log("Cache miss: downloading #{filename} from #{tarball_url}") + if File.exist?(cached_path) + rncore_log("Shared cache file #{filename} failed SHA verification. Re-downloading.") + else + rncore_log("Cache miss: downloading #{filename} from #{tarball_url}") + end # Download to a temporary file first so we don't cache incomplete downloads. tmp_file = "#{artifacts_dir()}/reactnative-core.download" `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` - # Save to shared cache for future use - `mkdir -p "#{shared_cache_dir()}"` - FileUtils.cp(destination_path, cached_path) - rncore_log("Saved #{filename} to shared cache (#{shared_cache_dir()})") + if validate_tarball(destination_path, tarball_url) + # Save to shared cache for future use + `mkdir -p "#{shared_cache_dir()}"` + FileUtils.cp(destination_path, cached_path) + rncore_log("Saved #{filename} to shared cache (#{shared_cache_dir()})") + else + rncore_log("Downloaded file #{filename} failed SHA verification!", :error) + end end return destination_path diff --git a/packages/react-native/scripts/cocoapods/rndependencies.rb b/packages/react-native/scripts/cocoapods/rndependencies.rb index 6e1e336b9637..0d7f592b53cb 100644 --- a/packages/react-native/scripts/cocoapods/rndependencies.rb +++ b/packages/react-native/scripts/cocoapods/rndependencies.rb @@ -6,6 +6,7 @@ require "json" require 'net/http' require 'rexml/document' +require 'digest' require_relative './utils.rb' @@ -235,6 +236,27 @@ def self.shared_cache_dir() return File.join(Dir.home, "Library", "Caches", "ReactNative") end + def self.fetch_maven_sha1(tarball_url) + sha1 = `curl -sL "#{tarball_url}.sha1"`.strip + return sha1.downcase if $?.success? && sha1.match?(/\A[a-fA-F0-9]{40}\z/) + nil + end + + def self.validate_tarball(path, tarball_url) + expected_sha1 = fetch_maven_sha1(tarball_url) + if expected_sha1.nil? + rndeps_log("SHA1 not available from Maven for #{File.basename(path)}. Skipping validation.") + return true + end + actual_sha1 = Digest::SHA1.file(path).hexdigest + if actual_sha1 == expected_sha1 + rndeps_log("SHA1 verified for #{File.basename(path)}") + return true + end + rndeps_log("SHA1 mismatch for #{File.basename(path)}: expected #{expected_sha1}, got #{actual_sha1}", :error) + return false + end + def self.download_rndeps_tarball(react_native_path, tarball_url, version, configuration) filename = configuration == nil ? "reactnative-dependencies-#{version}.tar.gz" : @@ -249,18 +271,26 @@ def self.download_rndeps_tarball(react_native_path, tarball_url, version, config `mkdir -p "#{artifacts_dir()}"` cached_path = File.join(shared_cache_dir(), filename) - if File.exist?(cached_path) + if File.exist?(cached_path) && validate_tarball(cached_path, tarball_url) rndeps_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})") FileUtils.cp(cached_path, destination_path) else - rndeps_log("Cache miss: downloading #{filename} from #{tarball_url}") + if File.exist?(cached_path) + rndeps_log("Shared cache file #{filename} failed SHA verification. Re-downloading.") + else + rndeps_log("Cache miss: downloading #{filename} from #{tarball_url}") + end # Download to a temporary file first so we don't cache incomplete downloads. tmp_file = "#{artifacts_dir()}/reactnative-dependencies.download" `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` - # Save to shared cache for future use - `mkdir -p "#{shared_cache_dir()}"` - FileUtils.cp(destination_path, cached_path) - rndeps_log("Saved #{filename} to shared cache (#{shared_cache_dir()})") + if validate_tarball(destination_path, tarball_url) + # Save to shared cache for future use + `mkdir -p "#{shared_cache_dir()}"` + FileUtils.cp(destination_path, cached_path) + rndeps_log("Saved #{filename} to shared cache (#{shared_cache_dir()})") + else + rndeps_log("Downloaded file #{filename} failed SHA verification!", :error) + end end return destination_path diff --git a/packages/react-native/sdks/hermes-engine/hermes-utils.rb b/packages/react-native/sdks/hermes-engine/hermes-utils.rb index b984655216e6..bbeeefb41d7a 100644 --- a/packages/react-native/sdks/hermes-engine/hermes-utils.rb +++ b/packages/react-native/sdks/hermes-engine/hermes-utils.rb @@ -3,6 +3,8 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. +require 'digest' + HERMES_GITHUB_URL = "https://github.com/facebook/hermes.git" ENV_BUILD_FROM_SOURCE = "RCT_BUILD_HERMES_FROM_SOURCE" @@ -210,6 +212,27 @@ def shared_cache_dir() return File.join(Dir.home, "Library", "Caches", "ReactNative") end +def fetch_maven_sha1(tarball_url) + sha1 = `curl -sL "#{tarball_url}.sha1"`.strip + return sha1.downcase if $?.success? && sha1.match?(/\A[a-fA-F0-9]{40}\z/) + nil +end + +def validate_tarball(path, tarball_url) + expected_sha1 = fetch_maven_sha1(tarball_url) + if expected_sha1.nil? + hermes_log("SHA1 not available from Maven for #{File.basename(path)}. Skipping validation.", :info) + return true + end + actual_sha1 = Digest::SHA1.file(path).hexdigest + if actual_sha1 == expected_sha1 + hermes_log("SHA1 verified for #{File.basename(path)}", :info) + return true + end + hermes_log("SHA1 mismatch for #{File.basename(path)}: expected #{expected_sha1}, got #{actual_sha1}", :error) + return false +end + def download_hermes_tarball(react_native_path, tarball_url, version, configuration) filename = configuration == nil ? "hermes-ios-#{version}.tar.gz" : @@ -224,18 +247,26 @@ def download_hermes_tarball(react_native_path, tarball_url, version, configurati `mkdir -p "#{artifacts_dir()}"` cached_path = File.join(shared_cache_dir(), filename) - if File.exist?(cached_path) + if File.exist?(cached_path) && validate_tarball(cached_path, tarball_url) hermes_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})", :info) FileUtils.cp(cached_path, destination_path) else - hermes_log("Cache miss: downloading #{filename} from #{tarball_url}", :info) + if File.exist?(cached_path) + hermes_log("Shared cache file #{filename} failed SHA verification. Re-downloading.", :info) + else + hermes_log("Cache miss: downloading #{filename} from #{tarball_url}", :info) + end # Download to a temporary file first so we don't cache incomplete downloads. tmp_file = "#{artifacts_dir()}/hermes-ios.download" `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` - # Save to shared cache for future use - `mkdir -p "#{shared_cache_dir()}"` - FileUtils.cp(destination_path, cached_path) - hermes_log("Saved #{filename} to shared cache (#{shared_cache_dir()})", :info) + if validate_tarball(destination_path, tarball_url) + # Save to shared cache for future use + `mkdir -p "#{shared_cache_dir()}"` + FileUtils.cp(destination_path, cached_path) + hermes_log("Saved #{filename} to shared cache (#{shared_cache_dir()})", :info) + else + hermes_log("Downloaded file #{filename} failed SHA verification!", :error) + end end return destination_path From 53e690834acc1410e76e3b903a973d8e96f3427f Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Fri, 15 May 2026 16:16:09 +0100 Subject: [PATCH 3/4] Extract shared cache and SHA validation into ReactNativePodsUtils Moves shared_cache_dir, fetch_maven_sha1, and validate_tarball into ReactNativePodsUtils in utils.rb to eliminate duplication across hermes-utils.rb, rndependencies.rb, and rncore.rb. --- .../react-native/scripts/cocoapods/rncore.rb | 38 +++-------------- .../scripts/cocoapods/rndependencies.rb | 38 +++-------------- .../react-native/scripts/cocoapods/utils.rb | 42 +++++++++++++++++++ .../sdks/hermes-engine/hermes-utils.rb | 39 ++++------------- 4 files changed, 61 insertions(+), 96 deletions(-) diff --git a/packages/react-native/scripts/cocoapods/rncore.rb b/packages/react-native/scripts/cocoapods/rncore.rb index 49172c5a87ed..9e488b346697 100644 --- a/packages/react-native/scripts/cocoapods/rncore.rb +++ b/packages/react-native/scripts/cocoapods/rncore.rb @@ -6,7 +6,6 @@ require 'json' require 'net/http' require 'rexml/document' -require 'digest' require_relative './utils.rb' @@ -406,31 +405,6 @@ def self.download_nightly_rncore(react_native_path, version, configuration, dsym download_rncore_tarball(react_native_path, tarball_url, version, configuration, dsyms) end - def self.shared_cache_dir() - return File.join(Dir.home, "Library", "Caches", "ReactNative") - end - - def self.fetch_maven_sha1(tarball_url) - sha1 = `curl -sL "#{tarball_url}.sha1"`.strip - return sha1.downcase if $?.success? && sha1.match?(/\A[a-fA-F0-9]{40}\z/) - nil - end - - def self.validate_tarball(path, tarball_url) - expected_sha1 = fetch_maven_sha1(tarball_url) - if expected_sha1.nil? - rncore_log("SHA1 not available from Maven for #{File.basename(path)}. Skipping validation.") - return true - end - actual_sha1 = Digest::SHA1.file(path).hexdigest - if actual_sha1 == expected_sha1 - rncore_log("SHA1 verified for #{File.basename(path)}") - return true - end - rncore_log("SHA1 mismatch for #{File.basename(path)}: expected #{expected_sha1}, got #{actual_sha1}", :error) - return false - end - def self.download_rncore_tarball(react_native_path, tarball_url, version, configuration, dsyms = false) filename = configuration == nil ? "reactnative-core-#{version}#{dsyms ? "-dSYM" : ""}.tar.gz" : @@ -444,9 +418,9 @@ def self.download_rncore_tarball(react_native_path, tarball_url, version, config `mkdir -p "#{artifacts_dir()}"` - cached_path = File.join(shared_cache_dir(), filename) - if File.exist?(cached_path) && validate_tarball(cached_path, tarball_url) - rncore_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})") + cached_path = File.join(ReactNativePodsUtils.shared_cache_dir(), filename) + if File.exist?(cached_path) && ReactNativePodsUtils.validate_tarball(cached_path, tarball_url) + rncore_log("Cache hit: copying #{filename} from shared cache (#{ReactNativePodsUtils.shared_cache_dir()})") FileUtils.cp(cached_path, destination_path) else if File.exist?(cached_path) @@ -457,11 +431,11 @@ def self.download_rncore_tarball(react_native_path, tarball_url, version, config # Download to a temporary file first so we don't cache incomplete downloads. tmp_file = "#{artifacts_dir()}/reactnative-core.download" `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` - if validate_tarball(destination_path, tarball_url) + if ReactNativePodsUtils.validate_tarball(destination_path, tarball_url) # Save to shared cache for future use - `mkdir -p "#{shared_cache_dir()}"` + `mkdir -p "#{ReactNativePodsUtils.shared_cache_dir()}"` FileUtils.cp(destination_path, cached_path) - rncore_log("Saved #{filename} to shared cache (#{shared_cache_dir()})") + rncore_log("Saved #{filename} to shared cache (#{ReactNativePodsUtils.shared_cache_dir()})") else rncore_log("Downloaded file #{filename} failed SHA verification!", :error) end diff --git a/packages/react-native/scripts/cocoapods/rndependencies.rb b/packages/react-native/scripts/cocoapods/rndependencies.rb index 0d7f592b53cb..179ed9ad4af2 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 'digest' require_relative './utils.rb' @@ -232,31 +231,6 @@ def self.podspec_source_download_prebuilt_nightly_tarball(version) return {:http => url } end - def self.shared_cache_dir() - return File.join(Dir.home, "Library", "Caches", "ReactNative") - end - - def self.fetch_maven_sha1(tarball_url) - sha1 = `curl -sL "#{tarball_url}.sha1"`.strip - return sha1.downcase if $?.success? && sha1.match?(/\A[a-fA-F0-9]{40}\z/) - nil - end - - def self.validate_tarball(path, tarball_url) - expected_sha1 = fetch_maven_sha1(tarball_url) - if expected_sha1.nil? - rndeps_log("SHA1 not available from Maven for #{File.basename(path)}. Skipping validation.") - return true - end - actual_sha1 = Digest::SHA1.file(path).hexdigest - if actual_sha1 == expected_sha1 - rndeps_log("SHA1 verified for #{File.basename(path)}") - return true - end - rndeps_log("SHA1 mismatch for #{File.basename(path)}: expected #{expected_sha1}, got #{actual_sha1}", :error) - return false - end - def self.download_rndeps_tarball(react_native_path, tarball_url, version, configuration) filename = configuration == nil ? "reactnative-dependencies-#{version}.tar.gz" : @@ -270,9 +244,9 @@ def self.download_rndeps_tarball(react_native_path, tarball_url, version, config `mkdir -p "#{artifacts_dir()}"` - cached_path = File.join(shared_cache_dir(), filename) - if File.exist?(cached_path) && validate_tarball(cached_path, tarball_url) - rndeps_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})") + cached_path = File.join(ReactNativePodsUtils.shared_cache_dir(), filename) + if File.exist?(cached_path) && ReactNativePodsUtils.validate_tarball(cached_path, tarball_url) + rndeps_log("Cache hit: copying #{filename} from shared cache (#{ReactNativePodsUtils.shared_cache_dir()})") FileUtils.cp(cached_path, destination_path) else if File.exist?(cached_path) @@ -283,11 +257,11 @@ def self.download_rndeps_tarball(react_native_path, tarball_url, version, config # Download to a temporary file first so we don't cache incomplete downloads. tmp_file = "#{artifacts_dir()}/reactnative-dependencies.download" `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` - if validate_tarball(destination_path, tarball_url) + if ReactNativePodsUtils.validate_tarball(destination_path, tarball_url) # Save to shared cache for future use - `mkdir -p "#{shared_cache_dir()}"` + `mkdir -p "#{ReactNativePodsUtils.shared_cache_dir()}"` FileUtils.cp(destination_path, cached_path) - rndeps_log("Saved #{filename} to shared cache (#{shared_cache_dir()})") + rndeps_log("Saved #{filename} to shared cache (#{ReactNativePodsUtils.shared_cache_dir()})") else rndeps_log("Downloaded file #{filename} failed SHA verification!", :error) end diff --git a/packages/react-native/scripts/cocoapods/utils.rb b/packages/react-native/scripts/cocoapods/utils.rb index 7901e8135ab1..5adf880e4ba1 100644 --- a/packages/react-native/scripts/cocoapods/utils.rb +++ b/packages/react-native/scripts/cocoapods/utils.rb @@ -4,6 +4,7 @@ # LICENSE file in the root directory of this source tree. require 'shellwords' +require 'digest' require_relative "./helpers.rb" require_relative "./jsengine.rb" @@ -724,4 +725,45 @@ def self.resolve_use_frameworks(spec, header_mappings_dir: nil, module_name: nil spec.header_mappings_dir = header_mappings_dir end end + + # ==================== # + # Shared download cache # + # ==================== # + + def self.shared_cache_dir() + return File.join(Dir.home, "Library", "Caches", "ReactNative") + end + + def self.fetch_maven_sha1(tarball_url) + sha1 = `curl -sL "#{tarball_url}.sha1"`.strip + return sha1.downcase if $?.success? && sha1.match?(/\A[a-fA-F0-9]{40}\z/) + nil + end + + def self.validate_tarball(path, tarball_url) + expected_sha1 = fetch_maven_sha1(tarball_url) + basename = File.basename(path) + if expected_sha1.nil? + cache_log("SHA1 not available from Maven for #{basename}. Skipping validation.") + return true + end + actual_sha1 = Digest::SHA1.file(path).hexdigest + if actual_sha1 == expected_sha1 + cache_log("SHA1 verified for #{basename}") + return true + end + cache_log("SHA1 mismatch for #{basename}: expected #{expected_sha1}, got #{actual_sha1}", :error) + return false + end + + def self.cache_log(message, level = :info) + return unless Object.const_defined?("Pod::UI") + prefix = '[Cache] ' + case level + when :error + Pod::UI.puts prefix.red + message + else + Pod::UI.puts prefix.green + message + end + end end diff --git a/packages/react-native/sdks/hermes-engine/hermes-utils.rb b/packages/react-native/sdks/hermes-engine/hermes-utils.rb index bbeeefb41d7a..b886599af539 100644 --- a/packages/react-native/sdks/hermes-engine/hermes-utils.rb +++ b/packages/react-native/sdks/hermes-engine/hermes-utils.rb @@ -3,7 +3,7 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. -require 'digest' +require_relative '../../scripts/cocoapods/utils' HERMES_GITHUB_URL = "https://github.com/facebook/hermes.git" ENV_BUILD_FROM_SOURCE = "RCT_BUILD_HERMES_FROM_SOURCE" @@ -208,31 +208,6 @@ def download_stable_hermes(react_native_path, version, configuration) download_hermes_tarball(react_native_path, tarball_url, version, configuration) end -def shared_cache_dir() - return File.join(Dir.home, "Library", "Caches", "ReactNative") -end - -def fetch_maven_sha1(tarball_url) - sha1 = `curl -sL "#{tarball_url}.sha1"`.strip - return sha1.downcase if $?.success? && sha1.match?(/\A[a-fA-F0-9]{40}\z/) - nil -end - -def validate_tarball(path, tarball_url) - expected_sha1 = fetch_maven_sha1(tarball_url) - if expected_sha1.nil? - hermes_log("SHA1 not available from Maven for #{File.basename(path)}. Skipping validation.", :info) - return true - end - actual_sha1 = Digest::SHA1.file(path).hexdigest - if actual_sha1 == expected_sha1 - hermes_log("SHA1 verified for #{File.basename(path)}", :info) - return true - end - hermes_log("SHA1 mismatch for #{File.basename(path)}: expected #{expected_sha1}, got #{actual_sha1}", :error) - return false -end - def download_hermes_tarball(react_native_path, tarball_url, version, configuration) filename = configuration == nil ? "hermes-ios-#{version}.tar.gz" : @@ -246,9 +221,9 @@ def download_hermes_tarball(react_native_path, tarball_url, version, configurati `mkdir -p "#{artifacts_dir()}"` - cached_path = File.join(shared_cache_dir(), filename) - if File.exist?(cached_path) && validate_tarball(cached_path, tarball_url) - hermes_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})", :info) + cached_path = File.join(ReactNativePodsUtils.shared_cache_dir(), filename) + if File.exist?(cached_path) && ReactNativePodsUtils.validate_tarball(cached_path, tarball_url) + hermes_log("Cache hit: copying #{filename} from shared cache (#{ReactNativePodsUtils.shared_cache_dir()})", :info) FileUtils.cp(cached_path, destination_path) else if File.exist?(cached_path) @@ -259,11 +234,11 @@ def download_hermes_tarball(react_native_path, tarball_url, version, configurati # Download to a temporary file first so we don't cache incomplete downloads. tmp_file = "#{artifacts_dir()}/hermes-ios.download" `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` - if validate_tarball(destination_path, tarball_url) + if ReactNativePodsUtils.validate_tarball(destination_path, tarball_url) # Save to shared cache for future use - `mkdir -p "#{shared_cache_dir()}"` + `mkdir -p "#{ReactNativePodsUtils.shared_cache_dir()}"` FileUtils.cp(destination_path, cached_path) - hermes_log("Saved #{filename} to shared cache (#{shared_cache_dir()})", :info) + hermes_log("Saved #{filename} to shared cache (#{ReactNativePodsUtils.shared_cache_dir()})", :info) else hermes_log("Downloaded file #{filename} failed SHA verification!", :error) end From 3aabd20f26c581f934a842d1f0304d494da93923 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Fri, 15 May 2026 16:34:34 +0100 Subject: [PATCH 4/4] Keep hermes-utils.rb self-contained and add verification logs hermes-utils.rb cannot use require_relative to utils.rb because the folder structure differs between the monorepo and published npm package. Instead, hermes-utils.rb gets its own copy of shared_cache_dir, fetch_maven_sha1, and validate_hermes_tarball. Also adds explicit "Verifying checksum for cached/downloaded ..." log lines in all three download functions so the verification step is visible in pod install output. --- .../react-native/scripts/cocoapods/rncore.rb | 25 +++++--- .../scripts/cocoapods/rndependencies.rb | 25 +++++--- .../sdks/hermes-engine/hermes-utils.rb | 61 +++++++++++++++---- 3 files changed, 85 insertions(+), 26 deletions(-) diff --git a/packages/react-native/scripts/cocoapods/rncore.rb b/packages/react-native/scripts/cocoapods/rncore.rb index 9e488b346697..d3bba6274391 100644 --- a/packages/react-native/scripts/cocoapods/rncore.rb +++ b/packages/react-native/scripts/cocoapods/rncore.rb @@ -419,18 +419,29 @@ def self.download_rncore_tarball(react_native_path, tarball_url, version, config `mkdir -p "#{artifacts_dir()}"` cached_path = File.join(ReactNativePodsUtils.shared_cache_dir(), filename) - if File.exist?(cached_path) && ReactNativePodsUtils.validate_tarball(cached_path, tarball_url) - rncore_log("Cache hit: copying #{filename} from shared cache (#{ReactNativePodsUtils.shared_cache_dir()})") - FileUtils.cp(cached_path, destination_path) - else - if File.exist?(cached_path) - rncore_log("Shared cache file #{filename} failed SHA verification. Re-downloading.") + if File.exist?(cached_path) + rncore_log("Verifying checksum for cached #{filename}...") + if ReactNativePodsUtils.validate_tarball(cached_path, tarball_url) + rncore_log("Cache hit: copying #{filename} from shared cache (#{ReactNativePodsUtils.shared_cache_dir()})") + FileUtils.cp(cached_path, destination_path) else - rncore_log("Cache miss: downloading #{filename} from #{tarball_url}") + rncore_log("Shared cache file #{filename} failed SHA verification. Re-downloading.") + tmp_file = "#{artifacts_dir()}/reactnative-core.download" + `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` + rncore_log("Verifying checksum for downloaded #{filename}...") + if ReactNativePodsUtils.validate_tarball(destination_path, tarball_url) + FileUtils.cp(destination_path, cached_path) + rncore_log("Saved #{filename} to shared cache (#{ReactNativePodsUtils.shared_cache_dir()})") + else + rncore_log("Downloaded file #{filename} failed SHA verification!", :error) + end end + else + rncore_log("Cache miss: downloading #{filename} from #{tarball_url}") # Download to a temporary file first so we don't cache incomplete downloads. tmp_file = "#{artifacts_dir()}/reactnative-core.download" `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` + rncore_log("Verifying checksum for downloaded #{filename}...") if ReactNativePodsUtils.validate_tarball(destination_path, tarball_url) # Save to shared cache for future use `mkdir -p "#{ReactNativePodsUtils.shared_cache_dir()}"` diff --git a/packages/react-native/scripts/cocoapods/rndependencies.rb b/packages/react-native/scripts/cocoapods/rndependencies.rb index 179ed9ad4af2..ce8c91ed6f9b 100644 --- a/packages/react-native/scripts/cocoapods/rndependencies.rb +++ b/packages/react-native/scripts/cocoapods/rndependencies.rb @@ -245,18 +245,29 @@ def self.download_rndeps_tarball(react_native_path, tarball_url, version, config `mkdir -p "#{artifacts_dir()}"` cached_path = File.join(ReactNativePodsUtils.shared_cache_dir(), filename) - if File.exist?(cached_path) && ReactNativePodsUtils.validate_tarball(cached_path, tarball_url) - rndeps_log("Cache hit: copying #{filename} from shared cache (#{ReactNativePodsUtils.shared_cache_dir()})") - FileUtils.cp(cached_path, destination_path) - else - if File.exist?(cached_path) - rndeps_log("Shared cache file #{filename} failed SHA verification. Re-downloading.") + if File.exist?(cached_path) + rndeps_log("Verifying checksum for cached #{filename}...") + if ReactNativePodsUtils.validate_tarball(cached_path, tarball_url) + rndeps_log("Cache hit: copying #{filename} from shared cache (#{ReactNativePodsUtils.shared_cache_dir()})") + FileUtils.cp(cached_path, destination_path) else - rndeps_log("Cache miss: downloading #{filename} from #{tarball_url}") + rndeps_log("Shared cache file #{filename} failed SHA verification. Re-downloading.") + tmp_file = "#{artifacts_dir()}/reactnative-dependencies.download" + `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` + rndeps_log("Verifying checksum for downloaded #{filename}...") + if ReactNativePodsUtils.validate_tarball(destination_path, tarball_url) + FileUtils.cp(destination_path, cached_path) + rndeps_log("Saved #{filename} to shared cache (#{ReactNativePodsUtils.shared_cache_dir()})") + else + rndeps_log("Downloaded file #{filename} failed SHA verification!", :error) + end end + else + rndeps_log("Cache miss: downloading #{filename} from #{tarball_url}") # Download to a temporary file first so we don't cache incomplete downloads. tmp_file = "#{artifacts_dir()}/reactnative-dependencies.download" `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` + rndeps_log("Verifying checksum for downloaded #{filename}...") if ReactNativePodsUtils.validate_tarball(destination_path, tarball_url) # Save to shared cache for future use `mkdir -p "#{ReactNativePodsUtils.shared_cache_dir()}"` diff --git a/packages/react-native/sdks/hermes-engine/hermes-utils.rb b/packages/react-native/sdks/hermes-engine/hermes-utils.rb index b886599af539..d4d4b0acfdfd 100644 --- a/packages/react-native/sdks/hermes-engine/hermes-utils.rb +++ b/packages/react-native/sdks/hermes-engine/hermes-utils.rb @@ -3,7 +3,7 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. -require_relative '../../scripts/cocoapods/utils' +require 'digest' HERMES_GITHUB_URL = "https://github.com/facebook/hermes.git" ENV_BUILD_FROM_SOURCE = "RCT_BUILD_HERMES_FROM_SOURCE" @@ -208,6 +208,32 @@ def download_stable_hermes(react_native_path, version, configuration) download_hermes_tarball(react_native_path, tarball_url, version, configuration) end +def shared_cache_dir() + return File.join(Dir.home, "Library", "Caches", "ReactNative") +end + +def fetch_maven_sha1(tarball_url) + sha1 = `curl -sL "#{tarball_url}.sha1"`.strip + return sha1.downcase if $?.success? && sha1.match?(/\A[a-fA-F0-9]{40}\z/) + nil +end + +def validate_hermes_tarball(path, tarball_url) + expected_sha1 = fetch_maven_sha1(tarball_url) + basename = File.basename(path) + if expected_sha1.nil? + hermes_log("SHA1 not available from Maven for #{basename}. Skipping validation.", :info) + return true + end + actual_sha1 = Digest::SHA1.file(path).hexdigest + if actual_sha1 == expected_sha1 + hermes_log("SHA1 verified for #{basename}", :info) + return true + end + hermes_log("SHA1 mismatch for #{basename}: expected #{expected_sha1}, got #{actual_sha1}", :error) + return false +end + def download_hermes_tarball(react_native_path, tarball_url, version, configuration) filename = configuration == nil ? "hermes-ios-#{version}.tar.gz" : @@ -221,24 +247,35 @@ def download_hermes_tarball(react_native_path, tarball_url, version, configurati `mkdir -p "#{artifacts_dir()}"` - cached_path = File.join(ReactNativePodsUtils.shared_cache_dir(), filename) - if File.exist?(cached_path) && ReactNativePodsUtils.validate_tarball(cached_path, tarball_url) - hermes_log("Cache hit: copying #{filename} from shared cache (#{ReactNativePodsUtils.shared_cache_dir()})", :info) - FileUtils.cp(cached_path, destination_path) - else - if File.exist?(cached_path) - hermes_log("Shared cache file #{filename} failed SHA verification. Re-downloading.", :info) + cached_path = File.join(shared_cache_dir(), filename) + if File.exist?(cached_path) + hermes_log("Verifying checksum for cached #{filename}...", :info) + if validate_hermes_tarball(cached_path, tarball_url) + hermes_log("Cache hit: copying #{filename} from shared cache (#{shared_cache_dir()})", :info) + FileUtils.cp(cached_path, destination_path) else - hermes_log("Cache miss: downloading #{filename} from #{tarball_url}", :info) + hermes_log("Shared cache file #{filename} failed SHA verification. Re-downloading.", :info) + tmp_file = "#{artifacts_dir()}/hermes-ios.download" + `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` + hermes_log("Verifying checksum for downloaded #{filename}...", :info) + if validate_hermes_tarball(destination_path, tarball_url) + FileUtils.cp(destination_path, cached_path) + hermes_log("Saved #{filename} to shared cache (#{shared_cache_dir()})", :info) + else + hermes_log("Downloaded file #{filename} failed SHA verification!", :error) + end end + else + hermes_log("Cache miss: downloading #{filename} from #{tarball_url}", :info) # Download to a temporary file first so we don't cache incomplete downloads. tmp_file = "#{artifacts_dir()}/hermes-ios.download" `curl "#{tarball_url}" -Lo "#{tmp_file}" && mv "#{tmp_file}" "#{destination_path}"` - if ReactNativePodsUtils.validate_tarball(destination_path, tarball_url) + hermes_log("Verifying checksum for downloaded #{filename}...", :info) + if validate_hermes_tarball(destination_path, tarball_url) # Save to shared cache for future use - `mkdir -p "#{ReactNativePodsUtils.shared_cache_dir()}"` + `mkdir -p "#{shared_cache_dir()}"` FileUtils.cp(destination_path, cached_path) - hermes_log("Saved #{filename} to shared cache (#{ReactNativePodsUtils.shared_cache_dir()})", :info) + hermes_log("Saved #{filename} to shared cache (#{shared_cache_dir()})", :info) else hermes_log("Downloaded file #{filename} failed SHA verification!", :error) end