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 6require 'json' 7require_relative './helpers.rb' 8require_relative './codegen_script_phase_extractor.rb' 9 10class CodegenUtils 11 12 def initialize() 13 end 14 15 @@REACT_CODEGEN_PODSPEC_GENERATED = false 16 17 def self.set_react_codegen_podspec_generated(value) 18 @@REACT_CODEGEN_PODSPEC_GENERATED = value 19 end 20 21 def self.react_codegen_podspec_generated 22 @@REACT_CODEGEN_PODSPEC_GENERATED 23 end 24 25 @@REACT_CODEGEN_DISCOVERY_DONE = false 26 27 def self.set_react_codegen_discovery_done(value) 28 @@REACT_CODEGEN_DISCOVERY_DONE = value 29 end 30 31 def self.react_codegen_discovery_done 32 @@REACT_CODEGEN_DISCOVERY_DONE 33 end 34 35 # It takes some cocoapods specs and writes them into a file 36 # 37 # Parameters 38 # - spec: the cocoapod specs 39 # - codegen_output_dir: the output directory for the codegen 40 # - file_manager: a class that implements the `File` interface. Defaults to `File`, the Dependency can be injected for testing purposes. 41 def generate_react_codegen_podspec!(spec, codegen_output_dir, file_manager: File) 42 # This podspec file should only be create once in the session/pod install. 43 # This happens when multiple targets are calling use_react_native!. 44 if @@REACT_CODEGEN_PODSPEC_GENERATED 45 Pod::UI.puts "[Codegen] Skipping ABI49_0_0React-Codegen podspec generation." 46 return 47 end 48 49 relative_installation_root = Pod::Config.instance.installation_root.relative_path_from(Pathname.pwd) 50 output_dir = "#{relative_installation_root}/#{codegen_output_dir}" 51 Pod::Executable.execute_command("mkdir", ["-p", output_dir]); 52 53 podspec_path = file_manager.join(output_dir, 'ABI49_0_0React-Codegen.podspec.json') 54 Pod::UI.puts "[Codegen] Generating #{podspec_path}" 55 56 file_manager.open(podspec_path, 'w') do |f| 57 f.write(spec.to_json) 58 f.fsync 59 end 60 61 @@REACT_CODEGEN_PODSPEC_GENERATED = true 62 end 63 64 # It generates the podspec object that represents the `ABI49_0_0React-Codegen.podspec` file 65 # 66 # Parameters 67 # - package_json_file: the path to the `package.json`, required to extract the proper React Native version 68 # - fabric_enabled: whether fabric is enabled or not. 69 # - hermes_enabled: whether hermes is enabled or not. 70 # - script_phases: whether we want to add some build script phases or not. 71 # - file_manager: a class that implements the `File` interface. Defaults to `File`, the Dependency can be injected for testing purposes. 72 def get_react_codegen_spec(package_json_file, folly_version: '2021.07.22.00', fabric_enabled: false, hermes_enabled: true, script_phases: nil, file_manager: File) 73 package = JSON.parse(file_manager.read(package_json_file)) 74 version = package['version'] 75 new_arch_disabled = ENV['RCT_NEW_ARCH_ENABLED'] != "1" 76 use_frameworks = ENV['USE_FRAMEWORKS'] != nil 77 folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' 78 boost_compiler_flags = '-Wno-documentation' 79 80 header_search_paths = [ 81 "\"$(PODS_ROOT)/boost\"", 82 "\"$(PODS_ROOT)/RCT-Folly\"", 83 "\"${PODS_ROOT}/Headers/Public/ABI49_0_0React-Codegen/react/renderer/components\"", 84 "\"$(PODS_ROOT)/Headers/Private/ABI49_0_0React-Fabric\"", 85 "\"$(PODS_ROOT)/Headers/Private/ABI49_0_0React-RCTFabric\"", 86 ] 87 framework_search_paths = [] 88 89 if use_frameworks 90 header_search_paths.concat([ 91 "\"$(PODS_ROOT)/DoubleConversion\"", 92 "\"$(PODS_TARGET_SRCROOT)\"", 93 "\"$(PODS_CONFIGURATION_BUILD_DIR)/React-Fabric/React_Fabric.framework/Headers\"", 94 "\"$(PODS_CONFIGURATION_BUILD_DIR)/React-graphics/React_graphics.framework/Headers\"", 95 "\"$(PODS_CONFIGURATION_BUILD_DIR)/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios\"", 96 "\"$(PODS_CONFIGURATION_BUILD_DIR)/ReactCommon/ReactCommon.framework/Headers\"", 97 "\"$(PODS_CONFIGURATION_BUILD_DIR)/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core\"", 98 "\"$(PODS_CONFIGURATION_BUILD_DIR)/React-NativeModulesApple/React_NativeModulesApple.framework/Headers\"", 99 "\"$(PODS_CONFIGURATION_BUILD_DIR)/React-RCTFabric/RCTFabric.framework/Headers\"", 100 "\"$(PODS_CONFIGURATION_BUILD_DIR)/React-debug/React_debug.framework/Headers\"", 101 "\"$(PODS_CONFIGURATION_BUILD_DIR)/React-utils/React_utils.framework/Headers\"", 102 ]) 103 end 104 105 spec = { 106 'name' => "ABI49_0_0React-Codegen", 107 'version' => version, 108 'summary' => 'Temp pod for generated files for React Native', 109 'homepage' => 'https://facebook.com/', 110 'license' => 'Unlicense', 111 'authors' => 'Facebook', 112 'compiler_flags' => "#{folly_compiler_flags} #{boost_compiler_flags} -Wno-nullability-completeness -std=c++17", 113 'source' => { :git => '' }, 114 'header_mappings_dir' => './', 115 'platforms' => { 116 'ios' => min_ios_version_supported, 117 }, 118 'source_files' => "**/*.{h,mm,cpp}", 119 'pod_target_xcconfig' => { 120 "HEADER_SEARCH_PATHS" => header_search_paths.join(' '), 121 "FRAMEWORK_SEARCH_PATHS" => framework_search_paths 122 }, 123 'dependencies': { 124 "ABI49_0_0React-jsiexecutor": [], 125 "RCT-Folly": [], 126 "ABI49_0_0RCTRequired": [], 127 "ABI49_0_0RCTTypeSafety": [], 128 "ABI49_0_0React-Core": [], 129 "ABI49_0_0React-jsi": [], 130 "ABI49_0_0ReactCommon/turbomodule/bridging": [], 131 "ABI49_0_0ReactCommon/turbomodule/core": [], 132 "ABI49_0_0React-NativeModulesApple": [], 133 "glog": [], 134 "DoubleConversion": [], 135 } 136 } 137 138 if fabric_enabled 139 spec[:'dependencies'].merge!({ 140 "ABI49_0_0React-graphics": [], 141 'React-Fabric': [], 142 'React-debug': [], 143 'React-utils': [], 144 }); 145 end 146 147 if hermes_enabled 148 spec[:'dependencies'].merge!({ 149 "ABI49_0_0hermes-engine": [], 150 }); 151 else 152 spec[:'dependencies'].merge!({ 153 "ABI49_0_0React-jsc": [], 154 }); 155 end 156 157 if new_arch_disabled 158 spec[:dependencies].merge!({ 159 "ABI49_0_0React-rncore": [], 160 "ABI49_0_0FBReactNativeSpec": [], 161 }) 162 end 163 164 if script_phases 165 Pod::UI.puts "[Codegen] Adding script_phases to ABI49_0_0React-Codegen." 166 spec[:'script_phases'] = script_phases 167 end 168 169 return spec 170 end 171 172 # It extracts the codegen config from the configuration file 173 # 174 # Parameters 175 # - config_path: a path to the configuration file 176 # - config_key: the codegen configuration key 177 # - file_manager: a class that implements the `File` interface. Defaults to `File`, the Dependency can be injected for testing purposes. 178 # 179 # Returns: the list of dependencies as extracted from the package.json 180 def get_codegen_config_from_file(config_path, config_key, file_manager: File) 181 empty = {} 182 if !file_manager.exist?(config_path) 183 return empty 184 end 185 186 config = JSON.parse(file_manager.read(config_path)) 187 return config[config_key] ? config[config_key] : empty 188 end 189 190 # It creates a list of JS files that contains the JS specifications that Codegen needs to use to generate the code 191 # 192 # Parameters 193 # - app_codegen_config: an object that contains the configurations 194 # - app_path: path to the app 195 # - file_manager: a class that implements the `File` interface. Defaults to `File`, the Dependency can be injected for testing purposes. 196 # 197 # Returns: the list of files that needs to be used by Codegen 198 def get_list_of_js_specs(app_codegen_config, app_path, file_manager: File) 199 file_list = [] 200 201 if app_codegen_config['libraries'] then 202 Pod::UI.warn '[Deprecated] You are using the old `libraries` array to list all your codegen.\nThis method will be removed in the future.\nUpdate your `package.json` with a single object.' 203 app_codegen_config['libraries'].each do |library| 204 library_dir = file_manager.join(app_path, library['jsSrcsDir']) 205 file_list.concat(Finder.find_codegen_file(library_dir)) 206 end 207 elsif app_codegen_config['jsSrcsDir'] then 208 codegen_dir = file_manager.join(app_path, app_codegen_config['jsSrcsDir']) 209 file_list.concat (Finder.find_codegen_file(codegen_dir)) 210 end 211 212 input_files = file_list.map { |filename| "${PODS_ROOT}/../#{Pathname.new(filename).realpath().relative_path_from(Pod::Config.instance.installation_root)}" } 213 214 return input_files 215 end 216 217 # It generates the build script phase for the codegen 218 # 219 # Parameters 220 # - app_path: the path to the app 221 # - fabric_enabled: whether fabric is enabled or not 222 # - config_file_dir: the directory of the config file 223 # - react_native_path: the path to React Native 224 # - config_key: the configuration key to use in the package.json for the Codegen 225 # - codegen_utils: an object which exposes utilities functions for the codegen 226 # - script_phase_extractor: an object that is able to extract the Xcode Script Phases for React Native 227 # - file_manager: a class that implements the `File` interface. Defaults to `File`, the Dependency can be injected for testing purposes. 228 # 229 # Return: an object containing the script phase 230 def get_react_codegen_script_phases( 231 app_path, 232 fabric_enabled: false, 233 hermes_enabled: false, 234 config_file_dir: '', 235 react_native_path: "../node_modules/react-native", 236 config_key: 'codegenConfig', 237 codegen_utils: CodegenUtils.new(), 238 script_phase_extractor: CodegenScriptPhaseExtractor.new(), 239 file_manager: File 240 ) 241 if !app_path 242 Pod::UI.warn '[Codegen] error: app_path is required to use codegen discovery.' 243 abort 244 end 245 246 # We need to convert paths to relative path from installation_root for the script phase for CI. 247 relative_app_root = Pathname.new(app_path).realpath().relative_path_from(Pod::Config.instance.installation_root) 248 249 relative_config_file_dir = '' 250 if config_file_dir != '' 251 relative_config_file_dir = Pathname.new(config_file_dir).relative_path_from(Pod::Config.instance.installation_root) 252 end 253 254 # Generate input files for in-app libaraies which will be used to check if the script needs to be run. 255 # TODO: Ideally, we generate the input_files list from generate-codegen-artifacts.js and read the result here. 256 # Or, generate this podspec in generate-codegen-artifacts.js as well. 257 app_package_path = file_manager.join(app_path, 'package.json') 258 app_codegen_config = codegen_utils.get_codegen_config_from_file(app_package_path, config_key) 259 input_files = codegen_utils.get_list_of_js_specs(app_codegen_config, app_path) 260 261 # Add a script phase to trigger generate artifact. 262 # Some code is duplicated so that it's easier to delete the old way and switch over to this once it's stabilized. 263 return { 264 'name': 'Generate Specs', 265 'execution_position': :before_compile, 266 'input_files' => input_files, 267 'show_env_vars_in_log': true, 268 'output_files': ["${DERIVED_FILE_DIR}/react-codegen.log"], 269 'script': script_phase_extractor.extract_script_phase( 270 react_native_path: react_native_path, 271 relative_app_root: relative_app_root, 272 relative_config_file_dir: relative_config_file_dir, 273 fabric_enabled: fabric_enabled 274 ), 275 } 276 end 277 278 def use_react_native_codegen_discovery!( 279 codegen_disabled, 280 app_path, 281 react_native_path: "../node_modules/react-native", 282 fabric_enabled: false, 283 hermes_enabled: true, 284 config_file_dir: '', 285 codegen_output_dir: 'build/generated/ios', 286 config_key: 'codegenConfig', 287 folly_version: '2021.07.22.00', 288 codegen_utils: CodegenUtils.new(), 289 file_manager: File 290 ) 291 return if codegen_disabled 292 293 if CodegenUtils.react_codegen_discovery_done() 294 Pod::UI.puts "[Codegen] Skipping use_react_native_codegen_discovery." 295 return 296 end 297 298 if !app_path 299 Pod::UI.warn '[Codegen] Error: app_path is required for use_react_native_codegen_discovery.' 300 Pod::UI.warn '[Codegen] If you are calling use_react_native_codegen_discovery! in your Podfile, please remove the call and pass `app_path` and/or `config_file_dir` to `use_react_native!`.' 301 abort 302 end 303 304 Pod::UI.warn '[Codegen] warn: using experimental new codegen integration' 305 relative_installation_root = Pod::Config.instance.installation_root.relative_path_from(Pathname.pwd) 306 307 # Generate ABI49_0_0React-Codegen podspec here to add the script phases. 308 script_phases = codegen_utils.get_react_codegen_script_phases( 309 app_path, 310 :fabric_enabled => fabric_enabled, 311 :config_file_dir => config_file_dir, 312 :react_native_path => react_native_path, 313 :config_key => config_key 314 ) 315 react_codegen_spec = codegen_utils.get_react_codegen_spec( 316 file_manager.join(react_native_path, "package.json"), 317 :folly_version => folly_version, 318 :fabric_enabled => fabric_enabled, 319 :hermes_enabled => hermes_enabled, 320 :script_phases => script_phases 321 ) 322 codegen_utils.generate_react_codegen_podspec!(react_codegen_spec, codegen_output_dir) 323 324 out = Pod::Executable.execute_command( 325 'node', 326 [ 327 "#{relative_installation_root}/#{react_native_path}/scripts/generate-codegen-artifacts.js", 328 "-p", "#{app_path}", 329 "-o", Pod::Config.instance.installation_root, 330 "-e", "#{fabric_enabled}", 331 "-c", "#{config_file_dir}", 332 ]) 333 Pod::UI.puts out; 334 335 CodegenUtils.set_react_codegen_discovery_done(true) 336 end 337 338 @@CLEANUP_DONE = false 339 340 def self.set_cleanup_done(newValue) 341 @@CLEANUP_DONE = newValue 342 end 343 344 def self.cleanup_done 345 return @@CLEANUP_DONE 346 end 347 348 def self.clean_up_build_folder(rn_path, app_path, ios_folder, codegen_dir, dir_manager: Dir, file_manager: File) 349 return if CodegenUtils.cleanup_done() 350 CodegenUtils.set_cleanup_done(true) 351 352 codegen_path = file_manager.join(app_path, ios_folder, codegen_dir) 353 return if !dir_manager.exist?(codegen_path) 354 355 FileUtils.rm_rf(dir_manager.glob("#{codegen_path}/*")) 356 base_provider_path = file_manager.join(rn_path, 'React', 'Fabric', 'RCTThirdPartyFabricComponentsProvider') 357 FileUtils.rm_rf("#{base_provider_path}.h") 358 FileUtils.rm_rf("#{base_provider_path}.mm") 359 CodegenUtils.assert_codegen_folder_is_empty(app_path, ios_folder, codegen_dir, dir_manager: dir_manager, file_manager: file_manager) 360 end 361 362 # Need to split this function from the previous one to be able to test it properly. 363 def self.assert_codegen_folder_is_empty(app_path, ios_folder, codegen_dir, dir_manager: Dir, file_manager: File) 364 # double check that the files have actually been deleted. 365 # Emit an error message if not. 366 codegen_path = file_manager.join(app_path, ios_folder, codegen_dir) 367 if dir_manager.exist?(codegen_path) && dir_manager.glob("#{codegen_path}/*").length() != 0 368 Pod::UI.warn "Unable to remove the content of #{codegen_path} folder. Please run rm -rf #{codegen_path} and try again." 369 abort 370 end 371 end 372end 373