CocoaPods/CocoaPods

View on GitHub
lib/cocoapods/project.rb

Summary

Maintainability
B
6 hrs
Test Coverage
A
97%
require 'xcodeproj'
require 'active_support/core_ext/string/inflections'

module Pod
  # The Pods project.
  #
  # Model class which provides helpers for working with the Pods project
  # through the installation process.
  #
  class Project < Xcodeproj::Project
    # @return [PBXGroup] The group for the support files of the aggregate
    #         targets.
    #
    attr_reader :support_files_group

    # @return [PBXGroup] The group for the Pods.
    #
    attr_reader :pods

    # @return [PBXGroup] The group for Development Pods.
    #
    attr_reader :development_pods

    # @return [PBXGroup] The group for dependencies.
    # Used by #generate_multiple_pod_projects installation option.
    #
    attr_reader :dependencies_group

    # @return [Boolean] Bool indicating if this project is a pod target subproject.
    # Used by `generate_multiple_pod_projects` installation option.
    #
    attr_reader :pod_target_subproject
    alias pod_target_subproject? pod_target_subproject

    # @return [String] The basename of the project path without .xcodeproj extension.
    #
    attr_reader :project_name

    # Initialize a new instance
    #
    # @param  [Pathname, String] path @see Xcodeproj::Project#path
    # @param  [Boolean] skip_initialization Whether the project should be initialized from scratch.
    # @param  [Int] object_version Object version to use for serialization, defaults to Xcode 3.2 compatible.
    #
    def initialize(path, skip_initialization = false,
                    object_version = Xcodeproj::Constants::DEFAULT_OBJECT_VERSION, pod_target_subproject: false)
      @uuid_prefix = Digest('SHA256').hexdigest(File.basename(path)).upcase
      super(path, skip_initialization, object_version)
      @support_files_group = new_group('Targets Support Files')
      @refs_by_absolute_path = {}
      @variant_groups_by_path_and_name = {}
      @pods = new_group('Pods')
      @development_pods = new_group('Development Pods')
      @dependencies_group = new_group('Dependencies')
      @pod_target_subproject = pod_target_subproject
      @project_name = Pathname(path).basename('.*').to_s
      self.symroot = LEGACY_BUILD_ROOT
    end

    # Generates a list of new UUIDs that created objects can be assigned.
    #
    # @note Overridden to generate UUIDs in a much faster way, since we don't need to check for collisions
    #       (as the Pods project is regenerated each time, and thus all UUIDs will have come from this method)
    #
    # @param [Integer] count
    #        The number of UUIDs to generate
    #
    # @return [Void]
    #
    def generate_available_uuid_list(count = 100)
      start = @generated_uuids.size
      uniques = Array.new(count) { |i| format('%.6s%07X0', @uuid_prefix, start + i) }
      @generated_uuids += uniques
      @available_uuids += uniques
    end

    public

    # @!group Legacy Xcode build root
    #-------------------------------------------------------------------------#

    LEGACY_BUILD_ROOT = '${SRCROOT}/../build'

    # @param [String] symroot
    #        The build root that is used when Xcode is configured to not use the
    #        workspace’s build root. Defaults to `${SRCROOT}/../build`.
    #
    # @return [void]
    #
    def symroot=(symroot)
      root_object.build_configuration_list.build_configurations.each do |config|
        config.build_settings['SYMROOT'] = symroot
      end
    end

    public

    # @!group Pod Groups
    #-------------------------------------------------------------------------#

    # Creates a new group for the Pod with the given name and configures its
    # path.
    #
    # @param  [String] pod_name
    #         The name of the Pod.
    #
    # @param  [#to_s] path
    #         The path to the root of the Pod.
    #
    # @param  [Boolean] development
    #         Whether the group should be added to the Development Pods group.
    #
    # @param  [Boolean] absolute
    #         Whether the path of the group should be set as absolute.
    #
    # @return [PBXGroup] The new group.
    #
    def add_pod_group(pod_name, path, development = false, absolute = false)
      raise '[BUG]' if pod_group(pod_name)

      parent_group =
        if pod_target_subproject
          main_group
        else
          development ? development_pods : pods
        end
      source_tree = absolute ? :absolute : :group

      group = parent_group.new_group(pod_name, path, source_tree)
      group
    end

    # Creates a new subproject reference for the given project and configures its
    # group location.
    #
    # @param [Project] project
    #        The subproject to be added.
    #
    # @param [Boolean] development
    #        Whether the project should be added to the Development Pods group.
    #        For projects where `pod_target_subproject` is enabled, all subprojects are added into the Dependencies group.
    #
    # @return [PBXFileReference] The new file reference.
    #
    def add_pod_subproject(project, development = false)
      parent_group = group_for_subproject_reference(development)
      add_subproject_reference(project, parent_group)
    end

    # Creates a new subproject reference for the given cached metadata and configures its
    # group location.
    #
    # @param [Sandbox] sandbox
    #        The sandbox used for installation.
    #
    # @param [TargetMetadata] metadata
    #        The project metadata to be added.
    #
    # @param [Boolean] development
    #        Whether the project should be added to the Development Pods group.
    #        For projects where `pod_target_subproject` is enabled, all subprojects are added into the Dependencies group.
    #
    # @return [PBXFileReference] The new file reference.
    #
    def add_cached_pod_subproject(sandbox, metadata, development = false)
      parent_group = group_for_subproject_reference(development)
      add_cached_subproject_reference(sandbox, metadata, parent_group)
    end

    # @return [Array<PBXGroup>] Returns all the group of the Pods.
    #
    def pod_groups
      if pod_target_subproject
        main_group.children.objects
      else
        pods.children.objects + development_pods.children.objects
      end
    end

    # Returns the group for the Pod with the given name.
    #
    # @param  [String] pod_name
    #         The name of the Pod.
    #
    # @return [PBXGroup] The group.
    #
    def pod_group(pod_name)
      pod_groups.find { |group| group.name == pod_name }
    end

    # @return [Hash] The names of the specification subgroups by key.
    #
    SPEC_SUBGROUPS = {
      :resources  => 'Resources',
      :frameworks => 'Frameworks',
      :developer  => 'Pod',
    }

    # Returns the group for the specification with the give name creating it if
    # needed.
    #
    # @param [String] spec_name
    #                 The full name of the specification.
    #
    # @return [PBXGroup] The group.
    #
    def group_for_spec(spec_name, subgroup_key = nil)
      pod_name = Specification.root_name(spec_name)
      group = pod_group(pod_name)
      raise "[Bug] Unable to locate group for Pod named `#{pod_name}`" unless group
      if spec_name != pod_name
        subspecs_names = spec_name.gsub(pod_name + '/', '').split('/')
        subspecs_names.each do |name|
          group = group[name] || group.new_group(name)
        end
      end

      if subgroup_key
        subgroup_name = SPEC_SUBGROUPS[subgroup_key]
        raise ArgumentError, "Unrecognized subgroup key `#{subgroup_key}`" unless subgroup_name
        group = group[subgroup_name] || group.new_group(subgroup_name)
      end

      group
    end

    # Returns the support files group for the Pod with the given name.
    #
    # @param  [String] pod_name
    #         The name of the Pod.
    #
    # @return [PBXGroup] The group.
    #
    def pod_support_files_group(pod_name, dir)
      group = pod_group(pod_name)
      support_files_group = group['Support Files']
      unless support_files_group
        support_files_group = group.new_group('Support Files', dir)
      end
      support_files_group
    end

    public

    # @!group File references
    #-------------------------------------------------------------------------#

    # Adds a file reference to given path as a child of the given group.
    #
    # @param  [Array<Pathname,String>] absolute_path
    #         The path of the file.
    #
    # @param  [PBXGroup] group
    #         The group for the new file reference.
    #
    # @param  [Boolean] reflect_file_system_structure
    #         Whether group structure should reflect the file system structure.
    #         If yes, where needed, intermediate groups are created, similar to
    #         how mkdir -p operates.
    #
    # @param  [Pathname] base_path
    #         The base path for newly created groups when reflect_file_system_structure is true.
    #         If nil, the provided group's real_path is used.
    #
    # @return [PBXFileReference] The new file reference.
    #
    def add_file_reference(absolute_path, group, reflect_file_system_structure = false, base_path = nil)
      file_path_name = absolute_path.is_a?(Pathname) ? absolute_path : Pathname(absolute_path)
      if ref = reference_for_path(file_path_name)
        return ref
      end

      group = group_for_path_in_group(file_path_name, group, reflect_file_system_structure, base_path)
      ref = group.new_file(file_path_name.realpath)
      @refs_by_absolute_path[file_path_name.to_s] = ref
    end

    # @!group File references
    #-------------------------------------------------------------------------#

    # Adds a file reference for a project as a child of the given group.
    #
    # @param  [Project] project
    #         The project to add as a subproject reference.
    #
    # @param  [PBXGroup] group
    #         The group for the new subproject reference.
    #
    # @return [PBXFileReference] The new file reference.
    #
    def add_subproject_reference(project, group)
      new_subproject_file_reference(project.path, group)
    end

    # Adds a file reference for a cached project as a child of the given group.
    #
    # @param  [Sandbox] sandbox
    #         The sandbox used for installation.
    #
    # @param  [MetadataCache] metadata
    #         The metadata holding the required properties to create a subproject reference.
    #
    # @param  [PBXGroup] group
    #         The group for the new subproject reference.
    #
    # @return [PBXFileReference] The new file reference.
    #
    def add_cached_subproject_reference(sandbox, metadata, group)
      new_subproject_file_reference(sandbox.root + metadata.container_project_path, group)
    end

    # Returns the file reference for the given absolute path.
    #
    # @param  [#to_s] absolute_path
    #         The absolute path of the file whose reference is needed.
    #
    # @return [PBXFileReference] The file reference.
    # @return [Nil] If no file reference could be found.
    #
    def reference_for_path(absolute_path)
      absolute_path = absolute_path.is_a?(Pathname) ? absolute_path : Pathname(absolute_path)
      unless absolute_path.absolute?
        raise ArgumentError, "Paths must be absolute #{absolute_path}"
      end

      refs_by_absolute_path[absolute_path.to_s] ||= refs_by_absolute_path[absolute_path.realpath.to_s]
    end

    # Adds a file reference to the Podfile.
    #
    # @param  [#to_s] podfile_path
    #         The path of the Podfile.
    #
    # @return [PBXFileReference] The new file reference.
    #
    def add_podfile(podfile_path)
      new_file(podfile_path, :project).tap do |podfile_ref|
        mark_ruby_file_ref(podfile_ref)
      end
    end

    # Sets the syntax of the provided file reference to be Ruby, in the case that
    # the file does not already have a ".rb" file extension (ex. the Podfile)
    #
    # @param  [PBXFileReference] file_ref
    #         The file reference to change
    #
    def mark_ruby_file_ref(file_ref)
      file_ref.xc_language_specification_identifier = 'xcode.lang.ruby'
      file_ref.explicit_file_type = 'text.script.ruby'
      file_ref.last_known_file_type = 'text'
      file_ref.tab_width = '2'
      file_ref.indent_width = '2'
    end

    # Adds a new build configuration to the project and populates it with
    # default settings according to the provided type.
    #
    # @note   This method extends the original Xcodeproj implementation to
    #         include a preprocessor definition named after the build
    #         setting. This is done to support the TargetEnvironmentHeader
    #         specification of Pods available only on certain build
    #         configurations.
    #
    # @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 new build configuration.
    #
    def add_build_configuration(name, type)
      build_configuration = super
      settings = build_configuration.build_settings
      definitions = settings['GCC_PREPROCESSOR_DEFINITIONS'] || ['$(inherited)']
      defines = [defininition_for_build_configuration(name)]
      defines << 'DEBUG' if type == :debug
      defines.each do |define|
        value = "#{define}=1"
        unless definitions.include?(value)
          definitions.unshift(value)
        end
      end
      settings['GCC_PREPROCESSOR_DEFINITIONS'] = definitions

      if type == :debug
        settings['SWIFT_ACTIVE_COMPILATION_CONDITIONS'] = 'DEBUG'
      end

      build_configuration
    end

    # @param  [String] name
    #         The name of the build configuration.
    #
    # @return [String] The preprocessor definition to set for the configuration.
    #
    def defininition_for_build_configuration(name)
      "POD_CONFIGURATION_#{name.underscore}".gsub(/[^a-zA-Z0-9_]/, '_').upcase
    end

    private

    # @!group Private helpers
    #-------------------------------------------------------------------------#

    # @return [Hash{String => PBXFileReference}] The file references grouped
    #         by absolute path.
    #
    attr_reader :refs_by_absolute_path

    # @return [Hash{[Pathname, String] => PBXVariantGroup}] The variant groups
    #         grouped by absolute path of parent dir and name.
    #
    attr_reader :variant_groups_by_path_and_name

    # Returns the group for an absolute file path in another group.
    # Creates subgroups to reflect the file system structure if
    # reflect_file_system_structure is set to true.
    # Makes a variant group if the path points to a localized file inside a
    # *.lproj directory. To support Apple Base Internationalization, the same
    # variant group is returned for interface files and strings files with
    # the same name.
    #
    # @param  [Pathname] absolute_pathname
    #         The pathname of the file to get the group for.
    #
    # @param  [PBXGroup] group
    #         The parent group used as the base of the relative path.
    #
    # @param  [Boolean] reflect_file_system_structure
    #         Whether group structure should reflect the file system structure.
    #         If yes, where needed, intermediate groups are created, similar to
    #         how mkdir -p operates.
    #
    # @param  [Pathname] base_path
    #         The base path for the newly created group. If nil, the provided group's real_path is used.
    #
    # @return [PBXGroup] The appropriate group for the filepath.
    #         Can be PBXVariantGroup, if the file is localized.
    #
    def group_for_path_in_group(absolute_pathname, group, reflect_file_system_structure, base_path = nil)
      unless absolute_pathname.absolute?
        raise ArgumentError, "Paths must be absolute #{absolute_pathname}"
      end
      unless base_path.nil? || base_path.absolute?
        raise ArgumentError, "Paths must be absolute #{base_path}"
      end

      relative_base = base_path.nil? ? group.real_path : base_path.realdirpath
      relative_pathname = absolute_pathname.relative_path_from(relative_base)
      relative_dir = relative_pathname.dirname

      # Add subgroups for directories, but treat .lproj as a file
      if reflect_file_system_structure
        path = relative_base
        relative_dir.each_filename do |name|
          break if name.to_s.downcase.include? '.lproj'
          next if name == '.'
          # Make sure groups have the correct absolute path set, as intermittent
          # directories may not be included in the group structure
          path += name
          group = group.children.find { |c| c.display_name == name } || group.new_group(name, path)
        end
      end

      # Turn files inside .lproj directories into a variant group
      if relative_dir.basename.to_s.downcase.include? '.lproj'
        group_name = variant_group_name(absolute_pathname)
        lproj_parent_dir = absolute_pathname.dirname.dirname
        group = @variant_groups_by_path_and_name[[lproj_parent_dir, group_name]] ||=
                  group.new_variant_group(group_name, lproj_parent_dir)
      end

      group
    end

    # Returns the name to be used for a the variant group for a file at a given path.
    # The path must be localized (within an *.lproj directory).
    #
    # @param  [Pathname] path The localized path to get a variant group name for.
    #
    # @return [String] The variant group name.
    #
    def variant_group_name(path)
      unless path.to_s.downcase.include?('.lproj/')
        raise ArgumentError, 'Only localized resources can be added to variant groups.'
      end

      # When using Base Internationalization for XIBs and Storyboards a strings
      # file is generated with the same name as the XIB/Storyboard in each .lproj
      # directory:
      #   Base.lproj/MyViewController.xib
      #   fr.lproj/MyViewController.strings
      #
      # In this scenario we want the variant group to be the same as the XIB or Storyboard.
      #
      # Base Internationalization: https://developer.apple.com/library/ios/documentation/MacOSX/Conceptual/BPInternational/InternationalizingYourUserInterface/InternationalizingYourUserInterface.html
      if path.extname.downcase == '.strings'
        %w(.xib .storyboard).each do |extension|
          possible_interface_file = path.dirname.dirname + 'Base.lproj' + path.basename.sub_ext(extension)
          return possible_interface_file.basename.to_s if possible_interface_file.exist?
        end
      end

      path.basename.to_s
    end

    def new_subproject_file_reference(project_path, group)
      if ref = reference_for_path(project_path)
        return ref
      end

      # We call into the private function `FileReferencesFactory.new_file_reference` instead of `FileReferencesFactory.new_reference`
      # because it delegates into `FileReferencesFactory.new_subproject` which has the extra behavior of opening the Project which
      # is an expensive operation for large projects.
      #
      ref = Xcodeproj::Project::FileReferencesFactory.send(:new_file_reference, group, project_path, :group)
      ref.name = Pathname(project_path).basename('.*').to_s
      ref.include_in_index = nil

      attribute = PBXProject.references_by_keys_attributes.find { |attrb| attrb.name == :project_references }
      project_reference = ObjectDictionary.new(attribute, group.project.root_object)
      project_reference[:project_ref] = ref
      root_object.project_references << project_reference
      refs_by_absolute_path[project_path.to_s] = ref
      ref
    end

    # Returns the parent group a new subproject reference should belong to.
    #
    def group_for_subproject_reference(development)
      if pod_target_subproject
        dependencies_group
      else
        development ? development_pods : pods
      end
    end

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