Skip to content

Commit fbdcf7a

Browse files
chrfalchclaude
andcommitted
fix(iOS): stamp the React Native version in the composed prebuilt headers
The prebuilt iOS core artifacts shipped ReactNativeVersion.h with the 1000.0.0 dev sentinel: the compose-xcframework job re-derives the shipped headers from its own fresh (unstamped) checkout, while only build-rn-slice runs set-rn-artifacts-version.js. Since the header-facades change, libraries resolve the prebuilt header instead of the stamped npm copy, so any library gating on REACT_NATIVE_VERSION_MAJOR/MINOR compiles the wrong branch (react-native-unistyles in nightly-tests). Rather than re-stamp (and then revert) the compose checkout, source just that one build-stamped header's CONTENT from the built header tree the compose job already downloads (.build/headers): stageEntries now takes an overlay dir and, for ReactNativeVersion.h only, prefers the built copy (stamped in the slice job) over the source sentinel. Header LAYOUT is still spec-derived from source, and every other header still copies from source, so a stale build tree can never ship wrong header content. Bump the compose cache key so pre-fix unstamped composed artifacts are not served. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e640278 commit fbdcf7a

3 files changed

Lines changed: 34 additions & 6 deletions

File tree

.github/workflows/prebuild-ios-core.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ jobs:
133133
uses: actions/cache/restore@v5
134134
with:
135135
path: packages/react-native/.build/output/xcframeworks
136-
key: v3-ios-core-xcframework-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/*.js', 'packages/react-native/scripts/ios-prebuild.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}
136+
key: v4-ios-core-xcframework-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/*.js', 'packages/react-native/scripts/ios-prebuild.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}
137137
- name: Setup node.js
138138
if: steps.restore-ios-xcframework.outputs.cache-hit != 'true'
139139
uses: ./.github/actions/setup-node
@@ -246,4 +246,4 @@ jobs:
246246
packages/react-native/.build/output/xcframeworks/ReactCore${{matrix.flavor}}.xcframework.tar.gz
247247
packages/react-native/.build/output/xcframeworks/ReactCore${{matrix.flavor}}.framework.dSYM.tar.gz
248248
packages/react-native/.build/output/xcframeworks/ReactNativeHeaders${{matrix.flavor}}.xcframework.tar.gz
249-
key: v3-ios-core-xcframework-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/*.js', 'packages/react-native/scripts/ios-prebuild.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}
249+
key: v4-ios-core-xcframework-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/*.js', 'packages/react-native/scripts/ios-prebuild.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ function stageEntries(
114114
stage /*: string */,
115115
entries /*: Array<SpecEntry> */,
116116
rnRoot /*: string */,
117+
overlayDir /*: ?string */ = null,
117118
) /*: void */ {
118119
for (const e of entries) {
119120
const dest = path.join(stage, e.relPath);
@@ -129,7 +130,24 @@ function stageEntries(
129130
`#import <${e.redirectTo}>\n`,
130131
);
131132
} else {
132-
fs.copyFileSync(path.join(rnRoot, e.source), dest);
133+
// ReactNativeVersion.h is the one shipped header STAMPED at build time:
134+
// the slice job runs set-rn-artifacts-version before building, while the
135+
// source tree keeps the 1000.0.0 dev sentinel. Take just this file's
136+
// content from the built header tree (`overlayDir`, i.e. `.build/headers`)
137+
// when present, so the compose ships the real version without re-stamping
138+
// its own checkout. Every OTHER header is authoritative in the source
139+
// tree (only the layout is spec-derived), so it always copies from
140+
// source — never from a build tree that could be stale relative to it.
141+
const isStamped = path.basename(e.source) === 'ReactNativeVersion.h';
142+
const overlaySource =
143+
isStamped && overlayDir != null
144+
? path.join(overlayDir, e.source)
145+
: null;
146+
const src =
147+
overlaySource != null && fs.existsSync(overlaySource)
148+
? overlaySource
149+
: path.join(rnRoot, e.source);
150+
fs.copyFileSync(src, dest);
133151
}
134152
}
135153
}
@@ -145,11 +163,12 @@ function emitReactFrameworkHeaders(
145163
xcfwPath /*: string */,
146164
plan /*: HeadersSpecPlan */,
147165
rnRoot /*: string */,
166+
overlayDir /*: ?string */ = null,
148167
) /*: void */ {
149168
const stage = fs.mkdtempSync(
150169
path.join(path.dirname(xcfwPath), '.react-stage-'),
151170
);
152-
stageEntries(stage, plan.react, rnRoot);
171+
stageEntries(stage, plan.react, rnRoot, overlayDir);
153172
fs.writeFileSync(
154173
path.join(stage, 'React-umbrella.h'),
155174
renderUmbrellaHeader(plan.umbrella),
@@ -239,10 +258,11 @@ function buildReactNativeHeadersXcframework(
239258
// namespace so `<hermes/...>` resolves without per-library wiring. null
240259
// when unstaged — then `<hermes/...>` stays unavailable.
241260
hermesHeaders /*: ?string */ = null,
261+
overlayDir /*: ?string */ = null,
242262
) /*: string */ {
243263
// ---- stage headers ----
244264
const stage = fs.mkdtempSync(path.join(outDir, '.rnh-stage-'));
245-
stageEntries(stage, plan.reactNativeHeaders, rnRoot);
265+
stageEntries(stage, plan.reactNativeHeaders, rnRoot, overlayDir);
246266
// Hermes public headers (separate source from the deps namespaces — they
247267
// come from the hermes-ios tarball, not ReactNativeDependencies). Vend only
248268
// the `hermes/` namespace; `jsi/` is already provided elsewhere, so copying

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,14 @@ function buildXCFrameworks(
8787
emitReactFrameworkHeaders,
8888
} = require('./headers-compose');
8989
const plan = computeSpecPlan(rootFolder);
90-
emitReactFrameworkHeaders(outputPath, plan, rootFolder);
90+
// Built header tree from the slice jobs (downloaded to `.build/headers`).
91+
// When present, the compose sources header CONTENT from here (see
92+
// stageEntries) so build-time generated/stamped headers — notably the
93+
// version-stamped ReactNativeVersion.h — ship without re-stamping this
94+
// checkout. Absent (e.g. a local compose with no prior build) → source tree.
95+
const builtHeadersDir = path.join(buildFolder, 'headers');
96+
const overlayDir = fs.existsSync(builtHeadersDir) ? builtHeadersDir : null;
97+
emitReactFrameworkHeaders(outputPath, plan, rootFolder, overlayDir);
9198
// ReactNativeHeaders is PURE-RN — the third-party deps namespaces ship in
9299
// the ReactNativeDependenciesHeaders sidecar built by the deps prebuild
93100
// (scripts/releases/ios-prebuild), so the core compose no longer needs the
@@ -115,6 +122,7 @@ function buildXCFrameworks(
115122
rootFolder,
116123
true, // include the mac-catalyst slice in the real compose
117124
hermesHeaders,
125+
overlayDir,
118126
);
119127

120128
if (identity) {

0 commit comments

Comments
 (0)