1require_relative 'constants'
2require_relative 'package'
3
4# Require extensions to CocoaPods' classes
5require_relative 'cocoapods/pod_target'
6require_relative 'cocoapods/sandbox'
7require_relative 'cocoapods/target_definition'
8require_relative 'cocoapods/umbrella_header_generator'
9require_relative 'cocoapods/user_project_integrator'
10
11module Expo
12  class AutolinkingManager
13    require 'colored2'
14    include Pod
15
16    public def initialize(podfile, target_definition, options)
17      @podfile = podfile
18      @target_definition = target_definition
19      @options = options
20      resolve_result = resolve()
21      @packages = resolve_result['modules'].map { |json_package| Package.new(json_package) }
22      @extraPods = resolve_result['extraDependencies']['iosPods']
23    end
24
25    public def use_expo_modules!
26      if has_packages?
27        return
28      end
29
30      global_flags = @options.fetch(:flags, {})
31      tests_only = @options.fetch(:testsOnly, false)
32      include_tests = @options.fetch(:includeTests, false)
33
34      project_directory = Pod::Config.instance.project_root
35
36      UI.section 'Using Expo modules' do
37        @packages.each { |package|
38          package.pods.each { |pod|
39            # The module can already be added to the target, in which case we can just skip it.
40            # This allows us to add a pod before `use_expo_modules` to provide custom flags.
41            if @target_definition.dependencies.any? { |dependency| dependency.name == pod.pod_name }
42              UI.message '— ' << package.name.green << ' is already added to the target'.yellow
43              next
44            end
45
46            podspec_dir_path = Pathname.new(pod.podspec_dir).relative_path_from(project_directory).to_path
47
48            # Ensure that the dependencies of packages with Swift code use modular headers, otherwise
49            # `pod install` may fail if there is no `use_modular_headers!` declaration or
50            # `:modular_headers => true` is not used for this particular dependency.
51            # The latter require adding transitive dependencies to user's Podfile that we'd rather like to avoid.
52            if package.has_swift_modules_to_link?
53              podspec = get_podspec_for_pod(pod)
54              use_modular_headers_for_dependencies(podspec.all_dependencies)
55            end
56
57            pod_options = {
58              :path => podspec_dir_path,
59              :configuration => package.debugOnly ? ['Debug'] : [] # An empty array means all configurations
60            }.merge(global_flags, package.flags)
61
62            if tests_only || include_tests
63              podspec = podspec || get_podspec_for_pod(pod)
64              test_specs_names = podspec.test_specs.map { |test_spec|
65                test_spec.name.delete_prefix(podspec.name + "/")
66              }
67
68              # Jump to the next package when it doesn't have any test specs (except interfaces, they're required)
69              # TODO: Can remove interface check once we move all the interfaces into the core.
70              next if tests_only && test_specs_names.empty? && !pod.pod_name.end_with?('Interface')
71
72              pod_options[:testspecs] = test_specs_names
73            end
74
75            # Install the pod.
76            @podfile.pod(pod.pod_name, pod_options)
77
78            # TODO: Can remove this once we move all the interfaces into the core.
79            next if pod.pod_name.end_with?('Interface')
80
81            UI.message "— #{package.name.green} (#{package.version})"
82          }
83        }
84      end
85
86      @extraPods.each { |pod|
87        UI.info "Adding extra pod - #{pod['name']} (#{pod['version'] || '*'})"
88        requirements = Array.new
89        requirements << pod['version'] if pod['version']
90        options = Hash.new
91        options[:configurations] = pod['configurations'] if pod['configurations']
92        options[:modular_headers] = pod['modular_headers'] if pod['modular_headers']
93        options[:source] = pod['source'] if pod['source']
94        options[:path] = pod['path'] if pod['path']
95        options[:podspec] = pod['podspec'] if pod['podspec']
96        options[:testspecs] = pod['testspecs'] if pod['testspecs']
97        options[:git] = pod['git'] if pod['git']
98        options[:branch] = pod['branch'] if pod['branch']
99        options[:tag] = pod['tag'] if pod['tag']
100        options[:commit] = pod['commit'] if pod['commit']
101        requirements << options
102        @podfile.pod(pod['name'], *requirements)
103      }
104      self
105    end
106
107    # Spawns `expo-module-autolinking generate-package-list` command.
108    public def generate_package_list(target_name, target_path)
109      Process.wait IO.popen(generate_package_list_command_args(target_path)).pid
110    end
111
112    # If there is any package to autolink.
113    public def has_packages?
114      @packages.empty?
115    end
116
117    # Filters only these packages that needs to be included in the generated modules provider.
118    public def packages_to_generate
119      @packages.select { |package| package.modules.any? }
120    end
121
122    # Returns the provider name which is also a name of the generated file
123    public def modules_provider_name
124      @options.fetch(:providerName, Constants::MODULES_PROVIDER_FILE_NAME)
125    end
126
127    # Absolute path to `Pods/Target Support Files/<pods target name>/<modules provider file>` within the project path
128    public def modules_provider_path(target)
129      File.join(target.support_files_dir, modules_provider_name)
130    end
131
132    # For now there is no need to generate the modules provider for testing.
133    public def should_generate_modules_provider?
134      return !@options.fetch(:testsOnly, false)
135    end
136
137    # privates
138
139    private def resolve
140      json = []
141
142      IO.popen(resolve_command_args) do |data|
143        while line = data.gets
144          json << line
145        end
146      end
147
148      begin
149        JSON.parse(json.join())
150      rescue => error
151        raise "Couldn't parse JSON coming from `expo-modules-autolinking` command:\n#{error}"
152      end
153    end
154
155    public def base_command_args
156      search_paths = @options.fetch(:searchPaths, @options.fetch(:modules_paths, nil))
157      ignore_paths = @options.fetch(:ignorePaths, nil)
158      exclude = @options.fetch(:exclude, [])
159      args = []
160
161      if !search_paths.nil? && !search_paths.empty?
162        args.concat(search_paths)
163      end
164
165      if !ignore_paths.nil? && !ignore_paths.empty?
166        args.concat(['--ignore-paths'], ignore_paths)
167      end
168
169      if !exclude.nil? && !exclude.empty?
170        args.concat(['--exclude'], exclude)
171      end
172
173      args
174    end
175
176    private def node_command_args(command_name)
177      eval_command_args = [
178        'node',
179        '--no-warnings',
180        '--eval',
181        'require(require.resolve(\'expo-modules-autolinking\', { paths: [\'' +  __dir__ + '\'] }))(process.argv.slice(1))',
182        command_name,
183        '--platform',
184        'ios'
185      ]
186      return eval_command_args.concat(base_command_args())
187    end
188
189    private def resolve_command_args
190      node_command_args('resolve').concat(['--json'])
191    end
192
193    public def generate_package_list_command_args(target_path)
194      node_command_args('generate-package-list').concat([
195        '--target',
196        target_path
197      ])
198    end
199
200    private def get_podspec_for_pod(pod)
201      podspec_file_path = File.join(pod.podspec_dir, pod.pod_name + ".podspec")
202      return Pod::Specification.from_file(podspec_file_path)
203    end
204
205    private def use_modular_headers_for_dependencies(dependencies)
206      dependencies.each { |dependency|
207        # The dependency name might be a subspec like `ReactCommon/turbomodule/core`,
208        # but the modular headers need to be enabled for the entire `ReactCommon` spec anyway,
209        # so we're stripping the subspec path from the dependency name.
210        root_spec_name = dependency.name.partition('/').first
211
212        unless @target_definition.build_pod_as_module?(root_spec_name)
213          UI.info "[Expo] ".blue << "Enabling modular headers for pod #{root_spec_name.green}"
214
215          # This is an equivalent to setting `:modular_headers => true` for the specific dependency.
216          @target_definition.set_use_modular_headers_for_pod(root_spec_name, true)
217        end
218      }
219    end
220
221  end # class AutolinkingManager
222end # module Expo
223