Skip to content

Commit 40bd75c

Browse files
chrfalchclaude
andcommitted
fix(cocoapods): install prebuilt RNCore pods as header-less facades
In prebuilt mode the React core pods' code + headers live entirely in React.xcframework / React-Core-prebuilt. Re-installing their SOURCE podspecs made them ship duplicate headers that shadow the prebuilt artifact and break the React framework's clang explicit-module precompile (-Wnon-modular-include-in-framework-module) under Xcode 26. Install those core pods as dependency-only FACADES instead: generated podspecs with no sources/headers, installed via :path (so nothing is fetched), each depending on React-Core-prebuilt. Version, subspecs, default_subspec and resources (e.g. the privacy manifest) are DERIVED from the real podspec so the facade stays graph- and resource-equivalent to the source pod. With the shadowing gone the React module precompiles cleanly with SWIFT_ENABLE_EXPLICIT_MODULES on, so the Xcode-26 workaround (#53457) is removed. The prebuilt header search path + ReactNativeHeaders module-map activation are consolidated into a single post-install injection site (configure_aggregate_xcconfig); add_rncore_dependency now only declares the React-Core-prebuilt dependency. rn-tester's NativeComponentExample uses the canonical <React/...> include for RCTFabricComponentsPlugins.h (resolved from the framework) so it builds against the facaded React-RCTFabric in prebuilt mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 934e8fe commit 40bd75c

5 files changed

Lines changed: 254 additions & 31 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def setup_fabric!(react_native_path: "../node_modules/react-native")
1111
pod 'React-Fabric', :path => "#{react_native_path}/ReactCommon"
1212
pod 'React-FabricComponents', :path => "#{react_native_path}/ReactCommon"
1313
pod 'React-graphics', :path => "#{react_native_path}/ReactCommon/react/renderer/graphics"
14-
pod 'React-RCTFabric', :path => "#{react_native_path}/React", :modular_headers => true
14+
rncore_pod 'React-RCTFabric', :path => "#{react_native_path}/React", :modular_headers => true
1515
pod 'React-ImageManager', :path => "#{react_native_path}/ReactCommon/react/renderer/imagemanager/platform/ios"
1616
pod 'React-FabricImage', :path => "#{react_native_path}/ReactCommon"
1717
end

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

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,16 @@
1111

1212
### Adds ReactNativeCore-prebuilt as a dependency to the given podspec if we're not
1313
### building ReactNativeCore from source (then this function does nothing).
14+
###
15+
### `<React/...>` resolves through the vendored React.framework; every other namespace
16+
### (`<react/...>`, `<yoga/...>`, `<hermes/...>`, ...) resolves through the flattened
17+
### ReactNativeHeaders headers that React-Core-prebuilt exposes. The header search path
18+
### and the ReactNativeHeaders module-map activation are NOT added here: they are applied
19+
### post-install by configure_aggregate_xcconfig, which covers aggregate, third-party AND
20+
### these pods from a single injection site. No clang VFS overlay.
1421
def add_rncore_dependency(s)
1522
if !ReactNativeCoreUtils.build_rncore_from_source()
16-
# `<React/...>` resolves through the vendored React.framework; every other
17-
# namespace (`<react/...>`, `<yoga/...>`, `<hermes/...>`, ...) resolves
18-
# through the flattened ReactNativeHeaders headers that React-Core-prebuilt
19-
# exposes on its header search path. No clang VFS overlay.
2023
s.dependency "React-Core-prebuilt"
21-
22-
current_pod_target_xcconfig = s.to_hash["pod_target_xcconfig"] || {}
23-
current_pod_target_xcconfig = current_pod_target_xcconfig.to_h unless current_pod_target_xcconfig.is_a?(Hash)
24-
25-
# HEADER_SEARCH_PATHS may already be a String or an Array (e.g. after
26-
# add_rn_third_party_dependencies); normalize to an Array before appending.
27-
header_search_paths = current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] || []
28-
header_search_paths = header_search_paths.split(" ") if header_search_paths.is_a?(String)
29-
header_search_paths << "\"$(PODS_ROOT)/React-Core-prebuilt/Headers\""
30-
current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] = header_search_paths
31-
32-
s.pod_target_xcconfig = current_pod_target_xcconfig
3324
end
3425
end
3526

@@ -516,9 +507,10 @@ def self.get_nightly_npm_version()
516507
return latest_nightly
517508
end
518509

519-
# Configures the xcconfig files for aggregate (main app) targets and third-party pod
520-
# targets so the prebuilt ReactNativeHeaders are resolvable. These targets do not go
521-
# through add_rncore_dependency, so they won't otherwise get the header search path.
510+
# Single post-install injection site for the prebuilt header resolution. Adds the
511+
# ReactNativeHeaders search path + module-map activation to the aggregate (main app)
512+
# target AND every pod target — RN core pods, third-party pods alike. (add_rncore_dependency
513+
# only declares the React-Core-prebuilt dependency; it no longer touches xcconfigs.)
522514
#
523515
# `<React/...>` resolves through the vendored React.framework; this adds the search
524516
# path to the flattened ReactNativeHeaders headers (every other namespace). There is
@@ -567,5 +559,12 @@ def self.add_prebuilt_header_search_paths(attributes, headers_search_path)
567559
ReactNativePodsUtils.add_flag_to_map_with_inheritance(attributes, "HEADER_SEARCH_PATHS", headers_search_path)
568560
# Suppress incomplete umbrella warnings for the prebuilt frameworks (it is expected, as our umbrella headers do not include all headers)
569561
ReactNativePodsUtils.add_flag_to_map_with_inheritance(attributes, "OTHER_SWIFT_FLAGS", " -Xcc -Wno-incomplete-umbrella")
562+
# Activate the ReactNativeHeaders module map so the relocated namespaces
563+
# (`yoga`, `RCTDeprecation`, `ReactNativeHeaders_react`, ...) are modular —
564+
# otherwise the React framework's clang explicit-module precompile trips
565+
# -Wnon-modular-include-in-framework-module on `<yoga/...>` / `<react/...>`.
566+
module_map_flag = " -fmodule-map-file=$(PODS_ROOT)/React-Core-prebuilt/Headers/module.modulemap"
567+
ReactNativePodsUtils.add_flag_to_map_with_inheritance(attributes, "OTHER_CFLAGS", module_map_flag)
568+
ReactNativePodsUtils.add_flag_to_map_with_inheritance(attributes, "OTHER_SWIFT_FLAGS", " -Xcc" + module_map_flag)
570569
end
571570
end
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
9+
# Facade podspecs for the prebuilt React Native Core path.
10+
#
11+
# In prebuilt mode the compiled code AND headers for the React core pods live
12+
# entirely inside React.xcframework + React-Core-prebuilt (which flattens the
13+
# ReactNativeHeaders namespaces into its Headers/). Re-installing the SOURCE
14+
# podspecs in that mode is what makes them ship duplicate headers that shadow the
15+
# prebuilt artifact (via HEADER_SEARCH_PATHS, CocoaPods .hmap header maps, and
16+
# the all-product-headers VFS overlay) and break the React framework's clang
17+
# explicit-module precompile.
18+
#
19+
# Instead we install dependency-only FACADE podspecs for those names: they ship
20+
# no source files and no headers, so CocoaPods makes them PBXAggregateTarget
21+
# placeholders (should_build? == false) and nothing is laid down to shadow. Each
22+
# facade depends on React-Core-prebuilt so its consumers transitively pick up the
23+
# prebuilt framework + headers. The pod NAMES still resolve, so ReactCodegen,
24+
# third-party modules, and RN's own podspec graph keep resolving `React-Core`,
25+
# `Yoga`, `React-Core/Default`, etc.
26+
#
27+
# MAINTENANCE MODEL: the set of facaded pods is explicit (FACADE_PODS) so the
28+
# prebuilt rollout can be staged, but each facade's VERSION and SUBSPECS are
29+
# DERIVED from the real podspec at `pod install` time (Pod::Specification.from_file).
30+
# That removes the drift risk that would otherwise bite third-party libraries:
31+
# if React adds/renames `React-Core/<Subspec>`, the facade exposes it
32+
# automatically — nobody has to hand-maintain a parallel subspec list.
33+
#
34+
# This is staged: phase 1 facades a small set and KEEPS the existing
35+
# podspec_sources / add_rncore_dependency / configure_aggregate_xcconfig /
36+
# -fmodule-map-file machinery. The set is expanded until the cold prebuilt
37+
# build passes; the distributed prebuilt helpers are only deleted afterwards.
38+
module RNCoreFacades
39+
# pod name => podspec path (relative to the react-native package root).
40+
# These are the React-core pods whose code + headers are fully provided by
41+
# the prebuilt React.xcframework / React-Core-prebuilt. Start small; expand as
42+
# the cold build surfaces more shadowing pods. (NOTE: not every caller of
43+
# add_rncore_dependency belongs here — e.g. ReactCodegen depends on the
44+
# prebuilt but still builds its own generated sources, so it is NOT a facade.)
45+
FACADE_PODS = {
46+
"React-Core" => "React-Core.podspec",
47+
"React-RCTFabric" => "React/React-RCTFabric.podspec",
48+
"React-RCTRuntime" => "React/Runtime/React-RCTRuntime.podspec",
49+
"Yoga" => "ReactCommon/yoga/Yoga.podspec",
50+
"RCTDeprecation" => "ReactApple/Libraries/RCTFoundation/RCTDeprecation/RCTDeprecation.podspec",
51+
"FBLazyVector" => "Libraries/FBLazyVector/FBLazyVector.podspec",
52+
"RCTRequired" => "Libraries/Required/RCTRequired.podspec",
53+
}
54+
55+
# Sub-directory (relative to the install root) that holds the generated facades.
56+
FACADE_RELDIR = File.join("build", "rncore-facades")
57+
58+
@@install_root = nil
59+
60+
# True when `name` should be installed as a facade instead of its source podspec.
61+
def self.facade?(name)
62+
FACADE_PODS.key?(name)
63+
end
64+
65+
# Generates the facade podspecs and returns the base directory holding them.
66+
# Each facade gets its OWN sub-directory containing a single
67+
# `<Name>.podspec.json`, so it can be installed as a LOCAL pod via
68+
# `:path => <dir>`. `:path` (PathSource) uses the spec in place and never
69+
# downloads `spec.source` — unlike `:podspec` (PodspecSource), which is an
70+
# *external* source whose `root_spec.source` CocoaPods would actually fetch
71+
# (i.e. git-clone react-native for every empty facade). Idempotent; safe to
72+
# call once per `pod install`.
73+
#
74+
# `react_native_path` locates the real podspecs we mirror. version + subspecs +
75+
# default_subspecs + resources are all DERIVED from the real spec so the facade
76+
# stays graph- and resource-equivalent to the source pod. A facaded pod whose
77+
# real podspec can't be read is a hard error (see load_real_spec) — silently
78+
# shipping an empty facade would hide exactly the drift this guards against.
79+
def self.generate(react_native_path, install_root, version, ios_version)
80+
@@install_root = install_root.to_s
81+
abs_base = File.join(@@install_root, FACADE_RELDIR)
82+
FileUtils.mkdir_p(abs_base)
83+
FACADE_PODS.each do |name, podspec_rel_path|
84+
podspec_path = File.join(react_native_path.to_s, podspec_rel_path)
85+
real = load_real_spec(podspec_path, name)
86+
podspec_dir = File.dirname(podspec_path)
87+
dir = File.join(abs_base, name)
88+
FileUtils.mkdir_p(dir)
89+
90+
spec = {
91+
"name" => name,
92+
"version" => real.version.to_s,
93+
"summary" => "Prebuilt facade for #{name} (code + headers live in React-Core-prebuilt).",
94+
"homepage" => "https://reactnative.dev/",
95+
"license" => "MIT",
96+
"authors" => "Meta Platforms, Inc. and its affiliates",
97+
"platforms" => { "ios" => ios_version },
98+
# Required podspec attribute, but never fetched: the pod is installed
99+
# as a LOCAL pod (`:path => <dir>`), which uses this spec in place and
100+
# ships no source_files. Placeholder only.
101+
"source" => { "git" => "https://github.com/facebook/react-native.git" },
102+
"dependencies" => { "React-Core-prebuilt" => [] },
103+
}
104+
105+
# Preserve non-code RESOURCES (privacy manifest, i18n bundles, ...). They
106+
# don't shadow headers, and React-Core-prebuilt doesn't vend them, so the
107+
# facade must carry them or prebuilt installs lose them. Globs are made
108+
# relative to the facade dir so they resolve back to the real source tree.
109+
resource_bundles = derive_resource_bundles(real, podspec_dir, dir)
110+
spec["resource_bundles"] = resource_bundles unless resource_bundles.empty?
111+
resources = derive_resources(real, podspec_dir, dir)
112+
spec["resources"] = resources unless resources.empty?
113+
114+
# Preserve default_subspec so a bare `pod '<Name>'` resolves to the SAME
115+
# subspec graph as the source pod (without it CocoaPods pulls every
116+
# subspec, which is not graph-equivalent).
117+
defaults = Array(real.default_subspecs)
118+
spec["default_subspecs"] = defaults unless defaults.empty?
119+
120+
subspecs = derive_subspecs(real)
121+
unless subspecs.empty?
122+
spec["subspecs"] = subspecs.map do |ss|
123+
{ "name" => ss, "dependencies" => { "React-Core-prebuilt" => [] } }
124+
end
125+
end
126+
127+
File.write(File.join(dir, "#{name}.podspec.json"), JSON.pretty_generate(spec))
128+
end
129+
abs_base
130+
end
131+
132+
# Facade dir for `<name>`, RELATIVE to the install root — pass to `pod :path =>`.
133+
# Relative (not absolute) so the path CocoaPods records in Podfile.lock is
134+
# portable rather than machine-specific.
135+
def self.facade_path(name)
136+
File.join(FACADE_RELDIR, name)
137+
end
138+
139+
# Loads the real podspec so we can mirror its structure. A facaded pod MUST have
140+
# a readable real podspec — if it's missing or unparseable we raise rather than
141+
# ship an empty facade, since that would silently drop subspecs/resources (the
142+
# very drift this mechanism exists to prevent).
143+
def self.load_real_spec(path, name)
144+
unless File.exist?(path)
145+
raise "[RNCoreFacades] Real podspec for facaded pod '#{name}' not found at #{path}. " \
146+
"Update FACADE_PODS in rncore_facades.rb if the podspec moved."
147+
end
148+
Pod::Specification.from_file(path)
149+
rescue => e
150+
raise "[RNCoreFacades] Failed to read real podspec for facaded pod '#{name}' at #{path}: #{e.message}"
151+
end
152+
private_class_method :load_real_spec
153+
154+
# Library (non-test, non-app) subspec names of the real spec, so third-party
155+
# libs depending on `<pod>/<subspec>` keep resolving. Derived, never hand-listed.
156+
def self.derive_subspecs(real)
157+
real.subspecs
158+
.reject { |ss| ss.test_specification? || (ss.respond_to?(:app_specification?) && ss.app_specification?) }
159+
.map(&:base_name)
160+
end
161+
private_class_method :derive_subspecs
162+
163+
# Effective resource_bundles of the real spec (e.g. React-Core_privacy), with
164+
# globs rewritten relative to the facade dir so they point back at the real
165+
# source files. Unions the `resource_bundle` (singular) and `resource_bundles`
166+
# (plural) DSL forms.
167+
def self.derive_resource_bundles(real, podspec_dir, facade_dir)
168+
out = {}
169+
[real.attributes_hash["resource_bundle"], real.attributes_hash["resource_bundles"]].each do |rb|
170+
next unless rb.is_a?(Hash)
171+
rb.each do |bundle, globs|
172+
out[bundle] = Array(globs).map { |g| rel_glob(g, podspec_dir, facade_dir) }
173+
end
174+
end
175+
out
176+
end
177+
private_class_method :derive_resource_bundles
178+
179+
# Loose `resources` of the real spec, rewritten relative to the facade dir.
180+
def self.derive_resources(real, podspec_dir, facade_dir)
181+
Array(real.attributes_hash["resources"]).map { |g| rel_glob(g, podspec_dir, facade_dir) }
182+
end
183+
private_class_method :derive_resources
184+
185+
# Rewrite a glob declared relative to `podspec_dir` into one relative to
186+
# `facade_dir`, so the generated facade (which lives under the app's build/)
187+
# still resolves the resource in the react-native source tree.
188+
def self.rel_glob(glob, podspec_dir, facade_dir)
189+
require "pathname"
190+
abs = File.expand_path(glob, podspec_dir)
191+
Pathname.new(abs).relative_path_from(Pathname.new(facade_dir)).to_s
192+
end
193+
private_class_method :rel_glob
194+
end

0 commit comments

Comments
 (0)