-
Notifications
You must be signed in to change notification settings - Fork 25.2k
feat(iOS): serve third-party deps from the prebuilt ReactNativeDependencies pod via dependency-only facades #57440
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: chrfalch/prebuilt-resources
Are you sure you want to change the base?
Changes from all commits
09348a9
b56596f
1f746e8
0856cc9
ef5ae80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # Prebuilt ReactNativeDependencies — self-serving headers + deps facades | ||
|
|
||
| How the third-party C/C++ deps (`RCT-Folly`, `glog`, `boost`, | ||
| `DoubleConversion`, `fmt`, `fast_float`, `SocketRocket`) are served when | ||
| `ReactNativeDependenciesUtils.build_react_native_deps_from_source()` is false | ||
| (prebuilt-deps mode). Source-deps mode is unaffected by everything below. | ||
|
|
||
| ## Pod-served headers, CocoaPods only (`rndependencies.rb`) | ||
|
|
||
| In prebuilt-deps mode the `ReactNativeDependencies` POD (CocoaPods) is the | ||
| single authority for the third-party deps: compiled code lives in its | ||
| xcframework binary, and the artifact's own | ||
| `Headers/{folly,glog,boost,fmt,double-conversion,fast_float,SocketRocket}` are | ||
| flattened into the pod's `Headers/` by the podspec's `prepare_command`. | ||
| Consumers resolve bare `<folly/...>` / `<SocketRocket/...>` via CocoaPods | ||
| public-header linkage from `s.dependency "ReactNativeDependencies"`, plus an | ||
| explicit `HEADER_SEARCH_PATHS` entry | ||
| (`$(PODS_ROOT)/ReactNativeDependencies/Headers`). The real source pods are | ||
| neither depended on nor searched. | ||
|
|
||
| NOTE: this is a CocoaPods-level contract. The deps XCFRAMEWORK itself is NOT | ||
| self-serving: it is framework-type without `HeadersPath`, so its root `Headers/` | ||
| is invisible to SPM binaryTargets (verified 2026-07-04 — `HeadersPath` is | ||
| rejected on framework entries). In SPM the six C++ namespaces are served by | ||
| ReactNativeHeaders.xcframework; serving them from the deps side requires the | ||
| phase-2 headers-only library sidecar (see rn-deps-self-serving plan). | ||
|
|
||
| ## Why SocketRocket is vended here | ||
|
|
||
| React-Core compiled from source (source-core + prebuilt-deps mix) imports | ||
| `<SocketRocket/SRWebSocket.h>` (`RCTReconnectingWebSocket.m`), and in | ||
| prebuilt-deps mode there is NO real SocketRocket pod in the graph — the artifact | ||
| is the sole supplier. This does not reintroduce the 2026-07-03 dual-copy | ||
| regression: that bug relocated SocketRocket copies onto every pod's search path | ||
| (via ReactNativeHeaders → React-Core-prebuilt) while a REAL SocketRocket pod | ||
| coexisted. Here there is exactly one physical copy and no coexisting pod. | ||
|
|
||
| ## Deps facades (`rndeps_facades.rb`, declared in `react_native_pods.rb`) | ||
|
|
||
| The real source pods are only declared in the deps-from-source branch, so in | ||
| prebuilt-deps mode a community podspec's hardcoded `s.dependency "RCT-Folly"` / | ||
| `"RCT-Folly/Fabric"` / `"glog"` would resolve from the CocoaPods trunk and | ||
| compile from source next to the prebuilt binary. `RNDepsFacades` generates | ||
| dependency-only facade podspecs (`build/rndeps-facades/<Name>/`), installed as | ||
| LOCAL pods (`:path`, so Podfile-local resolution beats trunk, nothing fetched): | ||
| no sources, no headers, single dependency on `ReactNativeDependencies`. | ||
| Versions + subspecs are DERIVED from the real podspecs in | ||
| `third-party-podspecs/` (RCT-Folly keeps `/Default` + `/Fabric`, | ||
| `default_subspecs = ["Default"]`). SocketRocket has no local podspec — its | ||
| facade version is SYNTHESIZED from | ||
| `Helpers::Constants::socket_rocket_config[:version]`, fail-closed if absent. | ||
| `:modular_headers` is intentionally dropped on facade declarations: a | ||
| dependency-only placeholder builds no module; consumers get modules from | ||
| `ReactNativeDependencies`. | ||
|
|
||
| ## Mode × supplier table | ||
|
|
||
| | core × deps | real 3P pods in graph | SocketRocket headers supplier | | ||
| | ------------------- | ----------------------------------------------- | ------------------------------- | | ||
| | source + source | yes (`react_native_pods.rb` deps-source branch) | real pod | | ||
| | source + prebuilt | no | RNDeps artifact (sole supplier) | | ||
| | prebuilt + source | yes | real pod | | ||
| | prebuilt + prebuilt | no | RNDeps artifact | | ||
|
|
||
| ## SocketRocket privacy manifest | ||
|
|
||
| Upstream SocketRocket ships NO privacy manifest, and the deps artifact | ||
| historically carried bundles only for boost/folly/glog. Fixed alongside this | ||
| work: the deps prebuild (`scripts/releases/ios-prebuild/configuration.js`) now | ||
| embeds `ReactNativeDependencies_SocketRocket.bundle/PrivacyInfo.xcprivacy`, | ||
| sourced from an RN-authored manifest at | ||
| `scripts/releases/ios-prebuild/resources/SocketRocket/PrivacyInfo.xcprivacy` | ||
| (accurate-empty: SocketRocket uses no Required Reason APIs). Facades remain | ||
| resource-free by design. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,10 +47,22 @@ def add_rn_third_party_dependencies(s) | |
| header_search_paths << "$(PODS_ROOT)/SocketRocket" | ||
| header_search_paths << "$(PODS_ROOT)/RCT-Folly" | ||
|
|
||
| current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] = header_search_paths | ||
| # uniq so a second call on the same spec can't duplicate entries. | ||
| current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] = header_search_paths.uniq | ||
| else | ||
| # Prebuilt-deps mode: this pod SELF-SERVES the third-party headers from its | ||
| # own xcframework (incl. SocketRocket - sole supplier in this mode). See | ||
| # scripts/cocoapods/__docs__/prebuilt-deps.md for the full contract. | ||
| s.dependency "ReactNativeDependencies" | ||
| current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] ||= [] << "$(PODS_ROOT)/ReactNativeDependencies" | ||
|
|
||
| header_search_paths = current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] || [] | ||
| if header_search_paths.is_a?(String) | ||
| header_search_paths = header_search_paths.split(" ") | ||
| end | ||
| # Artifact headers are flattened into the pod-local Headers/ by the podspec | ||
| # prepare_command (see __docs__/prebuilt-deps.md). | ||
| header_search_paths << "$(PODS_ROOT)/ReactNativeDependencies/Headers" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: This appends the search path unconditionally; if |
||
| current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] = header_search_paths | ||
| end | ||
|
|
||
| s.pod_target_xcconfig = current_pod_target_xcconfig | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| # Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| # | ||
| # This source code is licensed under the MIT license found in the | ||
| # LICENSE file in the root directory of this source tree. | ||
|
|
||
| require 'json' | ||
| require 'fileutils' | ||
| # Self-contained against require ordering: this module reads | ||
| # Helpers::Constants.socket_rocket_config. react_native_pods.rb normally loads | ||
| # helpers.rb first, but requiring it here (idempotent) removes that implicit | ||
| # dependency. The defined? guard at the use site stays as a backstop. | ||
| require_relative './helpers' | ||
|
|
||
| # Dependency-only facade podspecs for the third-party deps in prebuilt-deps | ||
| # mode (deps-side analogue of RNCoreFacades). Design + rationale: | ||
| # scripts/cocoapods/__docs__/prebuilt-deps.md | ||
| module RNDepsFacades | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Major (test coverage): This entire new module ships without tests, and several branchy behaviors are unit-testable without a full pod install: |
||
| # The name of the umbrella prebuilt-deps pod every facade depends on. Its pod | ||
| # self-serves the third-party headers + carries the binary (see | ||
| # rndependencies.rb, ReactNativeDependencies.podspec). | ||
| DEPS_POD = "ReactNativeDependencies" | ||
|
|
||
| # pod name => podspec path (relative to the react-native package root), or | ||
| # :synthesized for a pod with no local podspec (SocketRocket). Version + | ||
| # subspecs + default_subspecs are DERIVED from the real podspec where one | ||
| # exists; synthesized entries derive their version from a constant. | ||
| FACADE_PODS = { | ||
| "RCT-Folly" => "third-party-podspecs/RCT-Folly.podspec", | ||
| "glog" => "third-party-podspecs/glog.podspec", | ||
| "boost" => "third-party-podspecs/boost.podspec", | ||
| "DoubleConversion" => "third-party-podspecs/DoubleConversion.podspec", | ||
| "fmt" => "third-party-podspecs/fmt.podspec", | ||
| "fast_float" => "third-party-podspecs/fast_float.podspec", | ||
| "SocketRocket" => :synthesized, | ||
| } | ||
|
|
||
| # Sub-directory (relative to the install root) that holds the generated | ||
| # deps facades. Kept separate from RNCoreFacades' `build/rncore-facades` so | ||
| # the two families never collide. | ||
| FACADE_RELDIR = File.join("build", "rndeps-facades") | ||
|
|
||
| @@install_root = nil | ||
|
|
||
| # Generates the facade podspecs and returns the base directory holding them. | ||
| # Each facade gets its OWN sub-directory containing a single | ||
| # `<Name>.podspec.json`, so it can be installed as a LOCAL pod via | ||
| # `:path => <dir>` (PathSource uses the spec in place and never downloads | ||
| # `spec.source`). Idempotent; safe to call once per `pod install`. | ||
| # | ||
| # `react_native_path` locates the real third-party podspecs we mirror. | ||
| # version + subspecs + default_subspecs are DERIVED from the real spec (or, | ||
| # for SocketRocket, synthesized from the socket_rocket_config version) so the | ||
| # facade matches the source pod's spec/subspec SHAPE. It is not fully | ||
| # graph-equivalent: every derived subspec depends only on | ||
| # ReactNativeDependencies, so intra-pod subspec deps (e.g. RCT-Folly/Fabric | ||
| # -> RCT-Folly/Default) are not reproduced — harmless here because the deps | ||
| # are all declared explicitly in react_native_pods.rb. NO source_files and | ||
| # NO headers are emitted — the ReactNativeDependencies pod supplies both. A | ||
| # facaded pod whose real podspec can't be read is a hard error (see | ||
| # load_real_spec) — silently shipping an empty facade would hide drift. | ||
| def self.generate(react_native_path, install_root, ios_version) | ||
| @@install_root = install_root.to_s | ||
| abs_base = File.join(@@install_root, FACADE_RELDIR) | ||
| FileUtils.mkdir_p(abs_base) | ||
| FACADE_PODS.each do |name, podspec_rel_path| | ||
| dir = File.join(abs_base, name) | ||
| FileUtils.mkdir_p(dir) | ||
|
|
||
| if podspec_rel_path == :synthesized | ||
| spec = synthesized_spec(name, ios_version) | ||
| else | ||
| podspec_path = File.join(react_native_path.to_s, podspec_rel_path) | ||
| real = load_real_spec(podspec_path, name) | ||
| spec = derived_spec(name, real, ios_version) | ||
| end | ||
|
|
||
| File.write(File.join(dir, "#{name}.podspec.json"), JSON.pretty_generate(spec)) | ||
| end | ||
| abs_base | ||
| end | ||
|
|
||
| # Facade dir for `<name>`, RELATIVE to the install root — pass to `pod :path =>`. | ||
| # Relative (not absolute) so the path CocoaPods records in Podfile.lock is | ||
| # portable rather than machine-specific. | ||
| def self.facade_path(name) | ||
| File.join(FACADE_RELDIR, name) | ||
| end | ||
|
|
||
| # Base spec skeleton shared by derived + synthesized facades: dependency-only, | ||
| # no source_files, no headers. Depends solely on ReactNativeDependencies. | ||
| def self.base_spec(name, version, ios_version) | ||
| { | ||
| "name" => name, | ||
| "version" => version, | ||
| "summary" => "Prebuilt facade for #{name} (code + headers live in #{DEPS_POD}).", | ||
| "homepage" => "https://reactnative.dev/", | ||
| "license" => "MIT", | ||
| "authors" => "Meta Platforms, Inc. and its affiliates", | ||
| "platforms" => { "ios" => ios_version }, | ||
| # Required podspec attribute, but never fetched: installed as a LOCAL | ||
| # pod (`:path => <dir>`), which uses this spec in place and ships no | ||
| # source_files. Placeholder only. | ||
| "source" => { "git" => "https://github.com/facebook/react-native.git" }, | ||
| "dependencies" => { DEPS_POD => [] }, | ||
| } | ||
| end | ||
| private_class_method :base_spec | ||
|
|
||
| # Facade derived from a real third-party podspec: version + subspecs + | ||
| # default_subspecs mirror the real spec so a bare `pod '<Name>'` and any | ||
| # `pod '<Name>/<Subspec>'` resolve to the SAME graph (e.g. RCT-Folly's | ||
| # bare + /Default + /Fabric). Each subspec is also dependency-only and | ||
| # depends on ReactNativeDependencies. | ||
| # | ||
| # NOTE: resources (e.g. RCT-Folly's PrivacyInfo.xcprivacy) are intentionally | ||
| # NOT carried. In prebuilt-deps mode the third-party code — and its privacy | ||
| # manifest — is embedded in the ReactNativeDependencies artifact; the facade | ||
| # only needs to declare the dependency (see the design note in the PR). | ||
| def self.derived_spec(name, real, ios_version) | ||
| spec = base_spec(name, real.version.to_s, ios_version) | ||
|
|
||
| defaults = Array(real.default_subspecs) | ||
| spec["default_subspecs"] = defaults unless defaults.empty? | ||
|
|
||
| subspecs = derive_subspecs(real) | ||
| unless subspecs.empty? | ||
| spec["subspecs"] = subspecs.map do |ss| | ||
| { "name" => ss, "dependencies" => { DEPS_POD => [] } } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: Every derived subspec is emitted depending only on ReactNativeDependencies, so the facade |
||
| end | ||
| end | ||
|
|
||
| spec | ||
| end | ||
| private_class_method :derived_spec | ||
|
|
||
| # Facade synthesized for a pod with NO local podspec (SocketRocket, a trunk | ||
| # pod). Version comes from Helpers::Constants::socket_rocket_config; no | ||
| # subspecs. A missing/blank constant is a hard error rather than a silent | ||
| # versionless facade — a bare `pod 'SocketRocket'` in the source path is | ||
| # `"~> #{socket_rocket_config[:version]}"`, so the facade MUST carry a version | ||
| # that satisfies that constraint. | ||
| def self.synthesized_spec(name, ios_version) | ||
| version = synthesized_version(name) | ||
| base_spec(name, version, ios_version) | ||
| end | ||
| private_class_method :synthesized_spec | ||
|
|
||
| # Resolves the synthesized version for a no-podspec facade. Fail-closed on a | ||
| # missing constant/version. Only SocketRocket is synthesized today. | ||
| def self.synthesized_version(name) | ||
| case name | ||
| when "SocketRocket" | ||
| unless defined?(Helpers::Constants) && Helpers::Constants.respond_to?(:socket_rocket_config) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: This references |
||
| raise "[RNDepsFacades] Cannot synthesize facade for '#{name}': " \ | ||
| "Helpers::Constants.socket_rocket_config is unavailable." | ||
| end | ||
| version = Helpers::Constants.socket_rocket_config[:version] | ||
| if version.nil? || version.to_s.strip.empty? | ||
| raise "[RNDepsFacades] Cannot synthesize facade for '#{name}': " \ | ||
| "socket_rocket_config[:version] is missing or empty." | ||
| end | ||
| version.to_s | ||
| else | ||
| raise "[RNDepsFacades] No synthesized version rule for facaded pod '#{name}'. " \ | ||
| "Add one to synthesized_version or give it a real podspec in FACADE_PODS." | ||
| end | ||
| end | ||
| private_class_method :synthesized_version | ||
|
|
||
| # Loads the real podspec so we can mirror its structure. A facaded pod with a | ||
| # declared podspec path MUST have a readable real podspec — if it's missing or | ||
| # unparseable we raise rather than ship an empty facade (which would silently | ||
| # drop subspecs / the version, the very drift this mechanism prevents). | ||
| def self.load_real_spec(path, name) | ||
| unless File.exist?(path) | ||
| raise "[RNDepsFacades] Real podspec for facaded pod '#{name}' not found at #{path}. " \ | ||
| "Update FACADE_PODS in rndeps_facades.rb if the podspec moved." | ||
| end | ||
| Pod::Specification.from_file(path) | ||
| rescue => e | ||
| raise "[RNDepsFacades] Failed to read real podspec for facaded pod '#{name}' at #{path}: #{e.message}" | ||
| end | ||
| private_class_method :load_real_spec | ||
|
|
||
| # Library (non-test, non-app) subspec names of the real spec, so third-party | ||
| # libs depending on `<pod>/<subspec>` (e.g. `RCT-Folly/Fabric`) keep | ||
| # resolving. Derived, never hand-listed. | ||
| def self.derive_subspecs(real) | ||
| real.subspecs | ||
| .reject { |ss| ss.test_specification? || (ss.respond_to?(:app_specification?) && ss.app_specification?) } | ||
| .map(&:base_name) | ||
| end | ||
| private_class_method :derive_subspecs | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -161,6 +161,13 @@ function checkExistingVersion( | |
| dependencyLog( | ||
| `React Native Dependencies found on disk at: ${artifactsPath}.\nNo version file has been found. We are going to use it anyway, but there might be some unexpected behaviors.`, | ||
| ); | ||
| // Honor the message above: an artifact without a version marker is a | ||
| // locally-staged one (e.g. a freshly composed deps build). Use it as-is. | ||
| // NOTE: this returns BEFORE the version.txt write below, so a | ||
| // locally-staged artifact never gains a marker — every later run re-hits | ||
| // this branch. That is intentional (don't clobber a hand-staged build), | ||
| // but downstream code must not assume version.txt exists here. | ||
| return true; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: This early |
||
| } | ||
| } else { | ||
| dependencyLog('React Native Dependencies not found on disk'); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,13 +53,13 @@ Pod::Spec.new do |spec| | |
| # Check if XCFRAMEWORK_PATH is empty | ||
| if [ -z "$XCFRAMEWORK_PATH" ]; then | ||
| echo "ERROR: XCFRAMEWORK_PATH is empty." | ||
| exit 0 | ||
| exit 1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — this is a real bug fix. The old prepare_command printed 'ERROR' then exited 0, silently producing a broken install with no flattened headers; failing loudly is correct. |
||
| fi | ||
|
|
||
| # Check if HEADERS_PATH is empty | ||
| if [ -z "$HEADERS_PATH" ]; then | ||
| echo "ERROR: HEADERS_PATH is empty." | ||
| exit 0 | ||
| exit 1 | ||
| fi | ||
|
|
||
| cp -R "$HEADERS_PATH/." Headers | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice — this mirrors the from-source branch's String-vs-Array handling and fixes the prior
||= [] << ...idiom that never appended when the key already existed.