require_relative 'constants' require_relative 'package' require_relative 'packages_config' # Require extensions to CocoaPods' classes require_relative 'cocoapods/sandbox' require_relative 'cocoapods/target_definition' require_relative 'cocoapods/user_project_integrator' require_relative 'cocoapods/installer' module Expo class AutolinkingManager require 'colored2' include Pod public def initialize(podfile, target_definition, options) @podfile = podfile @target_definition = target_definition @options = options validate_target_definition() resolve_result = resolve() Expo::PackagesConfig.instance.coreFeatures = resolve_result['coreFeatures'] @packages = resolve_result['modules'].map { |json_package| Package.new(json_package) } @extraPods = resolve_result['extraDependencies'] end public def use_expo_modules! if has_packages? return end global_flags = @options.fetch(:flags, {}) tests_only = @options.fetch(:testsOnly, false) include_tests = @options.fetch(:includeTests, false) # Add any additional framework modules to patch using the patched Podfile class in installer.rb # We'll be reading from Podfile.properties.json and optionally parameters passed to use_expo_modules! podfile_properties = JSON.parse(File.read(File.join(Pod::Config.instance.project_root, 'Podfile.properties.json'))) rescue {} additional_framework_modules_to_patch = @options.fetch(:additionalFrameworkModulesToPatch, []) + JSON.parse(podfile_properties['ios.forceStaticLinking'] || "[]") Pod::UI.puts("Forcing static linking for pods: #{additional_framework_modules_to_patch}") if !additional_framework_modules_to_patch.empty? @podfile.expo_add_modules_to_patch(additional_framework_modules_to_patch) if !additional_framework_modules_to_patch.empty? project_directory = Pod::Config.instance.project_root UI.section 'Using Expo modules' do @packages.each { |package| package.pods.each { |pod| # The module can already be added to the target, in which case we can just skip it. # This allows us to add a pod before `use_expo_modules` to provide custom flags. if @target_definition.dependencies.any? { |dependency| dependency.name == pod.pod_name } UI.message '— ' << package.name.green << ' is already added to the target'.yellow next end # Skip if the podspec doesn't include the platform for the current target. unless pod.supports_platform?(@target_definition.platform) UI.message '- ' << package.name.green << " doesn't support #{@target_definition.platform.string_name} platform".yellow next end # Ensure that the dependencies of packages with Swift code use modular headers, otherwise # `pod install` may fail if there is no `use_modular_headers!` declaration or # `:modular_headers => true` is not used for this particular dependency. # The latter require adding transitive dependencies to user's Podfile that we'd rather like to avoid. if package.has_something_to_link? use_modular_headers_for_dependencies(pod.spec.all_dependencies) end podspec_dir_path = Pathname.new(pod.podspec_dir).relative_path_from(project_directory).to_path debug_configurations = @target_definition.build_configurations ? @target_definition.build_configurations.select { |config| config.include?('Debug') }.keys : ['Debug'] pod_options = { :path => podspec_dir_path, :configuration => package.debugOnly ? debug_configurations : [] # An empty array means all configurations }.merge(global_flags, package.flags) if tests_only || include_tests test_specs_names = pod.spec.test_specs.map { |test_spec| test_spec.name.delete_prefix(pod.spec.name + "/") } # Jump to the next package when it doesn't have any test specs (except interfaces, they're required) # TODO: Can remove interface check once we move all the interfaces into the core. next if tests_only && test_specs_names.empty? && !pod.pod_name.end_with?('Interface') pod_options[:testspecs] = test_specs_names end # Install the pod. @podfile.pod(pod.pod_name, pod_options) # TODO: Can remove this once we move all the interfaces into the core. next if pod.pod_name.end_with?('Interface') UI.message "— #{package.name.green} (#{package.version})" } } end @extraPods.each { |pod| UI.info "Adding extra pod - #{pod['name']} (#{pod['version'] || '*'})" requirements = Array.new requirements << pod['version'] if pod['version'] options = Hash.new options[:configurations] = pod['configurations'] if pod['configurations'] options[:modular_headers] = pod['modular_headers'] if pod['modular_headers'] options[:source] = pod['source'] if pod['source'] options[:path] = pod['path'] if pod['path'] options[:podspec] = pod['podspec'] if pod['podspec'] options[:testspecs] = pod['testspecs'] if pod['testspecs'] options[:git] = pod['git'] if pod['git'] options[:branch] = pod['branch'] if pod['branch'] options[:tag] = pod['tag'] if pod['tag'] options[:commit] = pod['commit'] if pod['commit'] requirements << options @podfile.pod(pod['name'], *requirements) } self end # Spawns `expo-module-autolinking generate-modules-provider` command. public def generate_modules_provider(target_name, target_path) Process.wait IO.popen(generate_modules_provider_command_args(target_path)).pid end # If there is any package to autolink. public def has_packages? @packages.empty? end # Filters only these packages that needs to be included in the generated modules provider. public def packages_to_generate platform = @target_definition.platform @packages.select do |package| # Check whether the package has any module to autolink # and if there is any pod that supports target's platform. package.has_something_to_link? && package.pods.any? { |pod| pod.supports_platform?(platform) } end end # Returns the provider name which is also a name of the generated file public def modules_provider_name @options.fetch(:providerName, Constants::MODULES_PROVIDER_FILE_NAME) end # Absolute path to `Pods/Target Support Files//` within the project path public def modules_provider_path(target) File.join(target.support_files_dir, modules_provider_name) end # For now there is no need to generate the modules provider for testing. public def should_generate_modules_provider? return !@options.fetch(:testsOnly, false) end # Returns the platform name of the current target definition. # Note that it is suitable to be presented to the user (i.e. is not lowercased). public def platform_name return @target_definition.platform&.string_name end # Returns the app project root if provided in the options. public def custom_app_root # TODO: Follow up on renaming `:projectRoot` and migrate to `appRoot` return @options.fetch(:appRoot, @options.fetch(:projectRoot, nil)) end # privates private def resolve json = [] IO.popen(resolve_command_args) do |data| while line = data.gets json << line end end begin JSON.parse(json.join()) rescue => error raise "Couldn't parse JSON coming from `expo-modules-autolinking` command:\n#{error}" end end public def base_command_args search_paths = @options.fetch(:searchPaths, @options.fetch(:modules_paths, nil)) ignore_paths = @options.fetch(:ignorePaths, nil) exclude = @options.fetch(:exclude, []) args = [] if !search_paths.nil? && !search_paths.empty? args.concat(search_paths) end if !ignore_paths.nil? && !ignore_paths.empty? args.concat(['--ignore-paths'], ignore_paths) end if !exclude.nil? && !exclude.empty? args.concat(['--exclude'], exclude) end args end private def node_command_args(command_name) eval_command_args = [ 'node', '--no-warnings', '--eval', 'require(\'expo/bin/autolinking\')', 'expo-modules-autolinking', command_name, '--platform', 'apple' ] return eval_command_args.concat(base_command_args()) end private def resolve_command_args resolve_command_args = ['--json'] project_root = @options.fetch(:projectRoot, nil) if project_root resolve_command_args.concat(['--project-root', project_root]) end node_command_args('resolve').concat(resolve_command_args) end public def generate_modules_provider_command_args(target_path) command_args = ['--target', target_path] if !custom_app_root.nil? command_args.concat(['--app-root', custom_app_root]) end node_command_args('generate-modules-provider').concat( command_args, ['--packages'], packages_to_generate.map(&:name) ) end private def use_modular_headers_for_dependencies(dependencies) dependencies.each { |dependency| # The dependency name might be a subspec like `ReactCommon/turbomodule/core`, # but the modular headers need to be enabled for the entire `ReactCommon` spec anyway, # so we're stripping the subspec path from the dependency name. root_spec_name = dependency.name.partition('/').first unless @target_definition.build_pod_as_module?(root_spec_name) UI.info "[Expo] ".blue << "Enabling modular headers for pod #{root_spec_name.green}" # This is an equivalent to setting `:modular_headers => true` for the specific dependency. @target_definition.set_use_modular_headers_for_pod(root_spec_name, true) end } end # Validates whether the Expo modules can be autolinked in the given target definition. private def validate_target_definition # The platform must be declared within the current target (e.g. `platform :ios, '13.0'`) if platform_name.nil? raise "Undefined platform for target #{@target_definition.name}, make sure to call `platform` method globally or inside the target" end # The declared platform must be iOS, macOS or tvOS, others are not supported. unless ['iOS', 'macOS', 'tvOS'].include?(platform_name) raise "Target #{@target_definition.name} is dedicated to #{platform_name} platform, which is not supported by Expo Modules" end end end # class AutolinkingManager end # module Expo