Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/prebuild-ios-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ jobs:
uses: actions/cache/restore@v5
with:
path: packages/react-native/.build/output/xcframeworks
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/**/*') }}
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/**/*') }}
- name: Setup node.js
if: steps.restore-ios-xcframework.outputs.cache-hit != 'true'
uses: ./.github/actions/setup-node
Expand Down Expand Up @@ -200,7 +200,7 @@ jobs:
# privileged-consumer/Expo fixtures). Catches consumer-facing header
# regressions here instead of in downstream builds.
cd packages/react-native
node scripts/ios-prebuild/headers-verify.js --flavor "${{ matrix.flavor }}"
node scripts/ios-prebuild/headers-verify.js --flavor "${{ matrix.flavor }}" ${{ inputs.version-type != '' && '--require-stamped-version' || '' }}
- name: Compress and Rename XCFramework
if: steps.restore-ios-xcframework.outputs.cache-hit != 'true'
run: |
Expand Down Expand Up @@ -246,4 +246,4 @@ jobs:
packages/react-native/.build/output/xcframeworks/ReactCore${{matrix.flavor}}.xcframework.tar.gz
packages/react-native/.build/output/xcframeworks/ReactCore${{matrix.flavor}}.framework.dSYM.tar.gz
packages/react-native/.build/output/xcframeworks/ReactNativeHeaders${{matrix.flavor}}.xcframework.tar.gz
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/**/*') }}
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/**/*') }}
26 changes: 23 additions & 3 deletions packages/react-native/scripts/ios-prebuild/headers-compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ function stageEntries(
stage /*: string */,
entries /*: Array<SpecEntry> */,
rnRoot /*: string */,
overlayDir /*: ?string */ = null,
) /*: void */ {
for (const e of entries) {
const dest = path.join(stage, e.relPath);
Expand All @@ -129,7 +130,24 @@ function stageEntries(
`#import <${e.redirectTo}>\n`,
);
} else {
fs.copyFileSync(path.join(rnRoot, e.source), dest);
// ReactNativeVersion.h is the one shipped header STAMPED at build time:
// the slice job runs set-rn-artifacts-version before building, while the
// source tree keeps the 1000.0.0 dev sentinel. Take just this file's
// content from the built header tree (`overlayDir`, i.e. `.build/headers`)
// when present, so the compose ships the real version without re-stamping
// its own checkout. Every OTHER header is authoritative in the source
// tree (only the layout is spec-derived), so it always copies from
// source — never from a build tree that could be stale relative to it.
const isStamped = path.basename(e.source) === 'ReactNativeVersion.h';
const overlaySource =
isStamped && overlayDir != null
? path.join(overlayDir, e.source)
: null;
const src =
overlaySource != null && fs.existsSync(overlaySource)
? overlaySource
: path.join(rnRoot, e.source);
fs.copyFileSync(src, dest);
}
}
}
Expand All @@ -145,11 +163,12 @@ function emitReactFrameworkHeaders(
xcfwPath /*: string */,
plan /*: HeadersSpecPlan */,
rnRoot /*: string */,
overlayDir /*: ?string */ = null,
) /*: void */ {
const stage = fs.mkdtempSync(
path.join(path.dirname(xcfwPath), '.react-stage-'),
);
stageEntries(stage, plan.react, rnRoot);
stageEntries(stage, plan.react, rnRoot, overlayDir);
fs.writeFileSync(
path.join(stage, 'React-umbrella.h'),
renderUmbrellaHeader(plan.umbrella),
Expand Down Expand Up @@ -239,10 +258,11 @@ function buildReactNativeHeadersXcframework(
// namespace so `<hermes/...>` resolves without per-library wiring. null
// when unstaged — then `<hermes/...>` stays unavailable.
hermesHeaders /*: ?string */ = null,
overlayDir /*: ?string */ = null,
) /*: string */ {
// ---- stage headers ----
const stage = fs.mkdtempSync(path.join(outDir, '.rnh-stage-'));
stageEntries(stage, plan.reactNativeHeaders, rnRoot);
stageEntries(stage, plan.reactNativeHeaders, rnRoot, overlayDir);
// Hermes public headers (separate source from the deps namespaces — they
// come from the hermes-ios tarball, not ReactNativeDependencies). Vend only
// the `hermes/` namespace; `jsi/` is already provided elsewhere, so copying
Expand Down
60 changes: 59 additions & 1 deletion packages/react-native/scripts/ios-prebuild/headers-verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* Usage:
* node scripts/ios-prebuild/headers-verify.js [--flavor Debug|Release]
* [--artifacts <dir>] [--skip-compile] [--update-baseline]
* [--require-stamped-version]
*/

const {computeInventory} = require('./headers-inventory');
Expand Down Expand Up @@ -410,6 +411,49 @@ function runCompileGates(
}
}

// ---------------------------------------------------------------------------
// Version stamp gate
// ---------------------------------------------------------------------------

/**
* Release/nightly artifacts must not ship ReactNativeVersion.h with the
* 1000.0.0 dev sentinel: the compose step copies headers from the source
* tree, so a compose job that forgot to run set-rn-artifacts-version.js
* would silently publish a sentinel header, breaking every library that
* gates code on REACT_NATIVE_VERSION_MAJOR/MINOR.
*/
function verifyVersionStamp(artifactsDir /*: string */) /*: void */ {
const copies = [];
const walk = (dir /*: string */) => {
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
const name = String(entry.name);
const full = path.join(dir, name);
if (entry.isDirectory()) {
walk(full);
} else if (name === 'ReactNativeVersion.h') {
copies.push(full);
}
}
};
walk(artifactsDir);
if (copies.length === 0) {
throw new Error(
`no ReactNativeVersion.h found under ${artifactsDir} — cannot verify the version stamp.`,
);
}
const unstamped = copies.filter(f =>
/REACT_NATIVE_VERSION_MAJOR\s+1000\b/.test(fs.readFileSync(f, 'utf8')),
);
if (unstamped.length > 0) {
throw new Error(
`ReactNativeVersion.h still contains the 1000.0.0 dev sentinel — run ` +
`scripts/releases/set-rn-artifacts-version.js before composing:\n ` +
unstamped.join('\n '),
);
}
log(`version stamp OK (${copies.length} copies checked).`);
}

// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------
Expand All @@ -419,11 +463,13 @@ function parseArgs(argv /*: Array<string> */) /*: {
artifacts: ?string,
skipCompile: boolean,
updateBaseline: boolean,
requireStampedVersion: boolean,
} */ {
let flavor = 'Debug';
let artifacts /*: ?string */ = null;
let skipCompile = false;
let updateBaseline = false;
let requireStampedVersion = false;
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--flavor') {
flavor = argv[++i];
Expand All @@ -433,9 +479,17 @@ function parseArgs(argv /*: Array<string> */) /*: {
skipCompile = true;
} else if (argv[i] === '--update-baseline') {
updateBaseline = true;
} else if (argv[i] === '--require-stamped-version') {
requireStampedVersion = true;
}
}
return {flavor, artifacts, skipCompile, updateBaseline};
return {
flavor,
artifacts,
skipCompile,
updateBaseline,
requireStampedVersion,
};
}

function main(argv /*:: ?: Array<string> */) /*: void */ {
Expand All @@ -460,6 +514,10 @@ function main(argv /*:: ?: Array<string> */) /*: void */ {
`(node scripts/ios-prebuild -c -f ${args.flavor}).`,
);
}
if (args.requireStampedVersion) {
verifyVersionStamp(artifactsDir);
}

const {reactSlice, rnhHeaders} = verifyStructural(plan, artifactsDir);

if (args.skipCompile) {
Expand Down
10 changes: 9 additions & 1 deletion packages/react-native/scripts/ios-prebuild/xcframework.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@ function buildXCFrameworks(
emitReactFrameworkHeaders,
} = require('./headers-compose');
const plan = computeSpecPlan(rootFolder);
emitReactFrameworkHeaders(outputPath, plan, rootFolder);
// Built header tree from the slice jobs (downloaded to `.build/headers`).
// When present, the compose sources header CONTENT from here (see
// stageEntries) so build-time generated/stamped headers — notably the
// version-stamped ReactNativeVersion.h — ship without re-stamping this
// checkout. Absent (e.g. a local compose with no prior build) → source tree.
const builtHeadersDir = path.join(buildFolder, 'headers');
const overlayDir = fs.existsSync(builtHeadersDir) ? builtHeadersDir : null;
emitReactFrameworkHeaders(outputPath, plan, rootFolder, overlayDir);
// ReactNativeHeaders is PURE-RN — the third-party deps namespaces ship in
// the ReactNativeDependenciesHeaders sidecar built by the deps prebuild
// (scripts/releases/ios-prebuild), so the core compose no longer needs the
Expand Down Expand Up @@ -115,6 +122,7 @@ function buildXCFrameworks(
rootFolder,
true, // include the mac-catalyst slice in the real compose
hermesHeaders,
overlayDir,
);

if (identity) {
Expand Down
Loading