|
| 1 | +# Copyright (c) Meta Platforms, Inc. and affiliates. |
| 2 | +# |
| 3 | +# This source code is licensed under the MIT license found in the |
| 4 | +# LICENSE file in the root directory of this source tree. |
| 5 | + |
| 6 | +require 'json' |
| 7 | +require 'fileutils' |
| 8 | + |
| 9 | +# Facade podspecs for the prebuilt React Native Core path. |
| 10 | +# |
| 11 | +# In prebuilt mode the compiled code AND headers for the React core pods live |
| 12 | +# entirely inside React.xcframework + React-Core-prebuilt (which flattens the |
| 13 | +# ReactNativeHeaders namespaces into its Headers/). Re-installing the SOURCE |
| 14 | +# podspecs in that mode is what makes them ship duplicate headers that shadow the |
| 15 | +# prebuilt artifact (via HEADER_SEARCH_PATHS, CocoaPods .hmap header maps, and |
| 16 | +# the all-product-headers VFS overlay) and break the React framework's clang |
| 17 | +# explicit-module precompile. |
| 18 | +# |
| 19 | +# Instead we install dependency-only FACADE podspecs for those names: they ship |
| 20 | +# no source files and no headers, so CocoaPods makes them PBXAggregateTarget |
| 21 | +# placeholders (should_build? == false) and nothing is laid down to shadow. Each |
| 22 | +# facade depends on React-Core-prebuilt so its consumers transitively pick up the |
| 23 | +# prebuilt framework + headers. The pod NAMES still resolve, so ReactCodegen, |
| 24 | +# third-party modules, and RN's own podspec graph keep resolving `React-Core`, |
| 25 | +# `Yoga`, `React-Core/Default`, etc. |
| 26 | +# |
| 27 | +# MAINTENANCE MODEL: the set of facaded pods is explicit (FACADE_PODS) so the |
| 28 | +# prebuilt rollout can be staged, but each facade's VERSION and SUBSPECS are |
| 29 | +# DERIVED from the real podspec at `pod install` time (Pod::Specification.from_file). |
| 30 | +# That removes the drift risk that would otherwise bite third-party libraries: |
| 31 | +# if React adds/renames `React-Core/<Subspec>`, the facade exposes it |
| 32 | +# automatically — nobody has to hand-maintain a parallel subspec list. |
| 33 | +# |
| 34 | +# This is staged: phase 1 facades a small set and KEEPS the existing |
| 35 | +# podspec_sources / add_rncore_dependency / configure_aggregate_xcconfig / |
| 36 | +# -fmodule-map-file machinery. The set is expanded until the cold prebuilt |
| 37 | +# build passes; the distributed prebuilt helpers are only deleted afterwards. |
| 38 | +module RNCoreFacades |
| 39 | + # pod name => podspec path (relative to the react-native package root). |
| 40 | + # These are the React-core pods whose code + headers are fully provided by |
| 41 | + # the prebuilt React.xcframework / React-Core-prebuilt. Start small; expand as |
| 42 | + # the cold build surfaces more shadowing pods. (NOTE: not every caller of |
| 43 | + # add_rncore_dependency belongs here — e.g. ReactCodegen depends on the |
| 44 | + # prebuilt but still builds its own generated sources, so it is NOT a facade.) |
| 45 | + FACADE_PODS = { |
| 46 | + "React-Core" => "React-Core.podspec", |
| 47 | + "React-RCTFabric" => "React/React-RCTFabric.podspec", |
| 48 | + "React-RCTRuntime" => "React/Runtime/React-RCTRuntime.podspec", |
| 49 | + "Yoga" => "ReactCommon/yoga/Yoga.podspec", |
| 50 | + "RCTDeprecation" => "ReactApple/Libraries/RCTFoundation/RCTDeprecation/RCTDeprecation.podspec", |
| 51 | + "FBLazyVector" => "Libraries/FBLazyVector/FBLazyVector.podspec", |
| 52 | + "RCTRequired" => "Libraries/Required/RCTRequired.podspec", |
| 53 | + } |
| 54 | + |
| 55 | + # Sub-directory (relative to the install root) that holds the generated facades. |
| 56 | + FACADE_RELDIR = File.join("build", "rncore-facades") |
| 57 | + |
| 58 | + @@install_root = nil |
| 59 | + |
| 60 | + # True when `name` should be installed as a facade instead of its source podspec. |
| 61 | + def self.facade?(name) |
| 62 | + FACADE_PODS.key?(name) |
| 63 | + end |
| 64 | + |
| 65 | + # Generates the facade podspecs and returns the base directory holding them. |
| 66 | + # Each facade gets its OWN sub-directory containing a single |
| 67 | + # `<Name>.podspec.json`, so it can be installed as a LOCAL pod via |
| 68 | + # `:path => <dir>`. `:path` (PathSource) uses the spec in place and never |
| 69 | + # downloads `spec.source` — unlike `:podspec` (PodspecSource), which is an |
| 70 | + # *external* source whose `root_spec.source` CocoaPods would actually fetch |
| 71 | + # (i.e. git-clone react-native for every empty facade). Idempotent; safe to |
| 72 | + # call once per `pod install`. |
| 73 | + # |
| 74 | + # `react_native_path` locates the real podspecs we mirror. version + subspecs + |
| 75 | + # default_subspecs + resources are all DERIVED from the real spec so the facade |
| 76 | + # stays graph- and resource-equivalent to the source pod. A facaded pod whose |
| 77 | + # real podspec can't be read is a hard error (see load_real_spec) — silently |
| 78 | + # shipping an empty facade would hide exactly the drift this guards against. |
| 79 | + def self.generate(react_native_path, install_root, version, ios_version) |
| 80 | + @@install_root = install_root.to_s |
| 81 | + abs_base = File.join(@@install_root, FACADE_RELDIR) |
| 82 | + FileUtils.mkdir_p(abs_base) |
| 83 | + FACADE_PODS.each do |name, podspec_rel_path| |
| 84 | + podspec_path = File.join(react_native_path.to_s, podspec_rel_path) |
| 85 | + real = load_real_spec(podspec_path, name) |
| 86 | + podspec_dir = File.dirname(podspec_path) |
| 87 | + dir = File.join(abs_base, name) |
| 88 | + FileUtils.mkdir_p(dir) |
| 89 | + |
| 90 | + spec = { |
| 91 | + "name" => name, |
| 92 | + "version" => real.version.to_s, |
| 93 | + "summary" => "Prebuilt facade for #{name} (code + headers live in React-Core-prebuilt).", |
| 94 | + "homepage" => "https://reactnative.dev/", |
| 95 | + "license" => "MIT", |
| 96 | + "authors" => "Meta Platforms, Inc. and its affiliates", |
| 97 | + "platforms" => { "ios" => ios_version }, |
| 98 | + # Required podspec attribute, but never fetched: the pod is installed |
| 99 | + # as a LOCAL pod (`:path => <dir>`), which uses this spec in place and |
| 100 | + # ships no source_files. Placeholder only. |
| 101 | + "source" => { "git" => "https://github.com/facebook/react-native.git" }, |
| 102 | + "dependencies" => { "React-Core-prebuilt" => [] }, |
| 103 | + } |
| 104 | + |
| 105 | + # Preserve non-code RESOURCES (privacy manifest, i18n bundles, ...). They |
| 106 | + # don't shadow headers, and React-Core-prebuilt doesn't vend them, so the |
| 107 | + # facade must carry them or prebuilt installs lose them. Globs are made |
| 108 | + # relative to the facade dir so they resolve back to the real source tree. |
| 109 | + resource_bundles = derive_resource_bundles(real, podspec_dir, dir) |
| 110 | + spec["resource_bundles"] = resource_bundles unless resource_bundles.empty? |
| 111 | + resources = derive_resources(real, podspec_dir, dir) |
| 112 | + spec["resources"] = resources unless resources.empty? |
| 113 | + |
| 114 | + # Preserve default_subspec so a bare `pod '<Name>'` resolves to the SAME |
| 115 | + # subspec graph as the source pod (without it CocoaPods pulls every |
| 116 | + # subspec, which is not graph-equivalent). |
| 117 | + defaults = Array(real.default_subspecs) |
| 118 | + spec["default_subspecs"] = defaults unless defaults.empty? |
| 119 | + |
| 120 | + subspecs = derive_subspecs(real) |
| 121 | + unless subspecs.empty? |
| 122 | + spec["subspecs"] = subspecs.map do |ss| |
| 123 | + { "name" => ss, "dependencies" => { "React-Core-prebuilt" => [] } } |
| 124 | + end |
| 125 | + end |
| 126 | + |
| 127 | + File.write(File.join(dir, "#{name}.podspec.json"), JSON.pretty_generate(spec)) |
| 128 | + end |
| 129 | + abs_base |
| 130 | + end |
| 131 | + |
| 132 | + # Facade dir for `<name>`, RELATIVE to the install root — pass to `pod :path =>`. |
| 133 | + # Relative (not absolute) so the path CocoaPods records in Podfile.lock is |
| 134 | + # portable rather than machine-specific. |
| 135 | + def self.facade_path(name) |
| 136 | + File.join(FACADE_RELDIR, name) |
| 137 | + end |
| 138 | + |
| 139 | + # Loads the real podspec so we can mirror its structure. A facaded pod MUST have |
| 140 | + # a readable real podspec — if it's missing or unparseable we raise rather than |
| 141 | + # ship an empty facade, since that would silently drop subspecs/resources (the |
| 142 | + # very drift this mechanism exists to prevent). |
| 143 | + def self.load_real_spec(path, name) |
| 144 | + unless File.exist?(path) |
| 145 | + raise "[RNCoreFacades] Real podspec for facaded pod '#{name}' not found at #{path}. " \ |
| 146 | + "Update FACADE_PODS in rncore_facades.rb if the podspec moved." |
| 147 | + end |
| 148 | + Pod::Specification.from_file(path) |
| 149 | + rescue => e |
| 150 | + raise "[RNCoreFacades] Failed to read real podspec for facaded pod '#{name}' at #{path}: #{e.message}" |
| 151 | + end |
| 152 | + private_class_method :load_real_spec |
| 153 | + |
| 154 | + # Library (non-test, non-app) subspec names of the real spec, so third-party |
| 155 | + # libs depending on `<pod>/<subspec>` keep resolving. Derived, never hand-listed. |
| 156 | + def self.derive_subspecs(real) |
| 157 | + real.subspecs |
| 158 | + .reject { |ss| ss.test_specification? || (ss.respond_to?(:app_specification?) && ss.app_specification?) } |
| 159 | + .map(&:base_name) |
| 160 | + end |
| 161 | + private_class_method :derive_subspecs |
| 162 | + |
| 163 | + # Effective resource_bundles of the real spec (e.g. React-Core_privacy), with |
| 164 | + # globs rewritten relative to the facade dir so they point back at the real |
| 165 | + # source files. Unions the `resource_bundle` (singular) and `resource_bundles` |
| 166 | + # (plural) DSL forms. |
| 167 | + def self.derive_resource_bundles(real, podspec_dir, facade_dir) |
| 168 | + out = {} |
| 169 | + [real.attributes_hash["resource_bundle"], real.attributes_hash["resource_bundles"]].each do |rb| |
| 170 | + next unless rb.is_a?(Hash) |
| 171 | + rb.each do |bundle, globs| |
| 172 | + out[bundle] = Array(globs).map { |g| rel_glob(g, podspec_dir, facade_dir) } |
| 173 | + end |
| 174 | + end |
| 175 | + out |
| 176 | + end |
| 177 | + private_class_method :derive_resource_bundles |
| 178 | + |
| 179 | + # Loose `resources` of the real spec, rewritten relative to the facade dir. |
| 180 | + def self.derive_resources(real, podspec_dir, facade_dir) |
| 181 | + Array(real.attributes_hash["resources"]).map { |g| rel_glob(g, podspec_dir, facade_dir) } |
| 182 | + end |
| 183 | + private_class_method :derive_resources |
| 184 | + |
| 185 | + # Rewrite a glob declared relative to `podspec_dir` into one relative to |
| 186 | + # `facade_dir`, so the generated facade (which lives under the app's build/) |
| 187 | + # still resolves the resource in the react-native source tree. |
| 188 | + def self.rel_glob(glob, podspec_dir, facade_dir) |
| 189 | + require "pathname" |
| 190 | + abs = File.expand_path(glob, podspec_dir) |
| 191 | + Pathname.new(abs).relative_path_from(Pathname.new(facade_dir)).to_s |
| 192 | + end |
| 193 | + private_class_method :rel_glob |
| 194 | +end |
0 commit comments