|
| 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 | +# Dependency-only facade podspecs for the third-party deps in prebuilt-deps |
| 10 | +# mode (deps-side analogue of RNCoreFacades). Design + rationale: |
| 11 | +# scripts/cocoapods/__docs__/prebuilt-deps.md |
| 12 | +module RNDepsFacades |
| 13 | + # The name of the umbrella prebuilt-deps pod every facade depends on. Its pod |
| 14 | + # self-serves the third-party headers + carries the binary (see |
| 15 | + # rndependencies.rb, ReactNativeDependencies.podspec). |
| 16 | + DEPS_POD = "ReactNativeDependencies" |
| 17 | + |
| 18 | + # pod name => podspec path (relative to the react-native package root), or |
| 19 | + # :synthesized for a pod with no local podspec (SocketRocket). Version + |
| 20 | + # subspecs + default_subspecs are DERIVED from the real podspec where one |
| 21 | + # exists; synthesized entries derive their version from a constant. |
| 22 | + FACADE_PODS = { |
| 23 | + "RCT-Folly" => "third-party-podspecs/RCT-Folly.podspec", |
| 24 | + "glog" => "third-party-podspecs/glog.podspec", |
| 25 | + "boost" => "third-party-podspecs/boost.podspec", |
| 26 | + "DoubleConversion" => "third-party-podspecs/DoubleConversion.podspec", |
| 27 | + "fmt" => "third-party-podspecs/fmt.podspec", |
| 28 | + "fast_float" => "third-party-podspecs/fast_float.podspec", |
| 29 | + "SocketRocket" => :synthesized, |
| 30 | + } |
| 31 | + |
| 32 | + # Sub-directory (relative to the install root) that holds the generated |
| 33 | + # deps facades. Kept separate from RNCoreFacades' `build/rncore-facades` so |
| 34 | + # the two families never collide. |
| 35 | + FACADE_RELDIR = File.join("build", "rndeps-facades") |
| 36 | + |
| 37 | + @@install_root = nil |
| 38 | + |
| 39 | + # Generates the facade podspecs and returns the base directory holding them. |
| 40 | + # Each facade gets its OWN sub-directory containing a single |
| 41 | + # `<Name>.podspec.json`, so it can be installed as a LOCAL pod via |
| 42 | + # `:path => <dir>` (PathSource uses the spec in place and never downloads |
| 43 | + # `spec.source`). Idempotent; safe to call once per `pod install`. |
| 44 | + # |
| 45 | + # `react_native_path` locates the real third-party podspecs we mirror. |
| 46 | + # version + subspecs + default_subspecs are DERIVED from the real spec (or, |
| 47 | + # for SocketRocket, synthesized from the socket_rocket_config version) so the |
| 48 | + # facade stays graph-equivalent to the source pod. NO source_files and NO |
| 49 | + # headers are emitted — the ReactNativeDependencies pod supplies both. A |
| 50 | + # facaded pod whose real podspec can't be read is a hard error (see |
| 51 | + # load_real_spec) — silently shipping an empty facade would hide drift. |
| 52 | + def self.generate(react_native_path, install_root, ios_version) |
| 53 | + @@install_root = install_root.to_s |
| 54 | + abs_base = File.join(@@install_root, FACADE_RELDIR) |
| 55 | + FileUtils.mkdir_p(abs_base) |
| 56 | + FACADE_PODS.each do |name, podspec_rel_path| |
| 57 | + dir = File.join(abs_base, name) |
| 58 | + FileUtils.mkdir_p(dir) |
| 59 | + |
| 60 | + if podspec_rel_path == :synthesized |
| 61 | + spec = synthesized_spec(name, ios_version) |
| 62 | + else |
| 63 | + podspec_path = File.join(react_native_path.to_s, podspec_rel_path) |
| 64 | + real = load_real_spec(podspec_path, name) |
| 65 | + spec = derived_spec(name, real, ios_version) |
| 66 | + end |
| 67 | + |
| 68 | + File.write(File.join(dir, "#{name}.podspec.json"), JSON.pretty_generate(spec)) |
| 69 | + end |
| 70 | + abs_base |
| 71 | + end |
| 72 | + |
| 73 | + # Facade dir for `<name>`, RELATIVE to the install root — pass to `pod :path =>`. |
| 74 | + # Relative (not absolute) so the path CocoaPods records in Podfile.lock is |
| 75 | + # portable rather than machine-specific. |
| 76 | + def self.facade_path(name) |
| 77 | + File.join(FACADE_RELDIR, name) |
| 78 | + end |
| 79 | + |
| 80 | + # Base spec skeleton shared by derived + synthesized facades: dependency-only, |
| 81 | + # no source_files, no headers. Depends solely on ReactNativeDependencies. |
| 82 | + def self.base_spec(name, version, ios_version) |
| 83 | + { |
| 84 | + "name" => name, |
| 85 | + "version" => version, |
| 86 | + "summary" => "Prebuilt facade for #{name} (code + headers live in #{DEPS_POD}).", |
| 87 | + "homepage" => "https://reactnative.dev/", |
| 88 | + "license" => "MIT", |
| 89 | + "authors" => "Meta Platforms, Inc. and its affiliates", |
| 90 | + "platforms" => { "ios" => ios_version }, |
| 91 | + # Required podspec attribute, but never fetched: installed as a LOCAL |
| 92 | + # pod (`:path => <dir>`), which uses this spec in place and ships no |
| 93 | + # source_files. Placeholder only. |
| 94 | + "source" => { "git" => "https://github.com/facebook/react-native.git" }, |
| 95 | + "dependencies" => { DEPS_POD => [] }, |
| 96 | + } |
| 97 | + end |
| 98 | + private_class_method :base_spec |
| 99 | + |
| 100 | + # Facade derived from a real third-party podspec: version + subspecs + |
| 101 | + # default_subspecs mirror the real spec so a bare `pod '<Name>'` and any |
| 102 | + # `pod '<Name>/<Subspec>'` resolve to the SAME graph (e.g. RCT-Folly's |
| 103 | + # bare + /Default + /Fabric). Each subspec is also dependency-only and |
| 104 | + # depends on ReactNativeDependencies. |
| 105 | + # |
| 106 | + # NOTE: resources (e.g. RCT-Folly's PrivacyInfo.xcprivacy) are intentionally |
| 107 | + # NOT carried. In prebuilt-deps mode the third-party code — and its privacy |
| 108 | + # manifest — is embedded in the ReactNativeDependencies artifact; the facade |
| 109 | + # only needs to declare the dependency (see the design note in the PR). |
| 110 | + def self.derived_spec(name, real, ios_version) |
| 111 | + spec = base_spec(name, real.version.to_s, ios_version) |
| 112 | + |
| 113 | + defaults = Array(real.default_subspecs) |
| 114 | + spec["default_subspecs"] = defaults unless defaults.empty? |
| 115 | + |
| 116 | + subspecs = derive_subspecs(real) |
| 117 | + unless subspecs.empty? |
| 118 | + spec["subspecs"] = subspecs.map do |ss| |
| 119 | + { "name" => ss, "dependencies" => { DEPS_POD => [] } } |
| 120 | + end |
| 121 | + end |
| 122 | + |
| 123 | + spec |
| 124 | + end |
| 125 | + private_class_method :derived_spec |
| 126 | + |
| 127 | + # Facade synthesized for a pod with NO local podspec (SocketRocket, a trunk |
| 128 | + # pod). Version comes from Helpers::Constants::socket_rocket_config; no |
| 129 | + # subspecs. A missing/blank constant is a hard error rather than a silent |
| 130 | + # versionless facade — a bare `pod 'SocketRocket'` in the source path is |
| 131 | + # `"~> #{socket_rocket_config[:version]}"`, so the facade MUST carry a version |
| 132 | + # that satisfies that constraint. |
| 133 | + def self.synthesized_spec(name, ios_version) |
| 134 | + version = synthesized_version(name) |
| 135 | + base_spec(name, version, ios_version) |
| 136 | + end |
| 137 | + private_class_method :synthesized_spec |
| 138 | + |
| 139 | + # Resolves the synthesized version for a no-podspec facade. Fail-closed on a |
| 140 | + # missing constant/version. Only SocketRocket is synthesized today. |
| 141 | + def self.synthesized_version(name) |
| 142 | + case name |
| 143 | + when "SocketRocket" |
| 144 | + unless defined?(Helpers::Constants) && Helpers::Constants.respond_to?(:socket_rocket_config) |
| 145 | + raise "[RNDepsFacades] Cannot synthesize facade for '#{name}': " \ |
| 146 | + "Helpers::Constants.socket_rocket_config is unavailable." |
| 147 | + end |
| 148 | + version = Helpers::Constants.socket_rocket_config[:version] |
| 149 | + if version.nil? || version.to_s.strip.empty? |
| 150 | + raise "[RNDepsFacades] Cannot synthesize facade for '#{name}': " \ |
| 151 | + "socket_rocket_config[:version] is missing or empty." |
| 152 | + end |
| 153 | + version.to_s |
| 154 | + else |
| 155 | + raise "[RNDepsFacades] No synthesized version rule for facaded pod '#{name}'. " \ |
| 156 | + "Add one to synthesized_version or give it a real podspec in FACADE_PODS." |
| 157 | + end |
| 158 | + end |
| 159 | + private_class_method :synthesized_version |
| 160 | + |
| 161 | + # Loads the real podspec so we can mirror its structure. A facaded pod with a |
| 162 | + # declared podspec path MUST have a readable real podspec — if it's missing or |
| 163 | + # unparseable we raise rather than ship an empty facade (which would silently |
| 164 | + # drop subspecs / the version, the very drift this mechanism prevents). |
| 165 | + def self.load_real_spec(path, name) |
| 166 | + unless File.exist?(path) |
| 167 | + raise "[RNDepsFacades] Real podspec for facaded pod '#{name}' not found at #{path}. " \ |
| 168 | + "Update FACADE_PODS in rndeps_facades.rb if the podspec moved." |
| 169 | + end |
| 170 | + Pod::Specification.from_file(path) |
| 171 | + rescue => e |
| 172 | + raise "[RNDepsFacades] Failed to read real podspec for facaded pod '#{name}' at #{path}: #{e.message}" |
| 173 | + end |
| 174 | + private_class_method :load_real_spec |
| 175 | + |
| 176 | + # Library (non-test, non-app) subspec names of the real spec, so third-party |
| 177 | + # libs depending on `<pod>/<subspec>` (e.g. `RCT-Folly/Fabric`) keep |
| 178 | + # resolving. Derived, never hand-listed. |
| 179 | + def self.derive_subspecs(real) |
| 180 | + real.subspecs |
| 181 | + .reject { |ss| ss.test_specification? || (ss.respond_to?(:app_specification?) && ss.app_specification?) } |
| 182 | + .map(&:base_name) |
| 183 | + end |
| 184 | + private_class_method :derive_subspecs |
| 185 | +end |
0 commit comments