CocoaPods/CocoaPods

View on GitHub
lib/cocoapods/installer/user_project_integrator/target_integrator.rb

Summary

Maintainability
D
2 days
Test Coverage
A
97%
require 'active_support/core_ext/string/inflections'
require 'cocoapods/xcode/framework_paths'
require 'cocoapods/target/build_settings'

module Pod
  class Installer
    class UserProjectIntegrator
      # This class is responsible for integrating the library generated by a
      # {TargetDefinition} with its destination project.
      #
      class TargetIntegrator
        autoload :XCConfigIntegrator, 'cocoapods/installer/user_project_integrator/target_integrator/xcconfig_integrator'

        # @return [String] the string to use as prefix for every build phase added to the user project
        #
        BUILD_PHASE_PREFIX = '[CP] '.freeze

        # @return [String] the string to use as prefix for every build phase declared by the user within a podfile
        #         or podspec.
        #
        USER_BUILD_PHASE_PREFIX = '[CP-User] '.freeze

        # @return [String] the name of the check manifest phase
        #
        CHECK_MANIFEST_PHASE_NAME = 'Check Pods Manifest.lock'.freeze

        # @return [Array<Symbol>] the symbol types, which require that the pod
        # frameworks are embedded in the output directory / product bundle.
        #
        # @note This does not include :app_extension or :watch_extension because
        # these types must have their frameworks embedded in their host targets.
        # For messages extensions, this only applies if it's embedded in a messages
        # application.
        #
        EMBED_FRAMEWORK_TARGET_TYPES = [:application, :application_on_demand_install_capable, :unit_test_bundle,
                                        :ui_test_bundle, :watch2_extension, :messages_application].freeze

        # @return [String] the name of the embed frameworks phase
        #
        EMBED_FRAMEWORK_PHASE_NAME = 'Embed Pods Frameworks'.freeze

        # @return [String] the name of the copy xcframeworks phase
        #
        COPY_XCFRAMEWORKS_PHASE_NAME = 'Copy XCFrameworks'.freeze

        # @return [String] the name of the copy resources phase
        #
        COPY_PODS_RESOURCES_PHASE_NAME = 'Copy Pods Resources'.freeze

        # @return [String] the name of the copy dSYM files phase
        #
        COPY_DSYM_FILES_PHASE_NAME = 'Copy dSYMs'.freeze

        # @return [Integer] the maximum number of input and output paths to use for a script phase
        #
        MAX_INPUT_OUTPUT_PATHS = 1000

        # @return [Array<String>] names of script phases that existed in previous versions of CocoaPods
        #
        REMOVED_SCRIPT_PHASE_NAMES = [
          'Prepare Artifacts'.freeze,
        ].freeze

        # @return [float] Returns Minimum Xcode Compatibility version for FileLists
        #
        MIN_FILE_LIST_COMPATIBILITY_VERSION = 9.3

        # @return [String] Returns Minimum Xcode Object version for FileLists
        #
        MIN_FILE_LIST_OBJECT_VERSION = 50

        # @return [AggregateTarget] the target that should be integrated.
        #
        attr_reader :target

        # @return [Boolean] whether to use input/output paths for build phase scripts
        #
        attr_reader :use_input_output_paths
        alias use_input_output_paths? use_input_output_paths

        # Init a new TargetIntegrator
        #
        # @param  [AggregateTarget] target @see #target
        # @param  [Boolean] use_input_output_paths @see #use_input_output_paths
        #
        def initialize(target, use_input_output_paths: true)
          @target = target
          @use_input_output_paths = use_input_output_paths
        end

        # @private
        #
        XCFileListConfigKey = Struct.new(:file_list_path, :file_list_relative_path)

        class << self
          # @param  [Xcodeproj::Project::Object::AbstractObject] object
          #
          # @return [Boolean] Whether input & output paths for the given object
          #         should be stored in a file list file.
          #
          def input_output_paths_use_filelist?(object)
            unless object.project.root_object.compatibility_version.nil?
              version_match = object.project.root_object.compatibility_version.match(/Xcode ([0-9]*\.[0-9]*)/).to_a
            end
            if version_match&.at(1).nil?
              object.project.object_version.to_i >= MIN_FILE_LIST_OBJECT_VERSION
            else
              Pod::Version.new(version_match[1]) >= Pod::Version.new(MIN_FILE_LIST_COMPATIBILITY_VERSION)
            end
          end

          # Sets the input & output paths for the given script build phase.
          #
          # @param  [Xcodeproj::Project::Object::PBXShellScriptBuildPhase] phase
          #         The phase to set input & output paths on.
          #
          # @param  [Hash] input_paths_by_config
          #
          # @return [Void]
          def set_input_output_paths(phase, input_paths_by_config, output_paths_by_config)
            if input_output_paths_use_filelist?(phase)
              [input_paths_by_config, output_paths_by_config].each do |hash|
                hash.each do |file_list, files|
                  generator = Generator::FileList.new(files)
                  Xcode::PodsProjectGenerator::TargetInstallerHelper.update_changed_file(generator, file_list.file_list_path)
                end
              end

              phase.input_paths = nil
              phase.output_paths = nil
              phase.input_file_list_paths = input_paths_by_config.each_key.map(&:file_list_relative_path).uniq
              phase.output_file_list_paths = output_paths_by_config.each_key.map(&:file_list_relative_path).uniq
            else
              input_paths = input_paths_by_config.values.flatten(1).uniq
              output_paths = output_paths_by_config.values.flatten(1).uniq
              TargetIntegrator.validate_input_output_path_limit(input_paths, output_paths)

              phase.input_paths = input_paths
              phase.output_paths = output_paths
              phase.input_file_list_paths = nil
              phase.output_file_list_paths = nil
            end
          end

          # Adds a shell script build phase responsible to copy (embed) the frameworks
          # generated by the TargetDefinition to the bundle of the product of the
          # targets.
          #
          # @param [PBXNativeTarget] native_target
          #        The native target to add the script phase into.
          #
          # @param [String] script_path
          #        The script path to execute as part of this script phase.
          #
          # @param [Hash<Array, String>] input_paths_by_config
          #        The input paths (if any) to include for this script phase.
          #
          # @param [Hash<Array, String>] output_paths_by_config
          #        The output paths (if any) to include for this script phase.
          #
          # @return [void]
          #
          def create_or_update_embed_frameworks_script_phase_to_target(native_target, script_path, input_paths_by_config = {}, output_paths_by_config = {})
            phase = TargetIntegrator.create_or_update_shell_script_build_phase(native_target, BUILD_PHASE_PREFIX + EMBED_FRAMEWORK_PHASE_NAME)
            phase.shell_script = %("#{script_path}"\n)
            TargetIntegrator.set_input_output_paths(phase, input_paths_by_config, output_paths_by_config)
          end

          # Delete a 'Embed Pods Frameworks' Script Build Phase if present
          #
          # @param [PBXNativeTarget] native_target
          #        The native target to remove the script phase from.
          #
          def remove_embed_frameworks_script_phase_from_target(native_target)
            remove_script_phase_from_target(native_target, EMBED_FRAMEWORK_PHASE_NAME)
          end

          # Adds a shell script build phase responsible to copy the xcframework slice
          # to the intermediate build directory.
          #
          # @param [PBXNativeTarget] native_target
          #        The native target to add the script phase into.
          #
          # @param [String] script_path
          #        The script path to execute as part of this script phase.
          #
          # @param [Hash<Array, String>] input_paths_by_config
          #        The input paths (if any) to include for this script phase.
          #
          # @param [Hash<Array, String>] output_paths_by_config
          #        The output paths (if any) to include for this script phase.
          #
          # @return [void]
          #
          def create_or_update_copy_xcframeworks_script_phase_to_target(native_target, script_path, input_paths_by_config = {}, output_paths_by_config = {})
            phase = TargetIntegrator.create_or_update_shell_script_build_phase(native_target, BUILD_PHASE_PREFIX + COPY_XCFRAMEWORKS_PHASE_NAME)
            phase.shell_script = %("#{script_path}"\n)
            TargetIntegrator.set_input_output_paths(phase, input_paths_by_config, output_paths_by_config)
            reorder_script_phase(native_target, phase, :before_compile)
          end

          # Delete a 'Copy XCFrameworks' Script Build Phase if present
          #
          # @param [PBXNativeTarget] native_target
          #        The native target to remove the script phase from.
          #
          def remove_copy_xcframeworks_script_phase_from_target(native_target)
            remove_script_phase_from_target(native_target, COPY_XCFRAMEWORKS_PHASE_NAME)
          end

          # Removes a script phase from a native target by name
          #
          # @param [PBXNativeTarget] native_target
          #        The target from which the script phased should be removed
          #
          # @param [String] phase_name
          #        The name of the script phase to remove
          #
          def remove_script_phase_from_target(native_target, phase_name)
            build_phase = native_target.shell_script_build_phases.find { |bp| bp.name && bp.name.end_with?(phase_name) }
            return unless build_phase.present?
            native_target.build_phases.delete(build_phase)
          end

          # Adds a shell script build phase responsible to copy the resources
          # generated by the TargetDefinition to the bundle of the product of the
          # targets.
          #
          # @param [PBXNativeTarget] native_target
          #        The native target to add the script phase into.
          #
          # @param [String] script_path
          #        The script path to execute as part of this script phase.
          #
          # @param [Hash<Array, String>] input_paths_by_config
          #        The input paths (if any) to include for this script phase.
          #
          # @param [Hash<Array, String>] output_paths_by_config
          #        The output paths (if any) to include for this script phase.
          #
          # @return [void]
          #
          def create_or_update_copy_resources_script_phase_to_target(native_target, script_path, input_paths_by_config = {}, output_paths_by_config = {})
            phase_name = COPY_PODS_RESOURCES_PHASE_NAME
            phase = TargetIntegrator.create_or_update_shell_script_build_phase(native_target, BUILD_PHASE_PREFIX + phase_name)
            phase.shell_script = %("#{script_path}"\n)
            TargetIntegrator.set_input_output_paths(phase, input_paths_by_config, output_paths_by_config)
          end

          # Delete a 'Copy Pods Resources' script phase if present
          #
          # @param [PBXNativeTarget] native_target
          #        The native target to remove the script phase from.
          #
          def remove_copy_resources_script_phase_from_target(native_target)
            build_phase = native_target.shell_script_build_phases.find { |bp| bp.name && bp.name.end_with?(COPY_PODS_RESOURCES_PHASE_NAME) }
            return unless build_phase.present?
            native_target.build_phases.delete(build_phase)
          end

          # Creates or update a shell script build phase for the given target.
          #
          # @param [PBXNativeTarget] native_target
          #        The native target to add the script phase into.
          #
          # @param [String] script_phase_name
          #        The name of the script phase to use.
          #
          # @param [String] show_env_vars_in_log
          #        The value to set for show environment variables in the log during execution of this script phase or
          #        `nil` for not setting the value at all.
          #
          # @return [PBXShellScriptBuildPhase] The existing or newly created shell script build phase.
          #
          def create_or_update_shell_script_build_phase(native_target, script_phase_name, show_env_vars_in_log = '0')
            build_phases = native_target.build_phases.grep(Xcodeproj::Project::Object::PBXShellScriptBuildPhase)
            build_phases.find { |phase| phase.name && phase.name.end_with?(script_phase_name) }.tap { |p| p.name = script_phase_name if p } ||
              native_target.project.new(Xcodeproj::Project::Object::PBXShellScriptBuildPhase).tap do |phase|
                UI.message("Adding Build Phase '#{script_phase_name}' to project.") do
                  phase.name = script_phase_name
                  unless show_env_vars_in_log.nil?
                    phase.show_env_vars_in_log = show_env_vars_in_log
                  end
                  native_target.build_phases << phase
                end
              end
          end

          # Updates all target script phases for the current target, including creating or updating, deleting
          # and re-ordering.
          #
          # @return [void]
          #
          def create_or_update_user_script_phases(script_phases, native_target)
            script_phase_names = script_phases.map { |k| k[:name] }
            # Delete script phases no longer present in the target.
            native_target_script_phases = native_target.shell_script_build_phases.select do |bp|
              !bp.name.nil? && bp.name.start_with?(USER_BUILD_PHASE_PREFIX)
            end
            native_target_script_phases.each do |script_phase|
              script_phase_name_without_prefix = script_phase.name.sub(USER_BUILD_PHASE_PREFIX, '')
              unless script_phase_names.include?(script_phase_name_without_prefix)
                native_target.build_phases.delete(script_phase)
              end
            end
            # Create or update the ones that are expected to be.
            script_phases.each do |script_phase|
              name_with_prefix = USER_BUILD_PHASE_PREFIX + script_phase[:name]
              phase = TargetIntegrator.create_or_update_shell_script_build_phase(native_target, name_with_prefix, nil)
              phase.shell_script = script_phase[:script]
              phase.shell_path = script_phase[:shell_path] || '/bin/sh'
              phase.input_paths = script_phase[:input_files]
              phase.output_paths = script_phase[:output_files]
              phase.input_file_list_paths = script_phase[:input_file_lists]
              phase.output_file_list_paths = script_phase[:output_file_lists]
              phase.dependency_file = script_phase[:dependency_file]
              phase.always_out_of_date = script_phase[:always_out_of_date]
              # At least with Xcode 10 `showEnvVarsInLog` is *NOT* set to any value even if it's checked and it only
              # gets set to '0' if the user has explicitly disabled this.
              if (show_env_vars_in_log = script_phase.fetch(:show_env_vars_in_log, '1')) == '0'
                phase.show_env_vars_in_log = show_env_vars_in_log
              end

              execution_position = script_phase[:execution_position]
              reorder_script_phase(native_target, phase, execution_position)
            end
          end

          def reorder_script_phase(native_target, script_phase, execution_position)
            return if execution_position == :any || execution_position.to_s.empty?
            target_phase_type = case execution_position
                                when :before_compile, :after_compile
                                  Xcodeproj::Project::Object::PBXSourcesBuildPhase
                                when :before_headers, :after_headers
                                  Xcodeproj::Project::Object::PBXHeadersBuildPhase
                                else
                                  raise ArgumentError, "Unknown execution position `#{execution_position}`"
                                end
            order_before = case execution_position
                           when :before_compile, :before_headers
                             true
                           when :after_compile, :after_headers
                             false
                           else
                             raise ArgumentError, "Unknown execution position `#{execution_position}`"
                           end

            target_phase_index = native_target.build_phases.index do |bp|
              bp.is_a?(target_phase_type)
            end
            return if target_phase_index.nil?
            script_phase_index = native_target.build_phases.index do |bp|
              bp.is_a?(Xcodeproj::Project::Object::PBXShellScriptBuildPhase) && !bp.name.nil? && bp.name == script_phase.name
            end
            if (order_before && script_phase_index > target_phase_index) ||
              (!order_before && script_phase_index < target_phase_index)
              native_target.build_phases.move_from(script_phase_index, target_phase_index)
            end
          end

          # Script phases can have a limited number of input and output paths due to each one being exported to `env`.
          # A large number can cause a build failure because of limitations in `env`. See issue
          # https://github.com/CocoaPods/CocoaPods/issues/7362.
          #
          # @param [Array<String>] input_paths
          #        The input paths to trim.
          #
          # @param [Array<String>] output_paths
          #        The output paths to trim.
          #
          # @return [void]
          #
          def validate_input_output_path_limit(input_paths, output_paths)
            if (input_paths.count + output_paths.count) > MAX_INPUT_OUTPUT_PATHS
              input_paths.clear
              output_paths.clear
            end
          end

          # Returns the resource output paths for all given input paths.
          #
          # @param [Array<String>] resource_input_paths
          #        The input paths to map to.
          #
          # @return [Array<String>] The resource output paths.
          #
          def resource_output_paths(resource_input_paths)
            resource_input_paths.map do |resource_input_path|
              base_path = '${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}'
              extname = File.extname(resource_input_path)
              basename = extname == '.xcassets' ? 'Assets' : File.basename(resource_input_path)
              output_extension = Target.output_extension_for_resource(extname)
              File.join(base_path, File.basename(basename, extname) + output_extension)
            end.uniq
          end

          # Returns the framework input paths for the given framework paths
          #
          # @param  [Array<Xcode::FrameworkPaths>] framework_paths
          #         The target's framework paths to map to input paths.
          #
          # @param  [Array<XCFramework>] xcframeworks
          #         The target's xcframeworks to map to input paths.
          #
          # @return [Array<String>] The embed frameworks script input paths
          #
          def embed_frameworks_input_paths(framework_paths, xcframeworks)
            input_paths = framework_paths.map(&:source_path)
            # Only include dynamic xcframeworks as the input since we will not be copying static xcframework slices
            xcframeworks.select { |xcf| xcf.build_type.dynamic_framework? }.each do |xcframework|
              name = xcframework.name
              input_paths << "#{Pod::Target::BuildSettings.xcframework_intermediate_dir(xcframework)}/#{name}.framework/#{name}"
            end
            input_paths
          end

          # Returns the framework output paths for the given framework paths
          #
          # @param  [Array<Xcode::FrameworkPaths>] framework_paths
          #         The framework input paths to map to output paths.
          #
          # @param  [Array<XCFramework>] xcframeworks
          #         The installed xcframeworks.
          #
          # @return [Array<String>] The embed framework script output paths
          #
          def embed_frameworks_output_paths(framework_paths, xcframeworks)
            paths = framework_paths.map do |framework_path|
              "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/#{File.basename(framework_path.source_path)}"
            end.uniq
            # Static xcframeworks are not copied to the build dir
            # so only include dynamic artifacts that will be copied to the build folder
            xcframework_paths = xcframeworks.select { |xcf| xcf.build_type.dynamic_framework? }.map do |xcframework|
              "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/#{xcframework.name}.framework"
            end
            paths + xcframework_paths
          end

          # Updates a projects native targets to include on demand resources specified by the supplied parameters.
          # Note that currently, only app level targets are allowed to include on demand resources.
          #
          # @param  [Sandbox] sandbox
          #         The sandbox to use for calculating ODR file references.
          #
          # @param  [Xcodeproj::Project] project
          #         The project to update known asset tags as well as add the ODR group.
          #
          # @param  [Xcodeproj::PBXNativeTarget, Array<Xcodeproj::PBXNativeTarget>] native_targets
          #         The native targets to integrate on demand resources into.
          #
          # @param  [Sandbox::FileAccessor, Array<Sandbox::FileAccessor>] file_accessors
          #         The file accessors that that provide the ODRs to integrate.
          #
          # @param  [Xcodeproj::PBXGroup] parent_odr_group
          #         The group to use as the parent to add ODR file references into.
          #
          # @param  [String] target_odr_group_name
          #         The name to use for the group created that contains the ODR file references.
          #
          # @return [void]
          #
          def update_on_demand_resources(sandbox, project, native_targets, file_accessors, parent_odr_group,
                                         target_odr_group_name)
            category_to_tags = {}
            file_accessors = Array(file_accessors)
            native_targets = Array(native_targets)

            # Target no longer provides ODR references so remove everything related to this target.
            if file_accessors.all? { |fa| fa.on_demand_resources.empty? }
              old_target_odr_group = parent_odr_group[target_odr_group_name]
              old_odr_file_refs = old_target_odr_group&.recursive_children_groups&.each_with_object({}) do |group, hash|
                hash[group.name] = group.files
              end || {}
              native_targets.each do |native_target|
                native_target.remove_on_demand_resources(old_odr_file_refs)
                update_on_demand_resources_build_settings(native_target, nil => old_odr_file_refs.keys)
              end
              old_target_odr_group&.remove_from_project
              return
            end

            target_odr_group = parent_odr_group[target_odr_group_name] || parent_odr_group.new_group(target_odr_group_name)
            current_file_refs = target_odr_group.recursive_children_groups.flat_map(&:files)

            added_file_refs = file_accessors.flat_map do |file_accessor|
              target_odr_files_refs = Hash[file_accessor.on_demand_resources.map do |tag, value|
                tag_group = target_odr_group[tag] || target_odr_group.new_group(tag)
                category_to_tags[value[:category]] ||= []
                category_to_tags[value[:category]] << tag
                resources_file_refs = value[:paths].map do |resource|
                  odr_resource_file_ref = Pathname.new(resource).relative_path_from(sandbox.root)
                  tag_group.find_file_by_path(odr_resource_file_ref.to_s) || tag_group.new_file(odr_resource_file_ref)
                end
                [tag, resources_file_refs]
              end]
              native_targets.each do |native_target|
                native_target.add_on_demand_resources(target_odr_files_refs)
              end
              target_odr_files_refs.values.flatten
            end

            # if the target ODR file references were updated, make sure we remove the ones that are no longer present
            # for the target.
            remaining_refs = current_file_refs - added_file_refs
            remaining_refs.each do |ref|
              native_targets.each do |user_target|
                user_target.resources_build_phase.remove_file_reference(ref)
              end
              ref.remove_from_project
            end
            target_odr_group.recursive_children_groups.each { |g| g.remove_from_project if g.empty? }

            attributes = project.root_object.attributes
            attributes['KnownAssetTags'] = (attributes['KnownAssetTags'] ||= []) | category_to_tags.values.flatten
            project.root_object.attributes = attributes

            native_targets.each do |native_target|
              update_on_demand_resources_build_settings(native_target, category_to_tags)
            end
          end

          def update_on_demand_resources_build_settings(native_target, category_to_tags)
            %w[ON_DEMAND_RESOURCES_INITIAL_INSTALL_TAGS ON_DEMAND_RESOURCES_PREFETCH_ORDER].each do |category_key|
              native_target.build_configurations.each do |c|
                key = case category_key
                      when 'ON_DEMAND_RESOURCES_INITIAL_INSTALL_TAGS'
                        :initial_install
                      when 'ON_DEMAND_RESOURCES_PREFETCH_ORDER'
                        :prefetched
                      else
                        :download_on_demand
                      end
                tags_for_category = (c.build_settings[category_key] || '').split
                category_to_tags_dup = category_to_tags.dup
                tags_to_add = category_to_tags_dup.delete(key) || []
                tags_to_delete = category_to_tags_dup.values.flatten
                tags_for_category = (tags_for_category + tags_to_add - tags_to_delete).flatten.compact.uniq
                if tags_for_category.empty?
                  val = c.build_settings.delete(category_key)
                  native_target.project.mark_dirty! unless val.nil?
                else
                  tags = tags_for_category.join(' ')
                  unless c.build_settings[category_key] == tags
                    c.build_settings[category_key] = tags
                    native_target.project.mark_dirty!
                  end
                end
              end
            end
          end
        end

        # Integrates the user project targets. Only the targets that do **not**
        # already have the Pods library in their frameworks build phase are
        # processed.
        #
        # @return [void]
        #
        def integrate!
          UI.section(integration_message) do
            XCConfigIntegrator.integrate(target, native_targets)

            remove_obsolete_script_phases
            add_pods_library
            add_embed_frameworks_script_phase
            remove_embed_frameworks_script_phase_from_embedded_targets
            add_copy_resources_script_phase
            add_check_manifest_lock_script_phase
            add_user_script_phases
            add_on_demand_resources
          end
        end

        # @return [String] a string representation suitable for debugging.
        #
        def inspect
          "#<#{self.class} for target `#{target.label}'>"
        end

        private

        # @!group Integration steps
        #---------------------------------------------------------------------#

        # Adds spec product reference to the frameworks build phase of the
        # {TargetDefinition} integration libraries. Adds a file reference to
        # the frameworks group of the project and adds it to the frameworks
        # build phase of the targets.
        #
        # @return [void]
        #
        def add_pods_library
          frameworks = user_project.frameworks_group
          native_targets.each do |native_target|
            build_phase = native_target.frameworks_build_phase
            product_name = target.product_name

            # Delete previously integrated references.
            product_build_files = build_phase.files.select do |build_file|
              build_file.display_name =~ Pod::Deintegrator::FRAMEWORK_NAMES
            end

            product_build_files.each do |product_file|
              next unless product_name != product_file.display_name
              UI.message("Removing old product reference `#{product_file.display_name}` from project.")
              frameworks.remove_reference(product_file.file_ref)
              build_phase.remove_build_file(product_file)
            end

            # Find or create and add a reference for the current product type
            new_product_ref = frameworks.files.find { |f| f.path == product_name } ||
                frameworks.new_product_ref_for_target(target.product_basename, target.product_type)
            build_phase.build_file(new_product_ref) ||
                build_phase.add_file_reference(new_product_ref, true)
          end
        end

        # Find or create a 'Copy Pods Resources' build phase
        #
        # @return [void]
        #
        def add_copy_resources_script_phase
          unless target.includes_resources?
            native_targets.each do |native_target|
              TargetIntegrator.remove_copy_resources_script_phase_from_target(native_target)
            end
            return
          end

          script_path = target.copy_resources_script_relative_path
          input_paths_by_config = {}
          output_paths_by_config = {}
          if use_input_output_paths
            target.resource_paths_by_config.each do |config, resource_paths|
              input_paths_key = XCFileListConfigKey.new(target.copy_resources_script_input_files_path(config),
                                                        target.copy_resources_script_input_files_relative_path)
              input_paths_by_config[input_paths_key] = [script_path] + resource_paths

              output_paths_key = XCFileListConfigKey.new(target.copy_resources_script_output_files_path(config),
                                                         target.copy_resources_script_output_files_relative_path)
              output_paths_by_config[output_paths_key] = TargetIntegrator.resource_output_paths(resource_paths)
            end
          end

          native_targets.each do |native_target|
            # Static library targets cannot include resources. Skip this phase from being added instead.
            next if native_target.symbol_type == :static_library
            TargetIntegrator.create_or_update_copy_resources_script_phase_to_target(native_target, script_path,
                                                                                    input_paths_by_config,
                                                                                    output_paths_by_config)
          end
        end

        # Removes the embed frameworks build phase from embedded targets
        #
        # @note Older versions of CocoaPods would add this build phase to embedded
        #       targets. They should be removed on upgrade because embedded targets
        #       will have their frameworks embedded in their host targets.
        #
        def remove_embed_frameworks_script_phase_from_embedded_targets
          return unless target.requires_host_target?
          native_targets.each do |native_target|
            if AggregateTarget::EMBED_FRAMEWORKS_IN_HOST_TARGET_TYPES.include? native_target.symbol_type
              TargetIntegrator.remove_embed_frameworks_script_phase_from_target(native_target)
            end
          end
        end

        # Find or create a 'Embed Pods Frameworks' Copy Files Build Phase
        #
        # @return [void]
        #
        def add_embed_frameworks_script_phase
          unless target.includes_frameworks? || (target.xcframeworks_by_config.values.flatten.any? { |xcf| xcf.build_type.dynamic_framework? })
            native_targets_to_embed_in.each do |native_target|
              TargetIntegrator.remove_embed_frameworks_script_phase_from_target(native_target)
            end
            return
          end

          script_path = target.embed_frameworks_script_relative_path
          input_paths_by_config = {}
          output_paths_by_config = {}
          if use_input_output_paths?
            configs = Set.new(target.framework_paths_by_config.keys + target.xcframeworks_by_config.keys).sort
            configs.each do |config|
              framework_paths = target.framework_paths_by_config[config] || []
              xcframeworks = target.xcframeworks_by_config[config] || []

              input_paths_key = XCFileListConfigKey.new(target.embed_frameworks_script_input_files_path(config), target.embed_frameworks_script_input_files_relative_path)
              input_paths_by_config[input_paths_key] = [script_path] + TargetIntegrator.embed_frameworks_input_paths(framework_paths, xcframeworks)

              output_paths_key = XCFileListConfigKey.new(target.embed_frameworks_script_output_files_path(config), target.embed_frameworks_script_output_files_relative_path)
              output_paths_by_config[output_paths_key] = TargetIntegrator.embed_frameworks_output_paths(framework_paths, xcframeworks)
            end
          end

          native_targets_to_embed_in.each do |native_target|
            TargetIntegrator.create_or_update_embed_frameworks_script_phase_to_target(native_target, script_path, input_paths_by_config, output_paths_by_config)
          end
        end

        # Updates all target script phases for the current target, including creating or updating, deleting
        # and re-ordering.
        #
        # @return [void]
        #
        def add_user_script_phases
          native_targets.each do |native_target|
            TargetIntegrator.create_or_update_user_script_phases(target.target_definition.script_phases, native_target)
          end
        end

        # Adds a shell script build phase responsible for checking if the Pods
        # locked in the Pods/Manifest.lock file are in sync with the Pods defined
        # in the Podfile.lock.
        #
        # @note   The build phase is appended to the front because to fail
        #         fast.
        #
        # @return [void]
        #
        def add_check_manifest_lock_script_phase
          phase_name = CHECK_MANIFEST_PHASE_NAME
          native_targets.each do |native_target|
            phase = TargetIntegrator.create_or_update_shell_script_build_phase(native_target, BUILD_PHASE_PREFIX + phase_name)
            native_target.build_phases.unshift(phase).uniq! unless native_target.build_phases.first == phase
            phase.shell_script = <<-SH.strip_heredoc
              diff "${PODS_PODFILE_DIR_PATH}/Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null
              if [ $? != 0 ] ; then
                  # print error to STDERR
                  echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2
                  exit 1
              fi
              # This output is used by Xcode 'outputs' to avoid re-running this script phase.
              echo "SUCCESS" > "${SCRIPT_OUTPUT_FILE_0}"
            SH
            phase.input_paths = %w(${PODS_PODFILE_DIR_PATH}/Podfile.lock ${PODS_ROOT}/Manifest.lock)
            phase.output_paths = [target.check_manifest_lock_script_output_file_path]
          end
        end

        # @param [Array<String>] removed_phase_names
        #        The names of the script phases that should be removed
        #
        def remove_obsolete_script_phases(removed_phase_names = REMOVED_SCRIPT_PHASE_NAMES)
          native_targets.each do |native_target|
            removed_phase_names.each do |phase_name|
              TargetIntegrator.remove_script_phase_from_target(native_target, phase_name)
            end
          end
        end

        def add_on_demand_resources
          target.pod_targets.each do |pod_target|
            # When integrating with the user's project we are only interested in integrating ODRs from library specs
            # and not test specs or app specs.
            library_file_accessors = pod_target.file_accessors.select { |fa| fa.spec.library_specification? }
            target_odr_group_name = "#{pod_target.label}-OnDemandResources"
            # The 'Pods' group would always be there for production code however for tests its sometimes not added.
            # This ensures its always present and makes it easier for existing and new tests.
            parent_odr_group = target.user_project.main_group['Pods'] || target.user_project.new_group('Pods')
            TargetIntegrator.update_on_demand_resources(target.sandbox, target.user_project, target.user_targets,
                                                        library_file_accessors, parent_odr_group, target_odr_group_name)
          end
        end

        private

        # @!group Private Helpers
        #---------------------------------------------------------------------#

        # @return [Array<PBXNativeTarget>] The list of all the targets that
        #         match the given target.
        #
        def native_targets
          @native_targets ||= target.user_targets
        end

        # @return [Array<PBXNativeTarget>] The list of all the targets that
        #         require that the pod frameworks are embedded in the output
        #         directory / product bundle.
        #
        def native_targets_to_embed_in
          return [] if target.requires_host_target?
          native_targets.select do |target|
            EMBED_FRAMEWORK_TARGET_TYPES.include?(target.symbol_type)
          end
        end

        # Read the project from the disk to ensure that it is up to date as
        # other TargetIntegrators might have modified it.
        #
        # @return [Project]
        #
        def user_project
          target.user_project
        end

        # @return [Specification::Consumer] the consumer for the specifications.
        #
        def spec_consumers
          @spec_consumers ||= target.pod_targets.map(&:file_accessors).flatten.map(&:spec_consumer)
        end

        # @return [String] the message that should be displayed for the target
        #         integration.
        #
        def integration_message
          "Integrating target `#{target.name}` " \
            "(#{UI.path target.user_project_path} project)"
        end
      end
    end
  end
end