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 def generate_react_codegen_podspec!(spec, codegen_output_dir) 41 # This podspec file should only be create once in the session/pod install. 42 # This happens when multiple targets are calling use_react_native!. 43 if @@REACT_CODEGEN_PODSPEC_GENERATED 44 Pod::UI.puts "[Codegen] Skipping ABI48_0_0React-Codegen podspec generation." 45 return 46 end 47 48 relative_installation_root = Pod::Config.instance.installation_root.relative_path_from(Pathname.pwd) 49 output_dir = "#{relative_installation_root}/#{codegen_output_dir}" 50 Pod::Executable.execute_command("mkdir", ["-p", output_dir]); 51 52 podspec_path = File.join(output_dir, 'ABI48_0_0React-Codegen.podspec.json') 53 Pod::UI.puts "[Codegen] Generating #{podspec_path}" 54 55 File.open(podspec_path, 'w') do |f| 56 f.write(spec.to_json) 57 f.fsync 58 end 59 60 @@REACT_CODEGEN_PODSPEC_GENERATED = true 61 end 62 63 # It generates the podspec object that represents the `ABI48_0_0React-Codegen.podspec` file 64 # 65 # Parameters 66 # - package_json_file: the path to the `package.json`, required to extract the proper React Native version 67 # - fabric_enabled: whether fabric is enabled or not. 68 # - hermes_enabled: whether hermes is enabled or not. 69 # - script_phases: whether we want to add some build script phases or not. 70 def get_react_codegen_spec(package_json_file, folly_version: '2021.07.22.00', fabric_enabled: false, hermes_enabled: true, script_phases: nil) 71 package = JSON.parse(File.read(package_json_file)) 72 version = package['version'] 73 74 folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' 75 boost_compiler_flags = '-Wno-documentation' 76 77 spec = { 78 'name' => "ABI48_0_0React-Codegen", 79 'version' => version, 80 'summary' => 'Temp pod for generated files for React Native', 81 'homepage' => 'https://facebook.com/', 82 'license' => 'Unlicense', 83 'authors' => 'Facebook', 84 'compiler_flags' => "#{folly_compiler_flags} #{boost_compiler_flags} -Wno-nullability-completeness -std=c++17", 85 'source' => { :git => '' }, 86 'header_mappings_dir' => './', 87 'platforms' => { 88 'ios' => '11.0', 89 }, 90 'source_files' => "**/*.{h,mm,cpp}", 91 'pod_target_xcconfig' => { "HEADER_SEARCH_PATHS" => 92 [ 93 "\"$(PODS_ROOT)/boost\"", 94 "\"$(PODS_ROOT)/RCT-Folly\"", 95 "\"${PODS_ROOT}/Headers/Public/ABI48_0_0React-Codegen/react/renderer/components\"", 96 "\"$(PODS_ROOT)/Headers/Private/ABI48_0_0React-Fabric\"", 97 "\"$(PODS_ROOT)/Headers/Private/ABI48_0_0React-RCTFabric\"", 98 ].join(' ') 99 }, 100 'dependencies': { 101 "ABI48_0_0FBReactNativeSpec": [], 102 "ABI48_0_0React-jsiexecutor": [], 103 "RCT-Folly": [], 104 "ABI48_0_0RCTRequired": [], 105 "ABI48_0_0RCTTypeSafety": [], 106 "ABI48_0_0React-Core": [], 107 "ABI48_0_0React-jsi": [], 108 "ABI48_0_0ReactCommon/turbomodule/bridging": [], 109 "ABI48_0_0ReactCommon/turbomodule/core": [] 110 } 111 } 112 113 if fabric_enabled 114 spec[:'dependencies'].merge!({ 115 "ABI48_0_0React-graphics": [], 116 "ABI48_0_0React-rncore": [], 117 }); 118 end 119 120 if hermes_enabled 121 spec[:'dependencies'].merge!({ 122 "ABI48_0_0hermes-engine": [], # let React Native decide which version of Hermes Engine to be used. Otherwise, this can create conflicts. 123 }); 124 else 125 spec[:'dependencies'].merge!({ 126 "ABI48_0_0React-jsc": [], 127 }); 128 end 129 130 if script_phases 131 Pod::UI.puts "[Codegen] Adding script_phases to ABI48_0_0React-Codegen." 132 spec[:'script_phases'] = script_phases 133 end 134 135 return spec 136 end 137 138 # It extracts the codegen config from the configuration file 139 # 140 # Parameters 141 # - config_path: a path to the configuration file 142 # - config_key: the codegen configuration key 143 # 144 # Returns: the list of dependencies as extracted from the package.json 145 def get_codegen_config_from_file(config_path, config_key) 146 empty = {} 147 if !File.exist?(config_path) 148 return empty 149 end 150 151 config = JSON.parse(File.read(config_path)) 152 return config[config_key] ? config[config_key] : empty 153 end 154 155 # It creates a list of JS files that contains the JS specifications that Codegen needs to use to generate the code 156 # 157 # Parameters 158 # - app_codegen_config: an object that contains the configurations 159 # - app_path: path to the app 160 # 161 # Returns: the list of files that needs to be used by Codegen 162 def get_list_of_js_specs(app_codegen_config, app_path) 163 file_list = [] 164 165 if app_codegen_config['libraries'] then 166 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.' 167 app_codegen_config['libraries'].each do |library| 168 library_dir = File.join(app_path, library['jsSrcsDir']) 169 file_list.concat(Finder.find_codegen_file(library_dir)) 170 end 171 elsif app_codegen_config['jsSrcsDir'] then 172 codegen_dir = File.join(app_path, app_codegen_config['jsSrcsDir']) 173 file_list.concat (Finder.find_codegen_file(codegen_dir)) 174 end 175 176 input_files = file_list.map { |filename| "${PODS_ROOT}/../#{Pathname.new(filename).realpath().relative_path_from(Pod::Config.instance.installation_root)}" } 177 178 return input_files 179 end 180 181 # It generates the build script phase for the codegen 182 # 183 # Parameters 184 # - app_path: the path to the app 185 # - fabric_enabled: whether fabric is enabled or not 186 # - config_file_dir: the directory of the config file 187 # - react_native_path: the path to React Native 188 # - config_key: the configuration key to use in the package.json for the Codegen 189 # - codegen_utils: an object which exposes utilities functions for the codegen 190 # - script_phase_extractor: an object that is able to extract the Xcode Script Phases for React Native 191 # 192 # Return: an object containing the script phase 193 def get_react_codegen_script_phases( 194 app_path, 195 fabric_enabled: false, 196 hermes_enabled: false, 197 config_file_dir: '', 198 react_native_path: "../node_modules/react-native", 199 config_key: 'codegenConfig', 200 codegen_utils: CodegenUtils.new(), 201 script_phase_extractor: CodegenScriptPhaseExtractor.new() 202 ) 203 if !app_path 204 Pod::UI.warn '[Codegen] error: app_path is requried to use codegen discovery.' 205 abort 206 end 207 208 # We need to convert paths to relative path from installation_root for the script phase for CI. 209 relative_app_root = Pathname.new(app_path).realpath().relative_path_from(Pod::Config.instance.installation_root) 210 211 relative_config_file_dir = '' 212 if config_file_dir != '' 213 relative_config_file_dir = Pathname.new(config_file_dir).relative_path_from(Pod::Config.instance.installation_root) 214 end 215 216 # Generate input files for in-app libaraies which will be used to check if the script needs to be run. 217 # TODO: Ideally, we generate the input_files list from generate-codegen-artifacts.js and read the result here. 218 # Or, generate this podspec in generate-codegen-artifacts.js as well. 219 app_package_path = File.join(app_path, 'package.json') 220 app_codegen_config = codegen_utils.get_codegen_config_from_file(app_package_path, config_key) 221 input_files = codegen_utils.get_list_of_js_specs(app_codegen_config, app_path) 222 223 # Add a script phase to trigger generate artifact. 224 # Some code is duplicated so that it's easier to delete the old way and switch over to this once it's stabilized. 225 return { 226 'name': 'Generate Specs', 227 'execution_position': :before_compile, 228 'input_files' => input_files, 229 'show_env_vars_in_log': true, 230 'output_files': ["${DERIVED_FILE_DIR}/react-codegen.log"], 231 'script': script_phase_extractor.extract_script_phase( 232 react_native_path: react_native_path, 233 relative_app_root: relative_app_root, 234 relative_config_file_dir: relative_config_file_dir, 235 fabric_enabled: fabric_enabled 236 ), 237 } 238 end 239 240 def use_react_native_codegen_discovery!( 241 codegen_disabled, 242 app_path, 243 react_native_path: "../node_modules/react-native", 244 fabric_enabled: false, 245 hermes_enabled: true, 246 config_file_dir: '', 247 codegen_output_dir: 'build/generated/ios', 248 config_key: 'codegenConfig', 249 folly_version: '2021.07.22.00', 250 codegen_utils: CodegenUtils.new() 251 ) 252 return if codegen_disabled 253 254 if CodegenUtils.react_codegen_discovery_done() 255 Pod::UI.puts "[Codegen] Skipping use_react_native_codegen_discovery." 256 return 257 end 258 259 if !app_path 260 Pod::UI.warn '[Codegen] Error: app_path is required for use_react_native_codegen_discovery.' 261 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!`.' 262 abort 263 end 264 265 Pod::UI.warn '[Codegen] warn: using experimental new codegen integration' 266 relative_installation_root = Pod::Config.instance.installation_root.relative_path_from(Pathname.pwd) 267 268 # Generate ABI48_0_0React-Codegen podspec here to add the script phases. 269 script_phases = codegen_utils.get_react_codegen_script_phases( 270 app_path, 271 :fabric_enabled => fabric_enabled, 272 :config_file_dir => config_file_dir, 273 :react_native_path => react_native_path, 274 :config_key => config_key 275 ) 276 react_codegen_spec = codegen_utils.get_react_codegen_spec( 277 File.join(react_native_path, "package.json"), 278 :folly_version => folly_version, 279 :fabric_enabled => fabric_enabled, 280 :hermes_enabled => hermes_enabled, 281 :script_phases => script_phases 282 ) 283 codegen_utils.generate_react_codegen_podspec!(react_codegen_spec, codegen_output_dir) 284 285 out = Pod::Executable.execute_command( 286 'node', 287 [ 288 "#{relative_installation_root}/#{react_native_path}/scripts/generate-codegen-artifacts.js", 289 "-p", "#{app_path}", 290 "-o", Pod::Config.instance.installation_root, 291 "-e", "#{fabric_enabled}", 292 "-c", "#{config_file_dir}", 293 ]) 294 Pod::UI.puts out; 295 296 CodegenUtils.set_react_codegen_discovery_done(true) 297 end 298 299 @@CLEANUP_DONE = false 300 301 def self.set_cleanup_done(newValue) 302 @@CLEANUP_DONE = newValue 303 end 304 305 def self.cleanup_done 306 return @@CLEANUP_DONE 307 end 308 309 def self.clean_up_build_folder(app_path, ios_folder, codegen_dir) 310 return if CodegenUtils.cleanup_done() 311 CodegenUtils.set_cleanup_done(true) 312 313 codegen_path = File.join(app_path, ios_folder, codegen_dir) 314 return if !Dir.exist?(codegen_path) 315 316 FileUtils.rm_rf(Dir.glob("#{codegen_path}/*")) 317 CodegenUtils.assert_codegen_folder_is_empty(app_path, ios_folder, codegen_dir) 318 end 319 320 # Need to split this function from the previous one to be able to test it properly. 321 def self.assert_codegen_folder_is_empty(app_path, ios_folder, codegen_dir) 322 # double check that the files have actually been deleted. 323 # Emit an error message if not. 324 codegen_path = File.join(app_path, ios_folder, codegen_dir) 325 if Dir.exist?(codegen_path) && Dir.glob("#{codegen_path}/*").length() != 0 326 Pod::UI.warn "Unable to remove the content of #{codegen_path} folder. Please run rm -rf #{codegen_path} and try again." 327 abort 328 end 329 end 330end 331