Skip to content

Commit 2de081b

Browse files
chrfalchclaude
andcommitted
fix(ios-prebuild): address review on the deps-headers sidecar
Review feedback on ReactNativeDependenciesHeaders + pure-RN ReactNativeHeaders: - prebuild-ios-dependencies.yml: the sidecar cache key now hashes the two cross-package files it is actually generated from — ios-prebuild/ headers-xcframework.js (emitter) + headers-spec.js (DEPS_NAMESPACES) — in BOTH occurrences, and bumps v4->v5. A DEPS_NAMESPACES/emitter edit can no longer serve a stale cached sidecar on main. - prebuild-ios-core.yml: bump the core xcframework cache key v2->v3 to reflect the added ReactNativeHeaders tarball in the artifact set (was relying only on the hashFiles component). - headers-xcframework.js: wrap both staging temp dirs (.stub-work-* / .deps-headers-stage-*) in try/finally so an xcrun/xcodebuild failure can't leave them behind in outDir (= third-party/). - verifyArtifactsAreOnMaven.js: clarify the comment — the 8 primary xcframework classifier tarballs are HEAD-checked; the 4 dSYM classifiers are intentionally excluded. - test-ios-rntester/action.yml: harden the use-prebuilds resolution — only true/false enable/disable; an unexpected value warns instead of silently disabling. - reactNativeDependencies.js: comment reconciling the two sidecar sources (the full-slice tarball copy vs the ensureHeadersLayout DEFAULT_STUB_SLICES fallback) — they agree on the iOS slices SPM needs; headers are slice-uniform. - Tests: stubSlicesFromXcframework known-slice mapping + unknown-slice throw (mocked plutil JSON). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 43812dd commit 2de081b

7 files changed

Lines changed: 138 additions & 56 deletions

File tree

.github/actions/test-ios-rntester/action.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,14 @@ runs:
6161
else
6262
echo "enabled=true" >> "$GITHUB_OUTPUT"
6363
fi
64+
elif [[ "${{ inputs.use-prebuilds }}" == "true" ]]; then
65+
echo "enabled=true" >> "$GITHUB_OUTPUT"
66+
elif [[ "${{ inputs.use-prebuilds }}" == "false" ]]; then
67+
echo "enabled=false" >> "$GITHUB_OUTPUT"
6468
else
65-
echo "enabled=${{ inputs.use-prebuilds }}" >> "$GITHUB_OUTPUT"
69+
# Don't silently treat a typo as 'disabled' — surface it.
70+
echo "::warning::Unexpected use-prebuilds value '${{ inputs.use-prebuilds }}' (expected auto/true/false); treating as disabled."
71+
echo "enabled=false" >> "$GITHUB_OUTPUT"
6672
fi
6773
- name: Download ReactNativeDependencies
6874
if: ${{ steps.prebuilds.outputs.enabled == 'true' }}

.github/workflow-scripts/verifyArtifactsAreOnMaven.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ const ARTIFACT_URL =
1616
'https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/';
1717
const ARTIFACT_NAME = 'react-native-artifacts-';
1818

19-
// The classifier-suffixed tarballs attached to the react-native-artifacts
20-
// publication (external-artifacts/build.gradle.kts). The POM check alone
21-
// would pass even when a classifier artifact never made it to Maven.
19+
// The primary xcframework classifier tarballs attached to the
20+
// react-native-artifacts publication (external-artifacts/build.gradle.kts).
21+
// The 4 dSYM classifiers (core/deps dSYM debug+release) are intentionally
22+
// excluded — they are debug-symbol sidecars, not consumed at install time.
23+
// The POM check alone would pass even when a classifier artifact never made
24+
// it to Maven.
2225
const ARTIFACT_CLASSIFIERS = [
2326
'reactnative-core-debug',
2427
'reactnative-core-release',

.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: v2-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: 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/**/*') }}
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: v2-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: 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/**/*') }}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ jobs:
130130
with:
131131
path: |
132132
packages/react-native/third-party/
133-
key: v4-ios-dependencies-xcframework-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js', 'scripts/releases/ios-prebuild/compose-framework.js') }}
133+
key: v5-ios-dependencies-xcframework-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js', 'scripts/releases/ios-prebuild/compose-framework.js', 'packages/react-native/scripts/ios-prebuild/headers-xcframework.js', 'packages/react-native/scripts/ios-prebuild/headers-spec.js') }}
134134
# If cache hit, we already have our binary. We don't need to do anything.
135135
- name: Yarn Install
136136
if: steps.restore-xcframework.outputs.cache-hit != 'true'
@@ -204,4 +204,4 @@ jobs:
204204
packages/react-native/third-party/ReactNativeDependencies${{ matrix.flavor }}.xcframework.tar.gz
205205
packages/react-native/third-party/ReactNativeDependenciesHeaders${{ matrix.flavor }}.xcframework.tar.gz
206206
packages/react-native/third-party/ReactNativeDependencies${{ matrix.flavor }}.framework.dSYM.tar.gz
207-
key: v4-ios-dependencies-xcframework-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js', 'scripts/releases/ios-prebuild/compose-framework.js') }}
207+
key: v5-ios-dependencies-xcframework-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js', 'scripts/releases/ios-prebuild/compose-framework.js', 'packages/react-native/scripts/ios-prebuild/headers-xcframework.js', 'packages/react-native/scripts/ios-prebuild/headers-spec.js') }}

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

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010

1111
'use strict';
1212

13-
const {buildDepsHeadersXcframework} = require('../headers-xcframework');
13+
const {
14+
buildDepsHeadersXcframework,
15+
stubSlicesFromXcframework,
16+
} = require('../headers-xcframework');
17+
const childProcess = require('child_process');
1418
const fs = require('fs');
1519
const os = require('os');
1620
const path = require('path');
@@ -57,3 +61,53 @@ describe('buildDepsHeadersXcframework set-equality gate', () => {
5761
).toThrow(/missing from .*Headers: glog/); // throws for glog, not stray.h
5862
});
5963
});
64+
65+
describe('stubSlicesFromXcframework', () => {
66+
// The plist shape is a pure function of plutil's JSON; mock it so the
67+
// SupportedPlatform/Variant -> key mapping and the unknown-slice guard can be
68+
// tested without a real xcframework or macOS tooling.
69+
const mockPlist = (obj /*: mixed */) =>
70+
jest
71+
.spyOn(childProcess, 'execSync')
72+
.mockReturnValue(Buffer.from(JSON.stringify(obj)));
73+
74+
afterEach(() => {
75+
jest.restoreAllMocks();
76+
});
77+
78+
test('maps ios / ios-simulator slices to their stub recipes', () => {
79+
mockPlist({
80+
AvailableLibraries: [
81+
{SupportedPlatform: 'ios', SupportedArchitectures: ['arm64']},
82+
{
83+
SupportedPlatform: 'ios',
84+
SupportedPlatformVariant: 'simulator',
85+
SupportedArchitectures: ['arm64', 'x86_64'],
86+
},
87+
],
88+
});
89+
const slices = stubSlicesFromXcframework('/fake.xcframework');
90+
expect(slices).toEqual([
91+
{name: 'ios', sdk: 'iphoneos', targets: ['arm64-apple-ios15.0']},
92+
{
93+
name: 'ios-simulator',
94+
sdk: 'iphonesimulator',
95+
targets: [
96+
'arm64-apple-ios15.0-simulator',
97+
'x86_64-apple-ios15.0-simulator',
98+
],
99+
},
100+
]);
101+
});
102+
103+
test('throws for an unknown slice, pointing at PLATFORM_STUB_RECIPES', () => {
104+
mockPlist({
105+
AvailableLibraries: [
106+
{SupportedPlatform: 'watchos', SupportedArchitectures: ['arm64']},
107+
],
108+
});
109+
expect(() => stubSlicesFromXcframework('/fake.xcframework')).toThrow(
110+
/no stub recipe for slice 'watchos'[\s\S]*PLATFORM_STUB_RECIPES/,
111+
);
112+
});
113+
});

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

Lines changed: 58 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -129,45 +129,50 @@ function composeHeadersOnlyXcframework(
129129
slices /*: Array<StubSlice> */,
130130
) /*: string */ {
131131
const work = fs.mkdtempSync(path.join(outDir, '.stub-work-'));
132-
fs.writeFileSync(
133-
path.join(work, 'stub.c'),
134-
`// ${name} is headers-only; this stub satisfies xcframework tooling.\n` +
135-
`static int ${name}Stub __attribute__((unused)) = 0;\n`,
136-
);
137-
const libs = slices.map(slice => {
138-
const sdkPath = execSync(`xcrun --sdk ${slice.sdk} --show-sdk-path`)
139-
.toString()
140-
.trim();
141-
const thins = slice.targets.map((t, i) => {
142-
const obj = path.join(work, `stub-${slice.name}-${i}.o`);
143-
execSync(
144-
`xcrun clang -c -target ${t} -isysroot "${sdkPath}" "${path.join(work, 'stub.c')}" -o "${obj}"`,
145-
);
146-
const lib = path.join(work, `stub-${slice.name}-${i}.a`);
147-
execSync(`xcrun libtool -static -o "${lib}" "${obj}" 2>/dev/null`);
148-
return lib;
132+
// try/finally so an xcrun/xcodebuild failure mid-compose doesn't leave the
133+
// .stub-work-* staging dir behind in outDir.
134+
try {
135+
fs.writeFileSync(
136+
path.join(work, 'stub.c'),
137+
`// ${name} is headers-only; this stub satisfies xcframework tooling.\n` +
138+
`static int ${name}Stub __attribute__((unused)) = 0;\n`,
139+
);
140+
const libs = slices.map(slice => {
141+
const sdkPath = execSync(`xcrun --sdk ${slice.sdk} --show-sdk-path`)
142+
.toString()
143+
.trim();
144+
const thins = slice.targets.map((t, i) => {
145+
const obj = path.join(work, `stub-${slice.name}-${i}.o`);
146+
execSync(
147+
`xcrun clang -c -target ${t} -isysroot "${sdkPath}" "${path.join(work, 'stub.c')}" -o "${obj}"`,
148+
);
149+
const lib = path.join(work, `stub-${slice.name}-${i}.a`);
150+
execSync(`xcrun libtool -static -o "${lib}" "${obj}" 2>/dev/null`);
151+
return lib;
152+
});
153+
const outLib = path.join(work, `lib${name}-${slice.name}.a`);
154+
if (thins.length === 1) {
155+
fs.copyFileSync(thins[0], outLib);
156+
} else {
157+
execSync(
158+
`xcrun lipo -create ${thins.map(l => `"${l}"`).join(' ')} -output "${outLib}"`,
159+
);
160+
}
161+
return outLib;
149162
});
150-
const outLib = path.join(work, `lib${name}-${slice.name}.a`);
151-
if (thins.length === 1) {
152-
fs.copyFileSync(thins[0], outLib);
153-
} else {
154-
execSync(
155-
`xcrun lipo -create ${thins.map(l => `"${l}"`).join(' ')} -output "${outLib}"`,
156-
);
157-
}
158-
return outLib;
159-
});
160163

161-
const outXcfw = path.join(outDir, `${name}.xcframework`);
162-
fs.rmSync(outXcfw, {recursive: true, force: true});
163-
execSync(
164-
`xcodebuild -create-xcframework ` +
165-
libs.map(l => `-library "${l}" -headers "${stage}"`).join(' ') +
166-
` -output "${outXcfw}"`,
167-
{stdio: 'pipe'},
168-
);
169-
fs.rmSync(work, {recursive: true, force: true});
170-
return outXcfw;
164+
const outXcfw = path.join(outDir, `${name}.xcframework`);
165+
fs.rmSync(outXcfw, {recursive: true, force: true});
166+
execSync(
167+
`xcodebuild -create-xcframework ` +
168+
libs.map(l => `-library "${l}" -headers "${stage}"`).join(' ') +
169+
` -output "${outXcfw}"`,
170+
{stdio: 'pipe'},
171+
);
172+
return outXcfw;
173+
} finally {
174+
fs.rmSync(work, {recursive: true, force: true});
175+
}
171176
}
172177

173178
const DEPS_HEADERS_XCFRAMEWORK_NAME = 'ReactNativeDependenciesHeaders';
@@ -211,18 +216,24 @@ function buildDepsHeadersXcframework(
211216
}
212217

213218
const stage = fs.mkdtempSync(path.join(outDir, '.deps-headers-stage-'));
214-
for (const ns of namespaces) {
215-
execSync(
216-
`/bin/cp -Rc "${path.join(depsHeaders, ns)}" "${path.join(stage, ns)}"`,
219+
// try/finally so a cp/compose failure doesn't leave the .deps-headers-stage-*
220+
// dir behind in outDir (= third-party/ for the deps path).
221+
let outXcfw;
222+
try {
223+
for (const ns of namespaces) {
224+
execSync(
225+
`/bin/cp -Rc "${path.join(depsHeaders, ns)}" "${path.join(stage, ns)}"`,
226+
);
227+
}
228+
outXcfw = composeHeadersOnlyXcframework(
229+
outDir,
230+
DEPS_HEADERS_XCFRAMEWORK_NAME,
231+
stage,
232+
slices,
217233
);
234+
} finally {
235+
fs.rmSync(stage, {recursive: true, force: true});
218236
}
219-
const outXcfw = composeHeadersOnlyXcframework(
220-
outDir,
221-
DEPS_HEADERS_XCFRAMEWORK_NAME,
222-
stage,
223-
slices,
224-
);
225-
fs.rmSync(stage, {recursive: true, force: true});
226237
console.log(
227238
`headers-xcframework: ${DEPS_HEADERS_XCFRAMEWORK_NAME}.xcframework ` +
228239
`(${slices.map(s => s.name).join(', ')}) -> ${outXcfw} ` +

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ async function prepareReactNativeDependenciesArtifactsAsync(
9898
// invisible to binaryTargets). Absent only in pre-sidecar tarballs (pinned
9999
// RN_DEP_VERSION): CocoaPods still works (the pod flattens the binary's
100100
// root Headers/), SwiftPM consumers regain it via ensureHeadersLayout.
101+
//
102+
// Slice coverage: this tarball sidecar carries ALL slices the deps prebuild
103+
// produced. The ensureHeadersLayout fallback (headers-compose.js) rebuilds it
104+
// from DEFAULT_STUB_SLICES (ios + ios-simulator only) — sufficient for a
105+
// SwiftPM iOS build, which is the only supported SPM target today. The two
106+
// paths therefore agree on the iOS slices; the fallback simply omits the
107+
// extra slices (catalyst/tvos/...) that no SPM consumer needs yet. Headers
108+
// are slice-uniform, so a consumer never sees divergent content per slice.
101109
const headersSidecarSource = path.join(
102110
path.dirname(xcframeworkSource),
103111
'ReactNativeDependenciesHeaders.xcframework',

0 commit comments

Comments
 (0)