Skip to content

feat(iOS): ReactNativeDependenciesHeaders sidecar + pure-RN ReactNativeHeaders, published to Maven#57442

Open
chrfalch wants to merge 5 commits into
chrfalch/prebuilt-deps-facadesfrom
chrfalch/deps-headers-sidecar
Open

feat(iOS): ReactNativeDependenciesHeaders sidecar + pure-RN ReactNativeHeaders, published to Maven#57442
chrfalch wants to merge 5 commits into
chrfalch/prebuilt-deps-facadesfrom
chrfalch/deps-headers-sidecar

Conversation

@chrfalch

@chrfalch chrfalch commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

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.

  1. New artifact: ReactNativeDependenciesHeaders.xcframework — the binary ReactNativeDependencies.xcframework is framework-type, so its root Headers/ is invisible to SwiftPM binaryTargets (HeadersPath is rejected on framework entries; verified empirically). The deps prebuild now emits a headers-only library-type sidecar (stub archives + per-slice Headers/ + HeadersPath — the exact ReactNativeHeaders recipe, factored into a shared headers-xcframework.js emitter) 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.

  2. ReactNativeHeaders goes pure-RN — the R2 relocation of deps namespaces (and the DEPS_NAMESPACES_NOT_RELOCATED SocketRocket exclusion list) is deleted. Relocated copies are what enabled the SocketRocket dual-copy regression (duplicate @interface / poisoned module graph under use_frameworks!); that bug class is now structurally impossible. Headers gate flipped: deps namespaces must be absent from RNH; the sidecar emitter enforces set-equality with DEPS_NAMESPACES fail-closed in both directions. On the CocoaPods side, a new ReactNativeDependenciesUtils.configure_aggregate_xcconfig injects the deps pod's Headers/ globally (aggregate + every pod target), mirroring the rncore injection — this replaces the folly/glog resolution pods previously got via the flattened React-Core-prebuilt/Headers.

  3. CI: prebuilt + dynamic-frameworks lane — the regression's exact config had no coverage (the test-ios-rntester action hard-coupled use-frameworks:true to source builds). New use-prebuilds input; test_ios_rntester's dynamic cells now consume the workflow-built prebuilt artifacts.

  4. Maven publishingReactNativeHeaders and ReactNativeDependenciesHeaders publish standalone on react-native-artifacts (classifiers reactnative-headers-*, reactnative-dependencies-headers-*); verifyArtifactsAreOnMaven now 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

  • Headers gate: include-health, structural (deps absent from RNH, byte-matched module maps), and compile smokes (React module + 14 namespace modules + Expo-shape ObjC++/Swift fixtures vs the deps include path) — ALL PASSED
  • jest: 33/33 (scripts/ios-prebuild/__tests__, incl. new sidecar set-equality tests)
  • ESLint (--max-warnings 0), Prettier, Flow (yarn flow-check): clean
  • E2E (locally built artifacts): rn-tester prebuilt static ✅, prebuilt USE_FRAMEWORKS=dynamic ✅ (the regression config — verified React-Core-prebuilt/Headers contains 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 ✅
  • Sidecar inspected: per-slice HeadersPath, 7 namespaces, slice parity with the binary
  • Publication validated end-to-end with publishReleasePublicationToMavenLocal: all 12 files + POM land with the expected classifier names

🤖 Generated with Claude Code

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Caution

Invalid Base Branch

The base branch for this PR is chrfalch/prebuilt-deps-facades, which is not main or a -stable branch.
Are you sure you want to target this branch?

@chrfalch chrfalch force-pushed the chrfalch/deps-headers-sidecar branch from 73226c2 to c01f55a Compare July 6, 2026 12:10
chrfalch and others added 5 commits July 6, 2026 18:36
…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>
@chrfalch chrfalch force-pushed the chrfalch/prebuilt-deps-facades branch from 069c350 to 017ca51 Compare July 6, 2026 16:37
@chrfalch chrfalch force-pushed the chrfalch/deps-headers-sidecar branch from 90d8f50 to 0c44552 Compare July 6, 2026 16:37
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') }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}"`, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-'));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Expo Partner: Expo Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants