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