feat(iOS): ReactNativeDependenciesHeaders sidecar + pure-RN ReactNativeHeaders, published to Maven#57442
Conversation
|
Caution Invalid Base Branch The base branch for this PR is |
73226c2 to
c01f55a
Compare
…idecar The binary ReactNativeDependencies.xcframework is FRAMEWORK-type: it cannot carry a HeadersPath key, so its root Headers/ dir is invisible to SwiftPM binaryTargets (verified empirically 2026-07-04 - xcodebuild rejects -headers without -library, and SwiftPM hard-rejects HeadersPath on framework entries). This adds a headers-only LIBRARY-type sidecar, ReactNativeDependenciesHeaders.xcframework, that SwiftPM auto-serves with zero flags: stub static archives paired with per-slice Headers/ holding the seven third-party namespaces (folly, glog, boost, fmt, double-conversion, fast_float, SocketRocket). - headers-xcframework.js (new): dependency-light shared emitter - composeHeadersOnlyXcframework (the stub-archive recipe factored out of buildReactNativeHeadersXcframework), stubSlicesFromXcframework (slice parity derived from the binary artifact's Info.plist, so the sidecar resolves for every platform the binary does), and buildDepsHeadersXcframework with a set-equality gate that fails closed in both directions (declared-but-missing and undeclared namespaces). - compose-framework.js: the deps compose emits and signs the sidecar right after assembling the root Headers/ (slice-uniform content). - prebuild-ios-dependencies.yml: the sidecar ships INSIDE the deps tarball (self-contained for CocoaPods) and as a standalone tarball; cache key bumped and now also hashes compose-framework.js. - reactNativeDependencies.js: consumers extract the sidecar alongside the binary; pre-sidecar tarballs (pinned RN_DEP_VERSION) warn and continue - CocoaPods is unaffected, SwiftPM recomposes it locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every namespace now has exactly ONE physical home: RN namespaces in ReactNativeHeaders, third-party deps namespaces in the ReactNativeDependenciesHeaders sidecar / the ReactNativeDependencies pod. Relocated deps copies are what made the 2026-07-03 SocketRocket regression possible (duplicate @interface + poisoned module graph under use_frameworks when a real pod coexisted with relocated copies) - that bug class is now structurally impossible, and the SocketRocket exclusion list is deleted rather than maintained. - headers-spec.js: R2 rewritten; DEPS_NAMESPACES now includes SocketRocket (= the sidecar contents = the artifact's Headers/ dirs, set-equality enforced); DEPS_NAMESPACES_NOT_RELOCATED deleted. - headers-compose.js: RNH stages RN headers only; ensureHeadersLayout also builds the sidecar from the cache slot's deps headers and returns it, so SwiftPM consumers need no published sidecar. - headers-verify.js: structural gate flipped - deps namespaces must be ABSENT from RNH; compile gates take the deps artifact's Headers on the include path (exactly how consumers resolve them). - CocoaPods: NEW ReactNativeDependenciesUtils.configure_aggregate_xcconfig injects $(PODS_ROOT)/ReactNativeDependencies/Headers into the aggregate and every pod target in prebuilt-deps mode, mirroring the rncore injection. Pods previously resolved folly/glog through the globally-injected React-Core-prebuilt/Headers flatten of RNH; after the flip the deps pod is the single global home, so the mirror injection is what keeps arbitrary pods compiling. Verified: headers gate green (include-health, structural, compile smokes vs the deps include path); rn-tester prebuilt static + dynamic (the regression config), helloworld static, source-core+prebuilt-deps (React compiled from source resolves folly via the deps pod), and a source-mode control with an unchanged dependency graph. BREAKING (prebuilt artifacts only): ReactNativeHeaders.xcframework no longer contains folly/glog/boost/fmt/double-conversion/fast_float; consumers reaching them through React-Core-prebuilt/Headers must use the ReactNativeDependencies pod / ReactNativeDependenciesHeaders sidecar paths instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prebuilt + use_frameworks is the exact configuration of the 2026-07-03
SocketRocket dual-copy regression, and no CI lane covered it: the
test-ios-rntester action hard-coupled use-frameworks:true to building
from source. Add a use-prebuilds input ('auto' keeps the historical
coupling for existing callers) and pass use-prebuilds:true from
test_ios_rntester, so its dynamic-frameworks matrix cells now consume
the prebuilt ReactCore + ReactNativeDependencies artifacts built in the
same workflow run. Source-built dynamic frameworks stay covered by
test_ios_rntester_dynamic_frameworks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eaders to Maven
Publish the two headers-only xcframeworks as standalone artifacts on
the react-native-artifacts coordinate so SwiftPM consumers can wire
them as separate binaryTargets (they also keep shipping inside the
ReactCore / ReactNativeDependencies tarballs for CocoaPods):
- external-artifacts/build.gradle.kts: four new PublishArtifacts with
classifiers reactnative-headers-{debug,release} and
reactnative-dependencies-headers-{debug,release}.
- prebuild-ios-core.yml: upload the standalone
ReactNativeHeaders<flavor>.xcframework.tar.gz the compose step
already produces (previously only bundled inside the ReactCore
tarball); include it in the workflow cache.
- build-npm-package action: download ReactNativeHeaders* artifacts
(ReactNativeDependenciesHeaders* already rides the existing
ReactNativeDependencies* pattern).
- verifyArtifactsAreOnMaven.js: HEAD-check every classifier tarball,
not just the POM - a release with a missing classifier artifact now
fails verification instead of passing silently.
Validated locally end-to-end with publishReleasePublicationToMavenLocal:
all 12 files + POM land on com.facebook.react:react-native-artifacts
with the expected classifier names.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The verifier now HEAD-checks the POM plus all eight classifier tarballs instead of GETting only the POM; the mocks/assertions follow, and a new case covers a missing classifier behind an existing POM (the exact gap the classifier checks were added to catch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
069c350 to
017ca51
Compare
90d8f50 to
0c44552
Compare
| path: | | ||
| packages/react-native/third-party/ | ||
| key: v3-ios-dependencies-xcframework-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js') }} | ||
| key: v4-ios-dependencies-xcframework-${{ matrix.flavor }}-${{ hashfiles('scripts/releases/ios-prebuild/configuration.js', 'scripts/releases/ios-prebuild/compose-framework.js') }} |
There was a problem hiding this comment.
Major: The deps sidecar cache key was bumped v3→v4 and hashes configuration.js + compose-framework.js, but the sidecar is generated via two cross-package requires — ios-prebuild/headers-xcframework.js (the emitter) and ios-prebuild/headers-spec.js (DEPS_NAMESPACES) — neither of which is in this key's hashFiles. A future edit to DEPS_NAMESPACES or the emitter that doesn't also touch configuration.js/compose-framework.js will serve a stale cached sidecar on main. Please add those two paths to the hashFiles list (both cache-key occurrences).
| 'https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/'; | ||
| const ARTIFACT_NAME = 'react-native-artifacts-'; | ||
|
|
||
| // The classifier-suffixed tarballs attached to the react-native-artifacts |
There was a problem hiding this comment.
Minor: The comment says this HEAD-checks 'every classifier tarball,' but build.gradle.kts publishes 12 tarballs and only 8 are listed — the 4 dSYM classifiers (core/deps dSYM debug+release) are omitted. Defensible to skip dSYMs, but the 'every classifier' wording is misleading; either add them or reword to 'the primary xcframework tarballs (dSYMs excluded).'
| * Derives stub slices matching an existing (binary) xcframework's slice set, | ||
| * so a headers-only sidecar resolves for every platform the binary does. | ||
| */ | ||
| function stubSlicesFromXcframework( |
There was a problem hiding this comment.
Minor (test gap): stubSlicesFromXcframework is the most complex new logic here — plist parsing, SupportedPlatform/Variant→key mapping, PLATFORM_STUB_RECIPES lookup, and the unknown-slice throw — and is entirely untested (headers-xcframework-test.js only covers the set-equality gate). The plist shape is a pure function of mockable plutil JSON, so an unknown-slice/variant test would be cheap and would guard the 'add it to PLATFORM_STUB_RECIPES' contract.
| 'ReactNativeDependenciesHeaders.xcframework', | ||
| ); | ||
| if (fs.existsSync(headersSidecarSource)) { | ||
| execSync(`cp -R "${headersSidecarSource}" "${artifactsPath}"`, { |
There was a problem hiding this comment.
Minor: There are two sidecar sources for SwiftPM consumers — this copies the full-slice ReactNativeDependenciesHeaders.xcframework from the tarball, while ensureHeadersLayout in headers-compose.js builds its own with DEFAULT_STUB_SLICES (ios + ios-sim only, no catalyst/tvos/xros/macos). Worth confirming the two paths don't diverge in slice coverage for a real SwiftPM build (Package.swift wiring isn't in this diff), or adding a comment reconciling them.
| stage /*: string */, | ||
| slices /*: Array<StubSlice> */, | ||
| ) /*: string */ { | ||
| const work = fs.mkdtempSync(path.join(outDir, '.stub-work-')); |
There was a problem hiding this comment.
Minor: The staging temp dirs (.deps-headers-stage-* / .stub-work-*) are created under outDir; an execSync failure mid-compose (e.g. an xcrun error) leaves them behind in outDir (= third-party for the deps path). A try/finally cleanup would be tidier.
| 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: 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/**/*') }} |
There was a problem hiding this comment.
Nit: The core artifact set changed (added ReactNativeHeaders...xcframework.tar.gz) but this key prefix stayed v2-. It's safe because the key's hashFiles component changes with this PR, but a matching prefix bump would make the 'artifact set changed' intent explicit rather than relying implicitly on the hash (the deps side was deliberately bumped v3→v4).
| echo "enabled=true" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| else | ||
| echo "enabled=${{ inputs.use-prebuilds }}" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
Nit: The else branch passes use-prebuilds through verbatim, so any value other than auto/true/false silently means 'disabled' downstream. Fine for current callers, just brittle.
| 'fmt', | ||
| 'double-conversion', | ||
| 'fast_float', | ||
| 'SocketRocket', |
There was a problem hiding this comment.
Nice — collapsing DEPS_NAMESPACES_NOT_RELOCATED into DEPS_NAMESPACES and giving SocketRocket a single physical home structurally eliminates the dual-copy class; spec/verify/inventory now all derive from one list.
Summary
Step 2 of the prebuilt-deps roadmap: ship the deps headers as a SwiftPM-ready, self-contained artifact and make every header namespace have exactly one physical home.
New artifact:
ReactNativeDependenciesHeaders.xcframework— the binaryReactNativeDependencies.xcframeworkis framework-type, so its rootHeaders/is invisible to SwiftPM binaryTargets (HeadersPathis rejected on framework entries; verified empirically). The deps prebuild now emits a headers-only library-type sidecar (stub archives + per-sliceHeaders/+HeadersPath— the exactReactNativeHeadersrecipe, factored into a sharedheaders-xcframework.jsemitter) carrying all seven deps namespaces incl. SocketRocket, with slice parity derived from the binary artifact's Info.plist. Ships inside the deps tarball and standalone.ReactNativeHeadersgoes pure-RN — the R2 relocation of deps namespaces (and theDEPS_NAMESPACES_NOT_RELOCATEDSocketRocket exclusion list) is deleted. Relocated copies are what enabled the SocketRocket dual-copy regression (duplicate@interface/ poisoned module graph underuse_frameworks!); that bug class is now structurally impossible. Headers gate flipped: deps namespaces must be absent from RNH; the sidecar emitter enforces set-equality withDEPS_NAMESPACESfail-closed in both directions. On the CocoaPods side, a newReactNativeDependenciesUtils.configure_aggregate_xcconfiginjects the deps pod'sHeaders/globally (aggregate + every pod target), mirroring the rncore injection — this replaces the folly/glog resolution pods previously got via the flattenedReact-Core-prebuilt/Headers.CI: prebuilt + dynamic-frameworks lane — the regression's exact config had no coverage (the
test-ios-rntesteraction hard-coupleduse-frameworks:trueto source builds). Newuse-prebuildsinput;test_ios_rntester's dynamic cells now consume the workflow-built prebuilt artifacts.Maven publishing —
ReactNativeHeadersandReactNativeDependenciesHeaderspublish standalone onreact-native-artifacts(classifiersreactnative-headers-*,reactnative-dependencies-headers-*);verifyArtifactsAreOnMavennow HEAD-checks every classifier tarball instead of only the POM.Stacked on #57440. The SwiftPM preview (#57332) rebases on top and wires the sidecar as its 5th binaryTarget.
Changelog:
[IOS] [CHANGED] - Prebuilt artifacts: ReactNativeHeaders is pure-RN; third-party deps headers ship in the new ReactNativeDependenciesHeaders.xcframework sidecar (and the ReactNativeDependencies pod), published standalone to Maven
Test Plan
scripts/ios-prebuild/__tests__, incl. new sidecar set-equality tests)--max-warnings 0), Prettier, Flow (yarn flow-check): cleanUSE_FRAMEWORKS=dynamic✅ (the regression config — verifiedReact-Core-prebuilt/Headerscontains no deps namespaces and the deps pod serves all seven), helloworld static ✅, source-core + prebuilt-deps ✅ (React compiled from source resolves folly via the deps pod), source-mode control with unchanged dependency graph ✅HeadersPath, 7 namespaces, slice parity with the binarypublishReleasePublicationToMavenLocal: all 12 files + POM land with the expected classifier names🤖 Generated with Claude Code