CocoaPods/Xcodeproj

View on GitHub
lib/xcodeproj/project/object/native_target.rb

Summary

Maintainability
D
2 days
Test Coverage
A
96%
module Xcodeproj
  class Project
    module Object
      class AbstractTarget < AbstractObject
        # @!group Attributes

        # @return [String] The name of the Target.
        #
        attribute :name, String

        # @return [String] the name of the build product.
        #
        attribute :product_name, String

        # @return [String] Comments associated with this target.
        #
        #   This is apparently no longer used by Xcode.
        #
        attribute :comments, String

        # @return [XCConfigurationList] the list of the build configurations of
        #         the target. This list commonly include two configurations
        #         `Debug` and `Release`.
        #
        has_one :build_configuration_list, XCConfigurationList

        # @return [ObjectList<PBXTargetDependency>] the targets necessary to
        #         build this target.
        #
        has_many :dependencies, PBXTargetDependency

        public

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

        # Gets the value for the given build setting in all the build
        # configurations or the value inheriting the value from the project
        # ones if needed.
        #
        # @param [String] key
        #        the key of the build setting.
        #
        # @param [Bool] resolve_against_xcconfig
        #        whether the resolved setting should take in consideration any
        #        configuration file present.
        #
        # @return [Hash{String => String}] The value of the build setting
        #         grouped by the name of the build configuration.
        #
        # TODO:   Full support for this would require to take into account
        #         the default values for the platform.
        #
        def resolved_build_setting(key, resolve_against_xcconfig = false)
          target_settings = build_configuration_list.get_setting(key, resolve_against_xcconfig, self)
          project_settings = project.build_configuration_list.get_setting(key, resolve_against_xcconfig)
          target_settings.merge(project_settings) do |_key, target_val, proj_val|
            target_includes_inherited = Constants::INHERITED_KEYWORDS.any? { |keyword| target_val.include?(keyword) } if target_val
            if target_includes_inherited && proj_val
              if target_val.is_a? String
                target_val.gsub(Regexp.union(Constants::INHERITED_KEYWORDS), proj_val)
              else
                target_val.flat_map { |value| Constants::INHERITED_KEYWORDS.include?(value) ? proj_val : value }
              end
            else
              target_val || proj_val
            end
          end
        end

        # Gets the value for the given build setting, properly inherited if
        # need, if shared across the build configurations.
        #
        # @param [String] key
        #        the key of the build setting.
        #
        # @param [Boolean] resolve_against_xcconfig
        #        whether the resolved setting should take in consideration any
        #        configuration file present.
        #
        # @raise  If the build setting has multiple values.
        #
        # @note   As it is common not to have a setting with no value for
        #         custom build configurations nil keys are not considered to
        #         determine if the setting is unique. This is an heuristic
        #         which might not closely match Xcode behaviour.
        #
        # @return [String] The value of the build setting.
        #
        def common_resolved_build_setting(key, resolve_against_xcconfig: false)
          values = resolved_build_setting(key, resolve_against_xcconfig).values.compact.uniq
          if values.count <= 1
            values.first
          else
            raise "[Xcodeproj] Consistency issue: build setting `#{key}` has multiple values: `#{resolved_build_setting(key)}`"
          end
        end

        # @return [String] the SDK that the target should use.
        #
        def sdk
          common_resolved_build_setting('SDKROOT')
        end

        # @return [Symbol] the name of the platform of the target.
        #
        def platform_name
          return unless sdk
          if sdk.include? 'iphoneos'
            :ios
          elsif sdk.include? 'macosx'
            :osx
          elsif sdk.include? 'appletvos'
            :tvos
          elsif sdk.include? 'xros'
            :visionos
          elsif sdk.include? 'watchos'
            :watchos
          end
        end

        # @return [String] the version of the SDK.
        #
        def sdk_version
          return unless sdk
          sdk.scan(/[0-9.]+/).first
        end

        # @visibility private
        #
        # @return [Hash<Symbol, String>]
        #         The name of the setting for the deployment target by platform
        #         name.
        #
        DEPLOYMENT_TARGET_SETTING_BY_PLATFORM_NAME = {
          :ios => 'IPHONEOS_DEPLOYMENT_TARGET',
          :osx => 'MACOSX_DEPLOYMENT_TARGET',
          :tvos => 'TVOS_DEPLOYMENT_TARGET',
          :visionos => 'XROS_DEPLOYMENT_TARGET',
          :watchos => 'WATCHOS_DEPLOYMENT_TARGET',
        }.freeze

        # @return [String] the deployment target of the target according to its
        #         platform.
        #
        def deployment_target
          return unless setting = DEPLOYMENT_TARGET_SETTING_BY_PLATFORM_NAME[platform_name]
          common_resolved_build_setting(setting)
        end

        # @param  [String] deployment_target the deployment target to set for
        #         the target according to its platform.
        #
        def deployment_target=(deployment_target)
          return unless setting = DEPLOYMENT_TARGET_SETTING_BY_PLATFORM_NAME[platform_name]
          build_configurations.each do |config|
            config.build_settings[setting] = deployment_target
          end
        end

        # @return [ObjectList<XCBuildConfiguration>] the build
        #         configurations of the target.
        #
        def build_configurations
          build_configuration_list.build_configurations
        end

        # Adds a new build configuration to the target and populates its with
        # default settings according to the provided type if one doesn't
        # exists.
        #
        # @note   If a build configuration with the given name is already
        #         present no new build configuration is added.
        #
        # @param  [String] name
        #         The name of the build configuration.
        #
        # @param  [Symbol] type
        #         The type of the build configuration used to populate the build
        #         settings, must be :debug or :release.
        #
        # @return [XCBuildConfiguration] the created build configuration or the
        #         existing one with the same name.
        #
        def add_build_configuration(name, type)
          if existing = build_configuration_list[name]
            existing
          else
            build_configuration = project.new(XCBuildConfiguration)
            build_configuration.name = name
            product_type = self.product_type if respond_to?(:product_type)
            build_configuration.build_settings = ProjectHelper.common_build_settings(type, platform_name, deployment_target, product_type)
            build_configuration_list.build_configurations << build_configuration
            build_configuration
          end
        end

        # @param  [String] build_configuration_name
        #         the name of a build configuration.
        #
        # @return [Hash] the build settings of the build configuration with the
        #         given name.
        #
        #
        def build_settings(build_configuration_name)
          build_configuration_list.build_settings(build_configuration_name)
        end

        # @!group Build Phases Helpers

        # @return [PBXFrameworksBuildPhase]
        #         the frameworks build phases of the target.
        #
        def frameworks_build_phases
          build_phases.find { |bp| bp.class == PBXFrameworksBuildPhase }
        end

        # @return [Array<PBXCopyFilesBuildPhase>]
        #         the copy files build phases of the target.
        #
        def copy_files_build_phases
          build_phases.grep(PBXCopyFilesBuildPhase)
        end

        # @return [Array<PBXShellScriptBuildPhase>]
        #         the shell script build phases of the target.
        #
        def shell_script_build_phases
          build_phases.grep(PBXShellScriptBuildPhase)
        end

        # Adds a dependency on the given target.
        #
        # @param  [AbstractTarget] target
        #         the target which should be added to the dependencies list of
        #         the receiver. The target may be a target of this target's
        #         project or of a subproject of this project. Note that the
        #         subproject must already be added to this target's project.
        #
        # @return [void]
        #
        def add_dependency(target)
          unless dependency_for_target(target)
            container_proxy = project.new(Xcodeproj::Project::PBXContainerItemProxy)
            if target.project == project
              container_proxy.container_portal = project.root_object.uuid
            else
              subproject_reference = project.reference_for_path(target.project.path)
              raise ArgumentError, 'add_dependency received target that belongs to a project that is not this project and is not a subproject of this project' unless subproject_reference
              container_proxy.container_portal = subproject_reference.uuid
            end
            container_proxy.proxy_type = Constants::PROXY_TYPES[:native_target]
            container_proxy.remote_global_id_string = target.uuid
            container_proxy.remote_info = target.name

            dependency = project.new(Xcodeproj::Project::PBXTargetDependency)
            dependency.name = target.name
            dependency.target = target if target.project == project
            dependency.target_proxy = container_proxy

            dependencies << dependency
          end
        end

        # Checks whether this target has a dependency on the given target.
        #
        # @param  [AbstractTarget] target
        #         the target to search for.
        #
        # @return [PBXTargetDependency]
        #
        def dependency_for_target(target)
          dependencies.find do |dep|
            if dep.target_proxy.remote?
              subproject_reference = project.reference_for_path(target.project.path)
              uuid = subproject_reference.uuid if subproject_reference
              dep.target_proxy.remote_global_id_string == target.uuid && dep.target_proxy.container_portal == uuid
            else
              dep.target.uuid == target.uuid
            end
          end
        end

        # Creates a new copy files build phase.
        #
        # @param  [String] name
        #         an optional name for the phase.
        #
        # @return [PBXCopyFilesBuildPhase] the new phase.
        #
        def new_copy_files_build_phase(name = nil)
          phase = project.new(PBXCopyFilesBuildPhase)
          phase.name = name
          build_phases << phase
          phase
        end

        # Creates a new shell script build phase.
        #
        # @param  (see #new_copy_files_build_phase)
        #
        # @return [PBXShellScriptBuildPhase] the new phase.
        #
        def new_shell_script_build_phase(name = nil)
          phase = project.new(PBXShellScriptBuildPhase)
          phase.name = name
          build_phases << phase
          phase
        end

        public

        # @!group System frameworks
        #--------------------------------------#

        # Adds a file reference for one or more system framework to the project
        # if needed and adds them to the Frameworks build phases.
        #
        # @param  [Array<String>, String] names
        #         The name or the list of the names of the framework.
        #
        # @note   Xcode behaviour is following: if the target has the same SDK
        #         of the project it adds the reference relative to the SDK root
        #         otherwise the reference is added relative to the Developer
        #         directory. This can create confusion or duplication of the
        #         references of frameworks linked by iOS and OS X targets. For
        #         this reason the new Xcodeproj behaviour is to add the
        #         frameworks in a subgroup according to the platform.
        #
        # @return [Array<PBXFileReference>] An array of the newly created file
        #         references.
        #
        def add_system_framework(names)
          Array(names).map do |name|
            case platform_name
            when :ios
              group = project.frameworks_group['iOS'] || project.frameworks_group.new_group('iOS')
              path_sdk_name = 'iPhoneOS'
              path_sdk_version = sdk_version || Constants::LAST_KNOWN_IOS_SDK
            when :osx
              group = project.frameworks_group['OS X'] || project.frameworks_group.new_group('OS X')
              path_sdk_name = 'MacOSX'
              path_sdk_version = sdk_version || Constants::LAST_KNOWN_OSX_SDK
            when :tvos
              group = project.frameworks_group['tvOS'] || project.frameworks_group.new_group('tvOS')
              path_sdk_name = 'AppleTVOS'
              path_sdk_version = sdk_version || Constants::LAST_KNOWN_TVOS_SDK
            when :visionos
              group = project.frameworks_group['visionOS'] || project.frameworks_group.new_group('visionOS')
              path_sdk_name = 'XROS'
              path_sdk_version = sdk_version || Constants::LAST_KNOWN_VISIONOS_SDK
            when :watchos
              group = project.frameworks_group['watchOS'] || project.frameworks_group.new_group('watchOS')
              path_sdk_name = 'WatchOS'
              path_sdk_version = sdk_version || Constants::LAST_KNOWN_WATCHOS_SDK
            else
              raise 'Unknown platform for target'
            end

            path = "Platforms/#{path_sdk_name}.platform/Developer/SDKs/#{path_sdk_name}#{path_sdk_version}.sdk/System/Library/Frameworks/#{name}.framework"
            unless ref = group.find_file_by_path(path)
              ref = group.new_file(path, :developer_dir)
            end
            frameworks_build_phase.add_file_reference(ref, true)
            ref
          end
        end
        alias_method :add_system_frameworks, :add_system_framework

        # Adds a file reference for one or more system dylib libraries to the project
        # if needed and adds them to the Frameworks build phases.
        #
        # @param  [Array<String>, String] names
        #         The name or the list of the names of the libraries.
        #
        # @return [void]
        #
        def add_system_library(names)
          add_system_library_extension(names, 'dylib')
        end
        alias_method :add_system_libraries, :add_system_library

        def add_system_library_extension(names, extension)
          Array(names).each do |name|
            path = "usr/lib/lib#{name}.#{extension}"
            files = project.frameworks_group.files
            unless reference = files.find { |ref| ref.path == path }
              reference = project.frameworks_group.new_file(path, :sdk_root)
            end
            frameworks_build_phase.add_file_reference(reference, true)
            reference
          end
        end
        private :add_system_library_extension

        # Adds a file reference for one or more system tbd libraries to the project
        # if needed and adds them to the Frameworks build phases.
        #
        # @param  [Array<String>, String] names
        #         The name or the list of the names of the libraries.
        #
        # @return [void]
        #
        def add_system_library_tbd(names)
          add_system_library_extension(names, 'tbd')
        end
        alias_method :add_system_libraries_tbd, :add_system_library_tbd

        public

        # @!group AbstractObject Hooks
        #--------------------------------------#

        # @return [Hash{String => Hash}] A hash suitable to display the object
        #         to the user.
        #
        def pretty_print
          {
            display_name => {
              'Build Phases' => build_phases.map(&:pretty_print),
              'Build Configurations' => build_configurations.map(&:pretty_print),
            },
          }
        end
      end

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

      # Represents a target handled by Xcode.
      #
      class PBXNativeTarget < AbstractTarget
        # @!group Attributes

        # @return [PBXBuildRule] the build rules of this target.
        #
        has_many :build_rules, PBXBuildRule

        # @return [String] the build product type identifier.
        #
        attribute :product_type, String

        # @return [PBXFileReference] the reference to the product file.
        #
        has_one :product_reference, PBXFileReference

        # @return [ObjectList<XCSwiftPackageProductDependency>] the Swift package products necessary to
        #         build this target.
        #
        has_many :package_product_dependencies, XCSwiftPackageProductDependency

        # @return [String] the install path of the product.
        #
        attribute :product_install_path, String

        # @return [ObjectList<AbstractBuildPhase>] the build phases of the
        #         target.
        #
        # @note   Apparently only PBXCopyFilesBuildPhase and
        #         PBXShellScriptBuildPhase can appear multiple times in a
        #         target.
        #
        has_many :build_phases, AbstractBuildPhase

        # @return [ObjectList<PBXFileSystemSynchronizedRootGroup>] the file system synchronized
        #         groups containing files to include to build this target.
        #
        has_many :file_system_synchronized_groups, PBXFileSystemSynchronizedRootGroup

        public

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

        # @return [Symbol] The type of the target expressed as a symbol.
        #
        def symbol_type
          Constants::PRODUCT_TYPE_UTI.key(product_type)
        end

        # @return [Boolean] Whether the target is a test target.
        #
        def test_target_type?
          case symbol_type
          when :octest_bundle, :unit_test_bundle, :ui_test_bundle
            true
          else
            false
          end
        end

        # @return [Boolean] Whether the target is an extension.
        #
        def extension_target_type?
          case symbol_type
          when :app_extension, :watch_extension, :watch2_extension, :tv_extension, :messages_extension
            true
          else
            false
          end
        end

        # @return [Boolean] Whether the target is launchable.
        #
        def launchable_target_type?
          case symbol_type
          when :application, :command_line_tool
            true
          else
            false
          end
        end

        # Adds source files to the target.
        #
        # @param  [Array<PBXFileReference>] file_references
        #         the files references of the source files that should be added
        #         to the target.
        #
        # @param  [String] compiler_flags
        #         the compiler flags for the source files.
        #
        # @yield_param [PBXBuildFile] each created build file.
        #
        # @return [Array<PBXBuildFile>] the created build files.
        #
        def add_file_references(file_references, compiler_flags = {})
          file_references.map do |file|
            extension = File.extname(file.path).downcase
            header_extensions = Constants::HEADER_FILES_EXTENSIONS
            is_header_phase = header_extensions.include?(extension)
            phase = is_header_phase ? headers_build_phase : source_build_phase

            unless build_file = phase.build_file(file)
              build_file = project.new(PBXBuildFile)
              build_file.file_ref = file
              phase.files << build_file
            end

            if compiler_flags && !compiler_flags.empty? && !is_header_phase
              (build_file.settings ||= {}).merge!('COMPILER_FLAGS' => compiler_flags) do |_, old, new|
                [old, new].compact.join(' ')
              end
            end

            yield build_file if block_given?

            build_file
          end
        end

        # Adds resource files to the resources build phase of the target.
        #
        # @param  [Array<PBXFileReference>] resource_file_references
        #         the files references of the resources to the target.
        #
        # @return [void]
        #
        def add_resources(resource_file_references)
          resource_file_references.each do |file|
            next if resources_build_phase.include?(file)
            build_file = project.new(PBXBuildFile)
            build_file.file_ref = file
            resources_build_phase.files << build_file
          end
        end

        # Adds on demand resources to the resources build phase of the target.
        #
        # @param  {String => [Array<PBXFileReference>]} on_demand_resource_tag_files
        #         the files references of the on demand resources to add to the target keyed by the tag.
        #
        # @return [void]
        #
        def add_on_demand_resources(on_demand_resource_tag_files)
          on_demand_resource_tag_files.each do |tag, file_refs|
            file_refs.each do |file_ref|
              if resources_build_phase.include?(file_ref)
                existing_build_file = resources_build_phase.build_file(file_ref)
                existing_build_file.settings ||= {}
                existing_build_file.settings['ASSET_TAGS'] ||= []
                existing_build_file.settings['ASSET_TAGS'] << tag
                existing_build_file.settings['ASSET_TAGS'].uniq!
                next
              end
              build_file = resources_build_phase.add_file_reference(file_ref, true)
              build_file.settings = (build_file.settings ||= {}).merge('ASSET_TAGS' => [tag])
            end
          end
        end

        # Remove on demand resources from the resources build phase of the target.
        #
        # @param  {String => [Array<PBXFileReference>]} on_demand_resource_tag_files
        #         the files references of the on demand resources to add to the target keyed by the tag.
        #
        # @return [void]
        #
        def remove_on_demand_resources(on_demand_resource_tag_files)
          on_demand_resource_tag_files.each do |tag, file_refs|
            file_refs.each do |file_ref|
              build_file = resources_build_phase.build_file(file_ref)
              next if build_file.nil?
              asset_tags = build_file.settings['ASSET_TAGS']
              asset_tags.delete(tag)
              resources_build_phase.remove_file_reference(file_ref) if asset_tags.empty?
            end
          end
        end

        # Finds or creates the headers build phase of the target.
        #
        # @note   A target should have only one headers build phase.
        #
        # @return [PBXHeadersBuildPhase] the headers build phase.
        #
        def headers_build_phase
          find_or_create_build_phase_by_class(PBXHeadersBuildPhase)
        end

        # Finds or creates the source build phase of the target.
        #
        # @note   A target should have only one source build phase.
        #
        # @return [PBXSourcesBuildPhase] the source build phase.
        #
        def source_build_phase
          find_or_create_build_phase_by_class(PBXSourcesBuildPhase)
        end

        # Finds or creates the frameworks build phase of the target.
        #
        # @note   A target should have only one frameworks build phase.
        #
        # @return [PBXFrameworksBuildPhase] the frameworks build phase.
        #
        def frameworks_build_phase
          find_or_create_build_phase_by_class(PBXFrameworksBuildPhase)
        end

        # Finds or creates the resources build phase of the target.
        #
        # @note   A target should have only one resources build phase.
        #
        # @return [PBXResourcesBuildPhase] the resources build phase.
        #
        def resources_build_phase
          find_or_create_build_phase_by_class(PBXResourcesBuildPhase)
        end

        private

        # @!group Internal Helpers
        #--------------------------------------#

        # Find or create a build phase by a given class
        #
        # @param [Class] phase_class the class of the build phase to find or create.
        #
        # @return [AbstractBuildPhase] the build phase whose class match the given phase_class.
        #
        def find_or_create_build_phase_by_class(phase_class)
          @phases ||= {}
          unless phase_class < AbstractBuildPhase
            raise ArgumentError, "#{phase_class} must be a subclass of #{AbstractBuildPhase.class}"
          end
          @phases[phase_class] ||= build_phases.find { |bp| bp.class == phase_class } ||
            project.new(phase_class).tap { |bp| build_phases << bp }
        end

        public

        # @!group AbstractObject Hooks
        #--------------------------------------#

        # Sorts the to many attributes of the object according to the display
        # name.
        #
        # Build phases are not sorted as they order is relevant.
        #
        def sort(_options = nil)
          attributes_to_sort = to_many_attributes.reject { |attr| attr.name == :build_phases }
          attributes_to_sort.each do |attrb|
            list = attrb.get_value(self)
            list.sort! do |x, y|
              x.display_name <=> y.display_name
            end
          end
        end

        def to_hash_as(method = :to_hash)
          hash_as = super
          excluded_keys_for_serialization_when_empty.each do |key|
            if !hash_as[key].nil? && hash_as[key].empty?
              hash_as.delete(key)
            end
          end
          hash_as
        end

        def to_ascii_plist
          plist = super
          excluded_keys_for_serialization_when_empty.each do |key|
            if !plist.value[key].nil? && plist.value[key].empty?
              plist.value.delete(key)
            end
          end
          plist
        end

        # @return [Array<String>] array of keys to exclude from serialization when the value is empty
        def excluded_keys_for_serialization_when_empty
          %w(packageProductDependencies fileSystemSynchronizedGroups)
        end
      end

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

      # Represents a target that only consists in a aggregate of targets.
      #
      # @todo Apparently it can't have build rules.
      #
      class PBXAggregateTarget < AbstractTarget
        # @!group Attributes

        # @return [PBXBuildRule] the build phases of the target.
        #
        # @note   Apparently only PBXCopyFilesBuildPhase and
        #         PBXShellScriptBuildPhase can appear multiple times in a
        #         target.
        #
        has_many :build_phases, [PBXCopyFilesBuildPhase, PBXShellScriptBuildPhase]
      end

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

      # Represents a legacy target which uses an external build tool.
      #
      # Apparently it can't have any build phase but the attribute can be
      # present.
      #
      class PBXLegacyTarget < AbstractTarget
        # @!group Attributes

        # @return [String] e.g "Dir"
        #
        attribute :build_working_directory, String

        # @return [String] e.g "$(ACTION)"
        #
        attribute :build_arguments_string, String

        # @return [String] e.g "1"
        #
        attribute :pass_build_settings_in_environment, String

        # @return [String] e.g "/usr/bin/make"
        #
        attribute :build_tool_path, String

        # @return [PBXBuildRule] the build phases of the target.
        #
        # @note   Apparently only PBXCopyFilesBuildPhase and
        #         PBXShellScriptBuildPhase can appear multiple times in a
        #         target.
        #
        has_many :build_phases, AbstractBuildPhase
      end

      #-----------------------------------------------------------------------#
    end
  end
end