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