Skip to content

Commit 8a35e69

Browse files
chrfalchreact-native-bot
authored andcommitted
fix(iOS): exclude C++-guarded foreign-namespace headers from React umbrella (#57575)
Summary: The first nightly containing the prebuilt/SwiftPM stack (`0.88.0-nightly-20260716`) broke the `react-native-unistyles` job in [react-native-community/nightly-tests](https://github.com/react-native-community/nightly-tests/actions/runs/29476835418/job/87551592185) with: ``` Pods/Headers/Public/React-Core-prebuilt/react/timing/primitives.h:28:7: error: redefinition of 'HighResDuration' note: additional include site in header from module 'ReactNativeHeaders_react' note: additional include site in header from module 'React.RCTFrameTimingsObserver' → could not build module 'React' ``` **Root cause:** `RCTFrameTimingsObserver.h` includes `<jsinspector-modern/tracing/FrameTimingSequence.h>` under `#ifdef __cplusplus`, transitively reaching `react/timing/primitives.h`, which is owned by the `ReactNativeHeaders_react` module. The headers inventory's reachability fixpoint only follows *unguarded* include edges (modeling a pure-ObjC consumer), so it couldn't see this edge and classified the header as umbrella-safe. Swift pods with C++ interop (unistyles, Nitro-based libs) build the `React` clang module as ObjC++, opening the guard — one header owned by two modules in a single compilation → C++ redefinition errors. Pure-ObjC consumers never open the guard, which is why only C++-interop libraries failed. **Fix:** a curated `UMBRELLA_CXX_GUARDED_EXCLUSIONS` set checked in `isUmbrellaSafe`, mirroring the existing `PRIVATE_REACT_HEADERS` pattern, with fail-closed validation (`validateUmbrellaExclusions`, wired into `planFromInventory`) so a renamed/deleted entry surfaces immediately. The header still ships in `React.framework/Headers` and stays importable via `#import <React/RCTFrameTimingsObserver.h>` (its only consumer today is `RCTHost.mm`); it is only removed from the modular umbrella surface. If a modular consumer ever needs it, it can be promoted to a `PRIVATE_REACT_HEADERS.textual`-style entry instead. ## Changelog: [IOS] [FIXED] - Fix "redefinition of 'HighResDuration'" / "could not build module 'React'" when building Swift pods with C++ interop against the prebuilt React-Core artifact Pull Request resolved: #57575 Test Plan: - New unit tests in `scripts/ios-prebuild/__tests__/headers-spec-test.js`: the excluded header is kept out of the umbrella (fails against the previous code with `Received array: ["React/RCTFrameTimingsObserver.h"]`), and exclusion drift fails closed. - Full ios-prebuild suite: 5 suites, 58/58 tests passing; prettier/eslint clean. - E2E: to be confirmed via a nightly/prebuilt cut building react-native-unistyles in prebuilt mode (`RCT_USE_RN_DEP=1 RCT_USE_PREBUILT_RNCORE=1`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed By: sammy-SC Differential Revision: D112320697 Pulled By: cipolleschi fbshipit-source-id: 9c7d1d2cb035ac821992914d8df4db7245905834
1 parent 31cf29a commit 8a35e69

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

packages/react-native/scripts/ios-prebuild/__tests__/headers-spec-test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ const validManifest = () => ({
7272
'objc-modular-candidate',
7373
),
7474
entry('React_RCTAppDelegate/RCTAppDelegate.h', 'objc-modular-candidate'),
75+
// Bucketed objc-modular-candidate because its only C++ include
76+
// (<jsinspector-modern/tracing/...> → react/timing/primitives.h) is behind
77+
// a `#ifdef __cplusplus` guard; the inventory's umbrella-safety fixpoint
78+
// follows unguarded edges only. It must still be kept OUT of the umbrella.
79+
entry('React/RCTFrameTimingsObserver.h', 'objc-modular-candidate'),
7580
],
7681
});
7782

@@ -166,6 +171,28 @@ describe('R10 per-namespace umbrella (React_RCTAppDelegate)', () => {
166171
});
167172
});
168173

174+
describe('R4 umbrella excludes C++-guarded foreign-namespace headers', () => {
175+
test('a guarded-C++ objc-modular-candidate header is kept out of the umbrella', () => {
176+
const plan = planFromInventoryForTest(validManifest());
177+
// RCTFrameTimingsObserver.h is objc-modular-candidate for a pure-ObjC
178+
// consumer, but reaches react/timing/primitives.h once the __cplusplus
179+
// guard opens (Swift/C++-interop compiles the React module as ObjC++).
180+
// primitives.h is owned by ReactNativeHeaders_react, so an umbrella entry
181+
// would make one header live in two modules → C++ redefinition.
182+
expect(plan.umbrella).not.toContain('React/RCTFrameTimingsObserver.h');
183+
});
184+
185+
test('fails closed when an excluded header is absent from the inventory', () => {
186+
const m = validManifest();
187+
m.headers = m.headers.filter(
188+
x => x.naturalPath !== 'React/RCTFrameTimingsObserver.h',
189+
);
190+
expect(() => planFromInventoryForTest(m)).toThrow(
191+
/RCTFrameTimingsObserver\.h is absent/,
192+
);
193+
});
194+
});
195+
169196
describe('R5 invalid-identifier exemption assert (H5)', () => {
170197
test('throws when an invalid-identifier namespace gains a modular candidate', () => {
171198
const m = validManifest();

packages/react-native/scripts/ios-prebuild/headers-spec.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,24 @@ const EXTERN_INLINE_RE /*: RegExp */ =
143143

144144
const MODULE_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
145145

146+
// R4 umbrella exclusion: ObjC headers whose ONLY C++ include is behind a
147+
// `#ifdef __cplusplus` guard, so the inventory buckets them
148+
// objc-modular-candidate — the reachability fixpoint (headers-inventory.js)
149+
// follows UNGUARDED edges only, modelling a pure-ObjC consumer. But the React
150+
// framework module is compiled as Objective-C++ by Swift/C++-interop consumers:
151+
// the guard opens, the umbrella transitively includes the foreign C++ header,
152+
// and that header is ALSO owned by the ReactNativeHeaders_<ns> module. One
153+
// physical header, two modules → C++ redefinition ("could not build module
154+
// 'React'"). Keeping the header OUT of the umbrella leaves it shipped and
155+
// textually importable (`#import <React/...>`); its guarded C++ includes then
156+
// resolve at the consumer's use site, exactly as PRIVATE_REACT_HEADERS.textual
157+
// (R9) already handles the unguarded objc-blocked case.
158+
// Found empirically: RCTFrameTimingsObserver.h (#56015) reaches
159+
// react/timing/primitives.h via <jsinspector-modern/tracing/FrameTimingSequence.h>.
160+
const UMBRELLA_CXX_GUARDED_EXCLUSIONS /*: Set<string> */ = new Set([
161+
'React/RCTFrameTimingsObserver.h',
162+
]);
163+
146164
// R9: Private React headers — a curated allowlist of `<React/...>` headers that
147165
// privileged framework consumers (e.g. Expo) import, but which the public
148166
// umbrella (R4) excludes (they are `+`-suffixed and/or objc-blocked). They are
@@ -202,8 +220,29 @@ function validatePrivateReactHeaders(manifest /*: any */) /*: void */ {
202220
}
203221
}
204222

223+
// Fail closed if an umbrella exclusion drifts: the header must still exist in
224+
// the inventory. If it was removed/renamed the stale exclusion would silently
225+
// stop matching and the header would re-enter the umbrella — reintroducing the
226+
// dual-module redefinition. Force the list to be updated instead.
227+
function validateUmbrellaExclusions(manifest /*: any */) /*: void */ {
228+
const naturals = new Set(manifest.headers.map(h => h.naturalPath));
229+
for (const np of UMBRELLA_CXX_GUARDED_EXCLUSIONS) {
230+
if (!naturals.has(np)) {
231+
throw new Error(
232+
`Umbrella exclusion ${np} is absent from the inventory ` +
233+
`(removed/renamed in source?). Update ` +
234+
`UMBRELLA_CXX_GUARDED_EXCLUSIONS.`,
235+
);
236+
}
237+
}
238+
}
239+
205240
function isUmbrellaSafe(h /*: any */, rnRoot /*: string */) /*: boolean */ {
206-
if (h.bucket !== 'objc-modular-candidate' || h.naturalPath.includes('+')) {
241+
if (
242+
h.bucket !== 'objc-modular-candidate' ||
243+
h.naturalPath.includes('+') ||
244+
UMBRELLA_CXX_GUARDED_EXCLUSIONS.has(h.naturalPath)
245+
) {
207246
return false;
208247
}
209248
try {
@@ -254,6 +293,7 @@ function planFromInventory(
254293
) /*: HeadersSpecPlan */ {
255294
const root = rnRoot ?? manifest.root ?? RN_ROOT;
256295
validatePrivateReactHeaders(manifest); // R9: fail closed on allowlist drift
296+
validateUmbrellaExclusions(manifest); // R4: fail closed on exclusion drift
257297
const react /*: Array<SpecEntry> */ = [];
258298
const reactNativeHeaders /*: Array<SpecEntry> */ = [];
259299
const umbrella /*: Array<string> */ = [];

0 commit comments

Comments
 (0)