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