Skip to content

Commit e325575

Browse files
chrfalchreact-native-bot
authored andcommitted
feat(iOS): serve third-party deps from the prebuilt ReactNativeDependencies pod via dependency-only facades (#57440)
Summary: Step 1 of making the prebuilt `ReactNativeDependencies` pod the **single header authority** for the third-party C/C++ deps (RCT-Folly, glog, boost, DoubleConversion, fmt, fast_float, SocketRocket) in prebuilt-deps mode. Today the deps **binary** replaces the source pods' code, but the pod still `s.dependency`'s the real source pods and borrows their headers via `$(PODS_ROOT)/<pod>` search paths. That split header authority is the dual-copy bug class behind the 2026-07-03 SocketRocket regression (`duplicate interface` under `use_frameworks!` — SocketRocket's ObjC headers have no include guards). Three commits: 1. **fix(cocoapods): harden prebuilt-deps header search paths and artifact handling** — `rndependencies.rb`'s `||= [] << path` only added the deps header search path when `HEADER_SEARCH_PATHS` was unset (silently dropped otherwise); normalize and always append, and point at the pod-local flattened `Headers/`. `ReactNativeDependencies.podspec` `prepare_command` now fails closed (`exit 1`) instead of silently producing a no-link pod. `reactNativeDependencies.js` no longer deletes + re-downloads a locally staged artifact that lacks a version marker. 2. **feat(cocoapods): dependency-only facades for third-party pods in prebuilt-deps mode** — in prebuilt-deps mode the real source pods are not declared, so a community podspec's hardcoded `s.dependency "RCT-Folly"` would resolve from trunk and compile from source next to the prebuilt binary. `RNDepsFacades` generates dependency-only local facade podspecs (no sources, no headers, a single dependency on `ReactNativeDependencies`); versions/subspecs are derived from the real podspecs in `third-party-podspecs/` (SocketRocket synthesized fail-closed from `socket_rocket_config`). Full contract documented in `scripts/cocoapods/__docs__/prebuilt-deps.md`. 3. **feat(ios-prebuild): SocketRocket privacy manifest + Xcode 26 header layout** — embed an RN-authored, accurate-empty `PrivacyInfo.xcprivacy` for SocketRocket (upstream ships none), and stage flat public headers into `include/` so Xcode 26's SwiftPM accepts the header layout. Stacked on #57305 (base: `chrfalch/prebuilt-resources`); the SwiftPM preview (#57332) rebases on top of this. Follow-up (separate PR): headers-only `ReactNativeDependenciesHeaders.xcframework` sidecar so SPM auto-serves the deps namespaces and `ReactNativeHeaders` goes pure-RN. ## Changelog: [IOS][CHANGED] - Prebuilt-deps mode: serve third-party headers from the ReactNativeDependencies pod itself and resolve community `s.dependency` on RCT-Folly/glog/boost/etc. via dependency-only facade pods Pull Request resolved: #57440 Test Plan: E2E matrix (2026-07-06, locally built deps artifact via `prepare-ios-prebuilds.js`): - rn-tester, prebuilt core + prebuilt deps, static linkage — builds - rn-tester, prebuilt core + prebuilt deps, `USE_FRAMEWORKS=dynamic` — builds (the SocketRocket-regression config) - helloworld (private), prebuilt core + prebuilt deps, static — builds - source-mode control: no facades generated, `Podfile.lock` identical to baseline 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed By: fabriziocucci Differential Revision: D111449257 Pulled By: cipolleschi fbshipit-source-id: ace5716868d126a08721200efd640e903b191658
1 parent 8959ebd commit e325575

8 files changed

Lines changed: 332 additions & 9 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Prebuilt ReactNativeDependencies — self-serving headers + deps facades
2+
3+
How the third-party C/C++ deps (`RCT-Folly`, `glog`, `boost`,
4+
`DoubleConversion`, `fmt`, `fast_float`, `SocketRocket`) are served when
5+
`ReactNativeDependenciesUtils.build_react_native_deps_from_source()` is false
6+
(prebuilt-deps mode). Source-deps mode is unaffected by everything below.
7+
8+
## Pod-served headers, CocoaPods only (`rndependencies.rb`)
9+
10+
In prebuilt-deps mode the `ReactNativeDependencies` POD (CocoaPods) is the
11+
single authority for the third-party deps: compiled code lives in its
12+
xcframework binary, and the artifact's own
13+
`Headers/{folly,glog,boost,fmt,double-conversion,fast_float,SocketRocket}` are
14+
flattened into the pod's `Headers/` by the podspec's `prepare_command`.
15+
Consumers resolve bare `<folly/...>` / `<SocketRocket/...>` via CocoaPods
16+
public-header linkage from `s.dependency "ReactNativeDependencies"`, plus an
17+
explicit `HEADER_SEARCH_PATHS` entry
18+
(`$(PODS_ROOT)/ReactNativeDependencies/Headers`). The real source pods are
19+
neither depended on nor searched.
20+
21+
NOTE: this is a CocoaPods-level contract. The deps XCFRAMEWORK itself is NOT
22+
self-serving: it is framework-type without `HeadersPath`, so its root `Headers/`
23+
is invisible to SPM binaryTargets (verified 2026-07-04 — `HeadersPath` is
24+
rejected on framework entries). In SPM the six C++ namespaces are served by
25+
ReactNativeHeaders.xcframework; serving them from the deps side requires the
26+
phase-2 headers-only library sidecar (see rn-deps-self-serving plan).
27+
28+
## Why SocketRocket is vended here
29+
30+
React-Core compiled from source (source-core + prebuilt-deps mix) imports
31+
`<SocketRocket/SRWebSocket.h>` (`RCTReconnectingWebSocket.m`), and in
32+
prebuilt-deps mode there is NO real SocketRocket pod in the graph — the artifact
33+
is the sole supplier. This does not reintroduce the 2026-07-03 dual-copy
34+
regression: that bug relocated SocketRocket copies onto every pod's search path
35+
(via ReactNativeHeaders → React-Core-prebuilt) while a REAL SocketRocket pod
36+
coexisted. Here there is exactly one physical copy and no coexisting pod.
37+
38+
## Deps facades (`rndeps_facades.rb`, declared in `react_native_pods.rb`)
39+
40+
The real source pods are only declared in the deps-from-source branch, so in
41+
prebuilt-deps mode a community podspec's hardcoded `s.dependency "RCT-Folly"` /
42+
`"RCT-Folly/Fabric"` / `"glog"` would resolve from the CocoaPods trunk and
43+
compile from source next to the prebuilt binary. `RNDepsFacades` generates
44+
dependency-only facade podspecs (`build/rndeps-facades/<Name>/`), installed as
45+
LOCAL pods (`:path`, so Podfile-local resolution beats trunk, nothing fetched):
46+
no sources, no headers, single dependency on `ReactNativeDependencies`.
47+
Versions + subspecs are DERIVED from the real podspecs in
48+
`third-party-podspecs/` (RCT-Folly keeps `/Default` + `/Fabric`,
49+
`default_subspecs = ["Default"]`). SocketRocket has no local podspec — its
50+
facade version is SYNTHESIZED from
51+
`Helpers::Constants::socket_rocket_config[:version]`, fail-closed if absent.
52+
`:modular_headers` is intentionally dropped on facade declarations: a
53+
dependency-only placeholder builds no module; consumers get modules from
54+
`ReactNativeDependencies`.
55+
56+
## Mode × supplier table
57+
58+
| core × deps | real 3P pods in graph | SocketRocket headers supplier |
59+
| ------------------- | ----------------------------------------------- | ------------------------------- |
60+
| source + source | yes (`react_native_pods.rb` deps-source branch) | real pod |
61+
| source + prebuilt | no | RNDeps artifact (sole supplier) |
62+
| prebuilt + source | yes | real pod |
63+
| prebuilt + prebuilt | no | RNDeps artifact |
64+
65+
## SocketRocket privacy manifest
66+
67+
Upstream SocketRocket ships NO privacy manifest, and the deps artifact
68+
historically carried bundles only for boost/folly/glog. Fixed alongside this
69+
work: the deps prebuild (`scripts/releases/ios-prebuild/configuration.js`) now
70+
embeds `ReactNativeDependencies_SocketRocket.bundle/PrivacyInfo.xcprivacy`,
71+
sourced from an RN-authored manifest at
72+
`scripts/releases/ios-prebuild/resources/SocketRocket/PrivacyInfo.xcprivacy`
73+
(accurate-empty: SocketRocket uses no Required Reason APIs). Facades remain
74+
resource-free by design.

packages/react-native/scripts/cocoapods/rndependencies.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require "json"
77
require 'net/http'
88
require 'rexml/document'
9+
require 'shellwords'
910

1011
require_relative './utils.rb'
1112

@@ -36,7 +37,7 @@ def add_rn_third_party_dependencies(s)
3637
header_search_paths = current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] || []
3738

3839
if header_search_paths.is_a?(String)
39-
header_search_paths = header_search_paths.split(" ")
40+
header_search_paths = Shellwords.shellsplit(header_search_paths)
4041
end
4142

4243
header_search_paths << "$(PODS_ROOT)/glog"
@@ -47,10 +48,24 @@ def add_rn_third_party_dependencies(s)
4748
header_search_paths << "$(PODS_ROOT)/SocketRocket"
4849
header_search_paths << "$(PODS_ROOT)/RCT-Folly"
4950

50-
current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] = header_search_paths
51+
# uniq so a second call on the same spec can't duplicate entries.
52+
current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] = header_search_paths.uniq
5153
else
54+
# Prebuilt-deps mode: this pod SELF-SERVES the third-party headers from its
55+
# own xcframework (incl. SocketRocket - sole supplier in this mode). See
56+
# scripts/cocoapods/__docs__/prebuilt-deps.md for the full contract.
5257
s.dependency "ReactNativeDependencies"
53-
current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] ||= [] << "$(PODS_ROOT)/ReactNativeDependencies"
58+
59+
header_search_paths = current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] || []
60+
if header_search_paths.is_a?(String)
61+
header_search_paths = Shellwords.shellsplit(header_search_paths)
62+
end
63+
# Artifact headers are flattened into the pod-local Headers/ by the podspec
64+
# prepare_command (see __docs__/prebuilt-deps.md).
65+
header_search_paths << "$(PODS_ROOT)/ReactNativeDependencies/Headers"
66+
67+
# uniq so a second call on the same spec can't duplicate entries.
68+
current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] = header_search_paths.uniq
5469
end
5570

5671
s.pod_target_xcconfig = current_pod_target_xcconfig
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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+
require 'json'
7+
require 'fileutils'
8+
# Self-contained against require ordering: this module reads
9+
# Helpers::Constants.socket_rocket_config. react_native_pods.rb normally loads
10+
# helpers.rb first, but requiring it here (idempotent) removes that implicit
11+
# dependency. The defined? guard at the use site stays as a backstop.
12+
require_relative './helpers'
13+
14+
# Dependency-only facade podspecs for the third-party deps in prebuilt-deps
15+
# mode (deps-side analogue of RNCoreFacades). Design + rationale:
16+
# scripts/cocoapods/__docs__/prebuilt-deps.md
17+
module RNDepsFacades
18+
# The name of the umbrella prebuilt-deps pod every facade depends on. Its pod
19+
# self-serves the third-party headers + carries the binary (see
20+
# rndependencies.rb, ReactNativeDependencies.podspec).
21+
DEPS_POD = "ReactNativeDependencies"
22+
23+
# pod name => podspec path (relative to the react-native package root), or
24+
# :synthesized for a pod with no local podspec (SocketRocket). Version +
25+
# subspecs + default_subspecs are DERIVED from the real podspec where one
26+
# exists; synthesized entries derive their version from a constant.
27+
FACADE_PODS = {
28+
"RCT-Folly" => "third-party-podspecs/RCT-Folly.podspec",
29+
"glog" => "third-party-podspecs/glog.podspec",
30+
"boost" => "third-party-podspecs/boost.podspec",
31+
"DoubleConversion" => "third-party-podspecs/DoubleConversion.podspec",
32+
"fmt" => "third-party-podspecs/fmt.podspec",
33+
"fast_float" => "third-party-podspecs/fast_float.podspec",
34+
"SocketRocket" => :synthesized,
35+
}
36+
37+
# Sub-directory (relative to the install root) that holds the generated
38+
# deps facades. Kept separate from RNCoreFacades' `build/rncore-facades` so
39+
# the two families never collide.
40+
FACADE_RELDIR = File.join("build", "rndeps-facades")
41+
42+
# Generates the facade podspecs and returns the base directory holding them.
43+
# Each facade gets its OWN sub-directory containing a single
44+
# `<Name>.podspec.json`, so it can be installed as a LOCAL pod via
45+
# `:path => <dir>` (PathSource uses the spec in place and never downloads
46+
# `spec.source`). Idempotent; safe to call once per `pod install`.
47+
#
48+
# `react_native_path` locates the real third-party podspecs we mirror.
49+
# version + subspecs + default_subspecs are DERIVED from the real spec (or,
50+
# for SocketRocket, synthesized from the socket_rocket_config version) so the
51+
# facade matches the source pod's spec/subspec SHAPE. It is not fully
52+
# graph-equivalent: every derived subspec depends only on
53+
# ReactNativeDependencies, so intra-pod subspec deps (e.g. RCT-Folly/Fabric
54+
# -> RCT-Folly/Default) are not reproduced — harmless here because the deps
55+
# are all declared explicitly in react_native_pods.rb. NO source_files and
56+
# NO headers are emitted — the ReactNativeDependencies pod supplies both. A
57+
# facaded pod whose real podspec can't be read is a hard error (see
58+
# load_real_spec) — silently shipping an empty facade would hide drift.
59+
def self.generate(react_native_path, install_root, ios_version)
60+
abs_base = File.join(install_root.to_s, FACADE_RELDIR)
61+
FileUtils.mkdir_p(abs_base)
62+
FACADE_PODS.each do |name, podspec_rel_path|
63+
dir = File.join(abs_base, name)
64+
FileUtils.mkdir_p(dir)
65+
66+
if podspec_rel_path == :synthesized
67+
spec = synthesized_spec(name, ios_version)
68+
else
69+
podspec_path = File.join(react_native_path.to_s, podspec_rel_path)
70+
real = load_real_spec(podspec_path, name)
71+
spec = derived_spec(name, real, ios_version)
72+
end
73+
74+
File.write(File.join(dir, "#{name}.podspec.json"), JSON.pretty_generate(spec))
75+
end
76+
abs_base
77+
end
78+
79+
# Facade dir for `<name>`, RELATIVE to the install root — pass to `pod :path =>`.
80+
# Relative (not absolute) so the path CocoaPods records in Podfile.lock is
81+
# portable rather than machine-specific.
82+
def self.facade_path(name)
83+
File.join(FACADE_RELDIR, name)
84+
end
85+
86+
# Base spec skeleton shared by derived + synthesized facades: dependency-only,
87+
# no source_files, no headers. Depends solely on ReactNativeDependencies.
88+
def self.base_spec(name, version, ios_version)
89+
{
90+
"name" => name,
91+
"version" => version,
92+
"summary" => "Prebuilt facade for #{name} (code + headers live in #{DEPS_POD}).",
93+
"homepage" => "https://reactnative.dev/",
94+
"license" => "MIT",
95+
"authors" => "Meta Platforms, Inc. and its affiliates",
96+
"platforms" => { "ios" => ios_version },
97+
# Required podspec attribute, but never fetched: installed as a LOCAL
98+
# pod (`:path => <dir>`), which uses this spec in place and ships no
99+
# source_files. Placeholder only.
100+
"source" => { "git" => "https://github.com/facebook/react-native.git" },
101+
"dependencies" => { DEPS_POD => [] },
102+
}
103+
end
104+
private_class_method :base_spec
105+
106+
# Facade derived from a real third-party podspec: version + subspecs +
107+
# default_subspecs mirror the real spec so a bare `pod '<Name>'` and any
108+
# `pod '<Name>/<Subspec>'` resolve to the SAME graph (e.g. RCT-Folly's
109+
# bare + /Default + /Fabric). Each subspec is also dependency-only and
110+
# depends on ReactNativeDependencies.
111+
#
112+
# NOTE: resources (e.g. RCT-Folly's PrivacyInfo.xcprivacy) are intentionally
113+
# NOT carried. In prebuilt-deps mode the third-party code — and its privacy
114+
# manifest — is embedded in the ReactNativeDependencies artifact; the facade
115+
# only needs to declare the dependency (see the design note in the PR).
116+
def self.derived_spec(name, real, ios_version)
117+
spec = base_spec(name, real.version.to_s, ios_version)
118+
119+
defaults = Array(real.default_subspecs)
120+
spec["default_subspecs"] = defaults unless defaults.empty?
121+
122+
subspecs = derive_subspecs(real)
123+
unless subspecs.empty?
124+
spec["subspecs"] = subspecs.map do |ss|
125+
{ "name" => ss, "dependencies" => { DEPS_POD => [] } }
126+
end
127+
end
128+
129+
spec
130+
end
131+
private_class_method :derived_spec
132+
133+
# Facade synthesized for a pod with NO local podspec (SocketRocket, a trunk
134+
# pod). Version comes from Helpers::Constants::socket_rocket_config; no
135+
# subspecs. A missing/blank constant is a hard error rather than a silent
136+
# versionless facade — a bare `pod 'SocketRocket'` in the source path is
137+
# `"~> #{socket_rocket_config[:version]}"`, so the facade MUST carry a version
138+
# that satisfies that constraint.
139+
def self.synthesized_spec(name, ios_version)
140+
version = synthesized_version(name)
141+
base_spec(name, version, ios_version)
142+
end
143+
private_class_method :synthesized_spec
144+
145+
# Resolves the synthesized version for a no-podspec facade. Fail-closed on a
146+
# missing constant/version. Only SocketRocket is synthesized today.
147+
def self.synthesized_version(name)
148+
case name
149+
when "SocketRocket"
150+
unless defined?(Helpers::Constants) && Helpers::Constants.respond_to?(:socket_rocket_config)
151+
raise "[RNDepsFacades] Cannot synthesize facade for '#{name}': " \
152+
"Helpers::Constants.socket_rocket_config is unavailable."
153+
end
154+
version = Helpers::Constants.socket_rocket_config[:version]
155+
if version.nil? || version.to_s.strip.empty?
156+
raise "[RNDepsFacades] Cannot synthesize facade for '#{name}': " \
157+
"socket_rocket_config[:version] is missing or empty."
158+
end
159+
version.to_s
160+
else
161+
raise "[RNDepsFacades] No synthesized version rule for facaded pod '#{name}'. " \
162+
"Add one to synthesized_version or give it a real podspec in FACADE_PODS."
163+
end
164+
end
165+
private_class_method :synthesized_version
166+
167+
# Loads the real podspec so we can mirror its structure. A facaded pod with a
168+
# declared podspec path MUST have a readable real podspec — if it's missing or
169+
# unparseable we raise rather than ship an empty facade (which would silently
170+
# drop subspecs / the version, the very drift this mechanism prevents).
171+
def self.load_real_spec(path, name)
172+
unless File.exist?(path)
173+
raise "[RNDepsFacades] Real podspec for facaded pod '#{name}' not found at #{path}. " \
174+
"Update FACADE_PODS in rndeps_facades.rb if the podspec moved."
175+
end
176+
begin
177+
Pod::Specification.from_file(path)
178+
rescue => e
179+
raise "[RNDepsFacades] Failed to read real podspec for facaded pod '#{name}' at #{path}: #{e.message}"
180+
end
181+
end
182+
private_class_method :load_real_spec
183+
184+
# Library (non-test, non-app) subspec names of the real spec, so third-party
185+
# libs depending on `<pod>/<subspec>` (e.g. `RCT-Folly/Fabric`) keep
186+
# resolving. Derived, never hand-listed.
187+
def self.derive_subspecs(real)
188+
real.subspecs
189+
.reject { |ss| ss.test_specification? || (ss.respond_to?(:app_specification?) && ss.app_specification?) }
190+
.map(&:base_name)
191+
end
192+
private_class_method :derive_subspecs
193+
end

packages/react-native/scripts/ios-prebuild/reactNativeDependencies.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ function checkExistingVersion(
161161
dependencyLog(
162162
`React Native Dependencies found on disk at: ${artifactsPath}.\nNo version file has been found. We are going to use it anyway, but there might be some unexpected behaviors.`,
163163
);
164+
// Honor the message above: an artifact without a version marker is a
165+
// locally-staged one (e.g. a freshly composed deps build). Use it as-is.
166+
// NOTE: this returns BEFORE the version.txt write below, so a
167+
// locally-staged artifact never gains a marker — every later run re-hits
168+
// this branch. That is intentional (don't clobber a hand-staged build),
169+
// but downstream code must not assume version.txt exists here.
170+
return true;
164171
}
165172
} else {
166173
dependencyLog('React Native Dependencies not found on disk');

packages/react-native/scripts/react_native_pods.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
require_relative './cocoapods/spm.rb'
2323
require_relative './cocoapods/rncore.rb'
2424
require_relative './cocoapods/rncore_facades.rb'
25+
require_relative './cocoapods/rndeps_facades.rb'
2526
# Importing to expose use_native_modules!
2627
require_relative './cocoapods/autolinking.rb'
2728

@@ -243,6 +244,17 @@ def use_react_native! (
243244
ReactNativeCoreUtils.rncore_log("Using React Native Core and React Native Dependencies prebuilt versions.")
244245
pod 'ReactNativeDependencies', :podspec => "#{prefix}/third-party-podspecs/ReactNativeDependencies.podspec", :modular_headers => true
245246

247+
# Facades: community pods' hardcoded s.dependency "RCT-Folly"/"glog"/... must
248+
# resolve locally instead of from trunk. See __docs__/prebuilt-deps.md.
249+
RNDepsFacades.generate(react_native_path, Pod::Config.instance.installation_root, min_ios_version_supported)
250+
pod 'DoubleConversion', :path => RNDepsFacades.facade_path('DoubleConversion')
251+
pod 'glog', :path => RNDepsFacades.facade_path('glog')
252+
pod 'boost', :path => RNDepsFacades.facade_path('boost')
253+
pod 'fast_float', :path => RNDepsFacades.facade_path('fast_float')
254+
pod 'fmt', :path => RNDepsFacades.facade_path('fmt')
255+
pod 'RCT-Folly', :path => RNDepsFacades.facade_path('RCT-Folly')
256+
pod 'SocketRocket', :path => RNDepsFacades.facade_path('SocketRocket')
257+
246258
if !ReactNativeCoreUtils.build_rncore_from_source()
247259
pod 'React-Core-prebuilt', :podspec => "#{prefix}/React-Core-prebuilt.podspec", :modular_headers => true
248260
end

packages/react-native/third-party-podspecs/ReactNativeDependencies.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ Pod::Spec.new do |spec|
5353
# Check if XCFRAMEWORK_PATH is empty
5454
if [ -z "$XCFRAMEWORK_PATH" ]; then
5555
echo "ERROR: XCFRAMEWORK_PATH is empty."
56-
exit 0
56+
exit 1
5757
fi
5858
5959
# Check if HEADERS_PATH is empty
6060
if [ -z "$HEADERS_PATH" ]; then
6161
echo "ERROR: HEADERS_PATH is empty."
62-
exit 0
62+
exit 1
6363
fi
6464
6565
cp -R "$HEADERS_PATH/." Headers

0 commit comments

Comments
 (0)