CocoaPods/CocoaPods

View on GitHub
lib/cocoapods/target/aggregate_target.rb

Summary

Maintainability
C
1 day
Test Coverage
A
95%
require 'cocoapods/xcode/framework_paths'
require 'cocoapods/xcode/xcframework'

module Pod
  # Stores the information relative to the target used to cluster the targets
  # of the single Pods. The client targets will then depend on this one.
  #
  class AggregateTarget < Target
    # Product types where the product's frameworks must be embedded in a host target
    #
    EMBED_FRAMEWORKS_IN_HOST_TARGET_TYPES = [:app_extension, :framework, :static_library, :messages_extension,
                                             :watch_extension, :xpc_service].freeze

    # @return [TargetDefinition] the target definition of the Podfile that
    #         generated this target.
    #
    attr_reader :target_definition

    # @return [Pathname] the folder where the client is stored used for
    #         computing the relative paths. If integrating it should be the
    #         folder where the user project is stored, otherwise it should
    #         be the installation root.
    #
    attr_reader :client_root

    # @return [Xcodeproj::Project] the user project that this target will
    #         integrate as identified by the analyzer.
    #
    attr_reader :user_project

    # @return [Array<String>] the list of the UUIDs of the user targets that
    #         will be integrated by this target as identified by the analyzer.
    #
    # @note   The target instances are not stored to prevent editing different
    #         instances.
    #
    attr_reader :user_target_uuids

    # @return [Hash<String, Xcodeproj::Config>] Map from configuration name to
    #         configuration file for the target
    #
    # @note   The configurations are generated by the {TargetInstaller} and
    #         used by {UserProjectIntegrator} to check for any overridden
    #         values.
    #
    attr_reader :xcconfigs

    # @return [Array<PodTarget>] The dependencies for this target.
    #
    attr_reader :pod_targets

    # @return [Array<AggregateTarget>] The aggregate targets whose pods this
    #         target must be able to import, but will not directly link against.
    #
    attr_reader :search_paths_aggregate_targets

    # Initialize a new instance
    #
    # @param [Sandbox] sandbox @see Target#sandbox
    # @param [BuildType] build_type @see Target#build_type
    # @param [Hash{String=>Symbol}] user_build_configurations @see Target#user_build_configurations
    # @param [Array<String>] archs @see Target#archs
    # @param [Platform] platform @see #Target#platform
    # @param [TargetDefinition] target_definition @see #target_definition
    # @param [Pathname] client_root @see #client_root
    # @param [Xcodeproj::Project] user_project @see #user_project
    # @param [Array<String>] user_target_uuids @see #user_target_uuids
    # @param [Hash{String=>Array<PodTarget>}] pod_targets_for_build_configuration @see #pod_targets_for_build_configuration
    #
    def initialize(sandbox, build_type, user_build_configurations, archs, platform, target_definition, client_root,
                   user_project, user_target_uuids, pod_targets_for_build_configuration)
      super(sandbox, build_type, user_build_configurations, archs, platform)
      raise "Can't initialize an AggregateTarget without a TargetDefinition!" if target_definition.nil?
      raise "Can't initialize an AggregateTarget with an abstract TargetDefinition!" if target_definition.abstract?
      @target_definition = target_definition
      @client_root = client_root
      @user_project = user_project
      @user_target_uuids = user_target_uuids
      @pod_targets_for_build_configuration = pod_targets_for_build_configuration
      @pod_targets = pod_targets_for_build_configuration.values.flatten.uniq
      @search_paths_aggregate_targets = []
      @xcconfigs = {}
    end

    # Merges this aggregate target with additional pod targets that are part of embedded aggregate targets.
    #
    # @param  [Hash{String=>Array<PodTarget>}] embedded_pod_targets_for_build_configuration
    #         The pod targets to merge with.
    #
    # @return [AggregateTarget] a new instance of this aggregate target with additional pod targets to be used from
    #         pod targets of embedded aggregate targets.
    #
    def merge_embedded_pod_targets(embedded_pod_targets_for_build_configuration)
      merged = @pod_targets_for_build_configuration.merge(embedded_pod_targets_for_build_configuration) do |_, before, after|
        (before + after).uniq
      end
      AggregateTarget.new(sandbox, build_type, user_build_configurations, archs, platform,
                          target_definition, client_root, user_project, user_target_uuids, merged).tap do |aggregate_target|
        aggregate_target.search_paths_aggregate_targets.concat(search_paths_aggregate_targets).freeze
        aggregate_target.mark_application_extension_api_only if application_extension_api_only
        aggregate_target.mark_build_library_for_distribution if build_library_for_distribution
      end
    end

    def build_settings(configuration_name = nil)
      if configuration_name
        @build_settings[configuration_name] ||
          raise(ArgumentError, "#{self} does not contain a build setting for the #{configuration_name.inspect} configuration, only #{@build_settings.keys.inspect}")
      else
        @build_settings.each_value.first ||
          raise(ArgumentError, "#{self} does not contain any build settings")
      end
    end

    # @return [Boolean] True if the user_target refers to a
    #         library (framework, static or dynamic lib).
    #
    def library?
      # Without a user_project, we can't say for sure
      # that this is a library
      return false if user_project.nil?
      symbol_types = user_targets.map(&:symbol_type).uniq
      unless symbol_types.count == 1
        raise ArgumentError, "Expected single kind of user_target for #{name}. Found #{symbol_types.join(', ')}."
      end
      [:framework, :dynamic_library, :static_library].include? symbol_types.first
    end

    # @return [Boolean] True if the user_target's pods are
    #         for an extension and must be embedded in a host,
    #         target, otherwise false.
    #
    def requires_host_target?
      # If we don't have a user_project, then we can't
      # glean any info about how this target is going to
      # be integrated, so return false since we can't know
      # for sure that this target refers to an extension
      # target that would require a host target
      return false if user_project.nil?
      symbol_types = user_targets.map(&:symbol_type).uniq
      unless symbol_types.count == 1
        raise ArgumentError, "Expected single kind of user_target for #{name}. Found #{symbol_types.join(', ')}."
      end
      EMBED_FRAMEWORKS_IN_HOST_TARGET_TYPES.include?(symbol_types[0])
    end

    # @return [String] the label for the target.
    #
    def label
      target_definition.label.to_s
    end

    # @return [Podfile] The podfile which declares the dependency
    #
    def podfile
      target_definition.podfile
    end

    # @return [Pathname] the path of the user project that this target will
    #         integrate as identified by the analyzer.
    #
    def user_project_path
      user_project.path if user_project
    end

    # List all user targets that will be integrated by this #target.
    #
    # @return [Array<PBXNativeTarget>]
    #
    def user_targets
      return [] unless user_project
      user_target_uuids.map do |uuid|
        native_target = user_project.objects_by_uuid[uuid]
        unless native_target
          raise Informative, '[Bug] Unable to find the target with ' \
            "the `#{uuid}` UUID for the `#{self}` integration library"
        end
        native_target
      end
    end

    # @param  [String] build_configuration The build configuration for which the
    #         the pod targets should be returned.
    #
    # @return [Array<PodTarget>] the pod targets for the given build
    #         configuration.
    #
    def pod_targets_for_build_configuration(build_configuration)
      @pod_targets_for_build_configuration[build_configuration] || []
    end

    # @return [Array<Specification>] The specifications used by this aggregate target.
    #
    def specs
      pod_targets.flat_map(&:specs)
    end

    # @return [Hash{Symbol => Array<Specification>}] The pod targets for each
    #         build configuration.
    #
    def specs_by_build_configuration
      result = {}
      user_build_configurations.each_key do |build_configuration|
        result[build_configuration] = pod_targets_for_build_configuration(build_configuration).
          flat_map(&:specs)
      end
      result
    end

    # @return [Array<Specification::Consumer>] The consumers of the Pod.
    #
    def spec_consumers
      specs.map { |spec| spec.consumer(platform) }
    end

    # @return [Boolean] Whether the target uses Swift code
    #
    def uses_swift?
      pod_targets.any?(&:uses_swift?)
    end

    # @return [Boolean] Whether the target contains any resources
    #
    def includes_resources?
      !resource_paths_by_config.each_value.all?(&:empty?)
    end

    # @return [Boolean] Whether the target contains any on demand resources
    #
    def includes_on_demand_resources?
      !on_demand_resources.empty?
    end

    # @return [Boolean] Whether the target contains frameworks to be embedded into
    #         the user target
    #
    def includes_frameworks?
      !framework_paths_by_config.each_value.all?(&:empty?)
    end

    # @return [Boolean] Whether the target contains xcframeworks to be embedded into
    #         the user target
    #
    def includes_xcframeworks?
      !xcframeworks_by_config.each_value.all?(&:empty?)
    end

    # @return [Hash{String => Array<FrameworkPaths>}] The vendored dynamic artifacts and framework target
    #         input and output paths grouped by config
    #
    def framework_paths_by_config
      @framework_paths_by_config ||= begin
        framework_paths_by_config = {}
        user_build_configurations.each_key do |config|
          relevant_pod_targets = pod_targets_for_build_configuration(config)
          framework_paths_by_config[config] = relevant_pod_targets.flat_map do |pod_target|
            library_specs = pod_target.library_specs.map(&:name)
            pod_target.framework_paths.values_at(*library_specs).flatten.compact.uniq
          end
        end
        framework_paths_by_config
      end
    end

    # @return [Hash{String => Array<Xcode::XCFramework>}] The vendored dynamic artifacts and framework target
    #         input and output paths grouped by config
    #
    def xcframeworks_by_config
      @xcframeworks_by_config ||= begin
        xcframeworks_by_config = {}
        user_build_configurations.each_key do |config|
          relevant_pod_targets = pod_targets_for_build_configuration(config)
          xcframeworks_by_config[config] = relevant_pod_targets.flat_map do |pod_target|
            library_specs = pod_target.library_specs.map(&:name)
            pod_target.xcframeworks.values_at(*library_specs).flatten.compact.uniq
          end
        end
        xcframeworks_by_config
      end
    end

    # @return [Array<Pathname>] Uniqued On Demand Resources for this target.
    #
    # @note On Demand Resources are not separated by config as they are integrated directly into the users target via
    # the resources build phase.
    #
    def on_demand_resources
      @on_demand_resources ||= begin
        pod_targets.flat_map do |pod_target|
          library_file_accessors = pod_target.file_accessors.select { |fa| fa.spec.library_specification? }
          library_file_accessors.flat_map(&:on_demand_resources_files)
        end.uniq
      end
    end

    # @return [Hash{String => Array<String>}] Uniqued Resources grouped by config
    #
    def resource_paths_by_config
      @resource_paths_by_config ||= begin
        relevant_pod_targets = pod_targets.reject do |pod_target|
          pod_target.should_build? && pod_target.build_as_dynamic_framework?
        end
        user_build_configurations.each_key.each_with_object({}) do |config, resources_by_config|
          targets = relevant_pod_targets & pod_targets_for_build_configuration(config)
          resources_by_config[config] = targets.flat_map do |pod_target|
            library_specs = pod_target.library_specs.map(&:name)
            resource_paths = pod_target.resource_paths.values_at(*library_specs).flatten

            if pod_target.build_as_static_framework?
              built_product_dir = Pathname.new(pod_target.build_product_path('${BUILT_PRODUCTS_DIR}'))
              resource_paths = resource_paths.map do |resource_path|
                extname = File.extname(resource_path)
                if self.class.resource_extension_compilable?(extname)
                  output_extname = self.class.output_extension_for_resource(extname)
                  output_path_components = Pathname(resource_path).each_filename.select { |component| File.extname(component) == '.lproj' }
                  output_path_components << File.basename(resource_path)
                  built_product_dir.join(*output_path_components).sub_ext(output_extname).to_s
                else
                  resource_path
                end
              end
            end

            resource_paths << bridge_support_file
            resource_paths.compact.uniq
          end
        end
      end
    end

    # @return [Pathname] the path of the bridge support file relative to the
    #         sandbox or `nil` if bridge support is disabled.
    #
    def bridge_support_file
      bridge_support_path.relative_path_from(sandbox.root) if podfile.generate_bridge_support?
    end

    #-------------------------------------------------------------------------#

    # @!group Support files

    # @return [Pathname] The absolute path of acknowledgements file.
    #
    # @note   The acknowledgements generators add the extension according to
    #         the file type.
    #
    def acknowledgements_basepath
      support_files_dir + "#{label}-acknowledgements"
    end

    # @return [Pathname] The absolute path of the copy resources script.
    #
    def copy_resources_script_path
      support_files_dir + "#{label}-resources.sh"
    end

    # @return [Pathname] The absolute path of the embed frameworks script.
    #
    def embed_frameworks_script_path
      support_files_dir + "#{label}-frameworks.sh"
    end

    # @param  [String] configuration the configuration this path is for.
    #
    # @return [Pathname] The absolute path of the copy resources script input file list.
    #
    def copy_resources_script_input_files_path(configuration)
      support_files_dir + "#{label}-resources-#{configuration}-input-files.xcfilelist"
    end

    # @param  [String] configuration the configuration this path is for.
    #
    # @return [Pathname] The absolute path of the copy resources script output file list.
    #
    def copy_resources_script_output_files_path(configuration)
      support_files_dir + "#{label}-resources-#{configuration}-output-files.xcfilelist"
    end

    # @param  [String] configuration the configuration this path is for.
    #
    # @return [Pathname] The absolute path of the embed frameworks script input file list.
    #
    def embed_frameworks_script_input_files_path(configuration)
      support_files_dir + "#{label}-frameworks-#{configuration}-input-files.xcfilelist"
    end

    # @param  [String] configuration the configuration this path is for.
    #
    # @return [Pathname] The absolute path of the embed frameworks script output file list.
    #
    def embed_frameworks_script_output_files_path(configuration)
      support_files_dir + "#{label}-frameworks-#{configuration}-output-files.xcfilelist"
    end

    # @param  [String] configuration the configuration this path is for.
    #
    # @return [Pathname] The absolute path of the prepare artifacts script input file list.
    #
    # @deprecated
    #
    # @todo Remove in 2.0
    #
    def prepare_artifacts_script_input_files_path(configuration)
      support_files_dir + "#{label}-artifacts-#{configuration}-input-files.xcfilelist"
    end

    # @param  [String] configuration the configuration this path is for.
    #
    # @return [Pathname] The absolute path of the prepare artifacts script output file list.
    #
    # @deprecated
    #
    # @todo Remove in 2.0
    #
    def prepare_artifacts_script_output_files_path(configuration)
      support_files_dir + "#{label}-artifacts-#{configuration}-output-files.xcfilelist"
    end

    # @return [String] The output file path fo the check manifest lock script.
    #
    def check_manifest_lock_script_output_file_path
      "$(DERIVED_FILE_DIR)/#{label}-checkManifestLockResult.txt"
    end

    # @return [Pathname] The relative path of the Pods directory from user project's directory.
    #
    def relative_pods_root_path
      sandbox.root.relative_path_from(client_root)
    end

    # @return [String] The xcconfig path of the root from the `$(SRCROOT)`
    #         variable of the user's project.
    #
    def relative_pods_root
      "${SRCROOT}/#{relative_pods_root_path}"
    end

    # @return [String] The path of the Podfile directory relative to the
    #         root of the user project.
    #
    def podfile_dir_relative_path
      podfile_path = target_definition.podfile.defined_in_file
      return "${SRCROOT}/#{podfile_path.relative_path_from(client_root).dirname}" unless podfile_path.nil?
      # Fallback to the standard path if the Podfile is not represented by a file.
      '${PODS_ROOT}/..'
    end

    # @param  [String] config_name The build configuration name to get the xcconfig for
    # @return [String] The path of the xcconfig file relative to the root of
    #         the user project.
    #
    def xcconfig_relative_path(config_name)
      xcconfig_path(config_name).relative_path_from(client_root).to_s
    end

    # @return [String] The path of the copy resources script relative to the
    #         root of the Pods project.
    #
    def copy_resources_script_relative_path
      "${PODS_ROOT}/#{relative_to_pods_root(copy_resources_script_path)}"
    end

    # @return [String] The path of the copy resources script input file list
    #         relative to the root of the Pods project.
    #
    def copy_resources_script_input_files_relative_path
      "${PODS_ROOT}/#{relative_to_pods_root(copy_resources_script_input_files_path('${CONFIGURATION}'))}"
    end

    # @return [String] The path of the copy resources script output file list
    #         relative to the root of the Pods project.
    #
    def copy_resources_script_output_files_relative_path
      "${PODS_ROOT}/#{relative_to_pods_root(copy_resources_script_output_files_path('${CONFIGURATION}'))}"
    end

    # @return [String] The path of the embed frameworks relative to the
    #         root of the Pods project.
    #
    def embed_frameworks_script_relative_path
      "${PODS_ROOT}/#{relative_to_pods_root(embed_frameworks_script_path)}"
    end

    # @return [String] The path of the embed frameworks script input file list
    #         relative to the root of the Pods project.
    #
    def embed_frameworks_script_input_files_relative_path
      "${PODS_ROOT}/#{relative_to_pods_root(embed_frameworks_script_input_files_path('${CONFIGURATION}'))}"
    end

    # @return [String] The path of the embed frameworks script output file list
    #         relative to the root of the Pods project.
    #
    def embed_frameworks_script_output_files_relative_path
      "${PODS_ROOT}/#{relative_to_pods_root(embed_frameworks_script_output_files_path('${CONFIGURATION}'))}"
    end

    # @return [String] The path of the prepare artifacts script relative to the
    #         root of the Pods project.
    #
    # @deprecated
    #
    # @todo Remove in 2.0
    #
    def prepare_artifacts_script_relative_path
      "${PODS_ROOT}/#{relative_to_pods_root(prepare_artifacts_script_path)}"
    end

    # @return [String] The path of the prepare artifacts script input file list
    #         relative to the root of the Pods project.
    #
    # @deprecated
    #
    # @todo Remove in 2.0
    #
    def prepare_artifacts_script_input_files_relative_path
      "${PODS_ROOT}/#{relative_to_pods_root(prepare_artifacts_script_input_files_path('${CONFIGURATION}'))}"
    end

    # @return [String] The path of the prepare artifacts script output file list
    #         relative to the root of the Pods project.
    #
    # @deprecated
    #
    # @todo Remove in 2.0
    #
    def prepare_artifacts_script_output_files_relative_path
      "${PODS_ROOT}/#{relative_to_pods_root(prepare_artifacts_script_output_files_path('${CONFIGURATION}'))}"
    end

    private

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

    # Computes the relative path of a sandboxed file from the `$(PODS_ROOT)`
    # variable of the Pods's project.
    #
    # @param  [Pathname] path
    #         A relative path from the root of the sandbox.
    #
    # @return [String] The computed path.
    #
    def relative_to_pods_root(path)
      path.relative_path_from(sandbox.root).to_s
    end

    def create_build_settings
      settings = {}

      user_build_configurations.each do |configuration_name, configuration|
        settings[configuration_name] = BuildSettings::AggregateTargetSettings.new(self, configuration_name, :configuration => configuration)
      end

      settings
    end
  end
end