lib/xcodeproj/project.rb



require 'fileutils'
require 'pathname'
require 'xcodeproj/xcodeproj_ext'

require 'xcodeproj/project/object'
require 'xcodeproj/project/recursive_diff'

module Xcodeproj

  # This class represents a Xcode project document.
  #
  # It can be used to manipulate existing documents or even create new ones
  # from scratch.
  #
  # An Xcode project document is a plist file where the root is a dictionary
  # containing the following keys:
  #
  # - archiveVersion: the version of the document.
  # - objectVersion: the version of the objects description.
  # - classes: a key that apparently is always empty.
  # - objects: a dictionary where the UUID of every object is associated to
  #   its attributes.
  # - rootObject: the UUID identifier of the root object ({PBXProject}).
  #
  # Every object is in turn a dictionary that specifies an `isa` (the class of
  # the object) and in accordance to it maintains a set attributes. Those
  # attributes might reference one or more other objects by UUID. If the
  # reference is a collection, it is ordered.
  #
  # The {Project} API returns instances of {AbstractObject} which wrap the
  # objects described in the Xcode project document. All the attributes types
  # are preserved from the plist, except for the relationships which are
  # replaced with objects instead of UUIDs.
  #
  # An object might be referenced by multiple objects, an when no other object
  # is references it, it becomes unreachable (the root object is referenced by
  # the project itself). Xcodeproj takes care of adding and removing those
  # objects from the `objects` dictionary so the project is always in a
  # consistent state.
  #
  class Project

    include Object

    # @return [String] the archive version.
    #
    attr_reader :archive_version

    # @return [Hash] an dictionary whose purpose is unknown.
    #
    attr_reader :classes

    # @return [String] the objects version.
    #
    attr_reader :object_version

    # @return [Hash{String => AbstractObject}] A hash containing all the
    #   objects of the project by UUID.
    #
    attr_reader :objects_by_uuid

    # @return [PBXProject] the root object of the project.
    #
    attr_reader :root_object

    # Creates a new Project instance or initializes one with the data of an
    # existing Xcode document.
    #
    # @param [Pathname, String] xcodeproj
    #   The path to the Xcode project document (xcodeproj).
    #
    # @raise If the project versions are more recent than the ones know to
    #   Xcodeproj to prevent it from corrupting existing projects.  Naturally,
    #   this would never happen with a project generated by xcodeproj itself.
    #
    # @raise If it can't find the root object. This means that the project is
    #   malformed.
    #
    # @example Opening a project
    #   Project.new("path/to/Project.xcodeproj")
    #
    def initialize(xcodeproj = nil)
      @objects_by_uuid = {}
      @generated_uuids = []
      @available_uuids = []

      if xcodeproj
        file = File.join(xcodeproj, 'project.pbxproj')
        plist = Xcodeproj.read_plist(file.to_s)

        @archive_version =  plist['archiveVersion']
        @object_version  =  plist['objectVersion']
        @classes         =  plist['classes']

        root_object_uuid = plist['rootObject']
        @root_object = new_from_plist(root_object_uuid, plist['objects'], self)

      if (@archive_version.to_i > Constants::LAST_KNOWN_ARCHIVE_VERSION || @object_version.to_i > Constants::LAST_KNOWN_OBJECT_VERSION)
          raise '[Xcodeproj] Unknown archive or object version.'
        end

        unless @root_object
          raise "[Xcodeproj] Unable to find a root object in #{file}."
        end
      else
        @archive_version =  Constants::LAST_KNOWN_ARCHIVE_VERSION.to_s
        @object_version  =  Constants::LAST_KNOWN_OBJECT_VERSION.to_s
        @classes         =  {}

        root_object = new(PBXProject)
        root_object.main_group = new(PBXGroup)
        root_object.product_ref_group = root_object.main_group.new_group('Products')

        config_list = new(XCConfigurationList)
        config_list.default_configuration_name = 'Release'
        config_list.default_configuration_is_visible = '0'
        root_object.build_configuration_list = config_list

        %w| Release Debug |.each do |name|
          build_configuration = new(XCBuildConfiguration)
          build_configuration.name = name
          build_configuration.build_settings = {}
          config_list.build_configurations << build_configuration
        end

        @root_object = root_object
        root_object.add_referrer(self)
        new_group('Frameworks')
      end
    end

    # Compares the project to another one, or to a plist representation.
    #
    # @param [#to_hash] other the object to compare.
    #
    # @return [Boolean] whether the project is equivalent to the given object.
    #
    def ==(other)
      other.respond_to?(:to_hash) && to_hash == other.to_hash
    end

    def to_s
      "Project with root object UUID: #{root_object.uuid}"
    end

    alias :inspect :to_s



    # @!group Plist serialization

    # Creates a new object from the given UUID and `objects` hash (of a plist).
    #
    # The method sets up any relationship of the new object, generating the
    # destination object(s) if not already present in the project.
    #
    # @note This method is used to generate the root object
    #       from a plist. Subsequent invocation are called by the
    #       {AbstractObject#configure_with_plist}. Clients of {Xcodeproj} are
    #       not expected to call this method.
    #
    # @visibility private.
    #
    # @param [String] uuid
    #   the UUID of the object that needs to be generated.
    #
    # @param [Hash {String => Hash}] objects_by_uuid_plist
    #   the `objects` hash of the plist representation of the project.
    #
    # @param [Boolean] root_object
    #   whether the requested object is the root object and needs to be
    #   retained by the project before configuration to add it to the `objects`
    #   hash and avoid infinite loops.
    #
    # @return [AbstractObject] the new object.
    #
    def new_from_plist(uuid, objects_by_uuid_plist, root_object = false)
      attributes = objects_by_uuid_plist[uuid]
      klass = Object.const_get(attributes['isa'])
      object = klass.new(self, uuid)
      object.add_referrer(self) if root_object

      object.configure_with_plist(objects_by_uuid_plist)
      object
    end

    # @return [Hash] The plist representation of the project.
    #
    def to_hash
      plist = {}
      objects_dictionary = {}
      objects.each { |obj| objects_dictionary[obj.uuid] = obj.to_plist }
      plist['objects']        =  objects_dictionary
      plist['archiveVersion'] =  archive_version.to_s
      plist['objectVersion']  =  object_version.to_s
      plist['classes']        =  classes
      plist['rootObject']     =  root_object.uuid
      plist
    end

    alias :to_plist :to_hash

    # Converts the objects tree to a hash substituting the hash
    # of the referenced to their uuid reference. As a consequene the hash of an
    # object might appear multiple times and the information about their
    # uniqueness is lost.
    #
    # This method is designed to work in conjuction with {Hash#recursive_diff}
    # to provie a complete, yet redable, diff of two projects *not* affected by
    # isa differences.
    #
    # @return [Hash] a hash reppresentation of the project different from the
    #   plist one.
    #
    def to_tree_hash
      hash = {}
      objects_dictionary = {}
      hash['objects']        =  objects_dictionary
      hash['archiveVersion'] =  archive_version.to_s
      hash['objectVersion']  =  object_version.to_s
      hash['classes']        =  classes
      hash['rootObject']     =  root_object.to_tree_hash
      hash
    end

    # Serializes the internal data as a property list and stores it on disk at
    # the given path (`xcodeproj` file).
    #
    # @example Saving a project
    #   project.save_as("path/to/Project.xcodeproj") #=> true
    #
    # @param [String, Pathname] projpath  The path where the data should be
    #                                     stored.
    #
    # @return [Boolean]                   Whether or not saving was successful.
    #
    def save_as(projpath)
      projpath = projpath.to_s
      FileUtils.mkdir_p(projpath)
      Xcodeproj.write_plist(to_plist, File.join(projpath, 'project.pbxproj'))
    end



    # @!group Creating objects

    # Creates a new object with a suitable UUID.
    #
    # The object is only configured with the default values of the `:simple`
    # attributes, for this reason it is better to use the convenience methods
    # offered by the {AbstractObject} subclasses or by this class.
    #
    # @param [Class] klass     The concrete subclass of AbstractObject for new
    #                          object.
    #
    # @return [AbstractObject] the new object.
    #
    def new(klass)
      object = klass.new(self, generate_uuid)
      object.initialize_defaults
      object
    end

    # Generates a UUID unique for the project.
    #
    # @note UUIDs are not guaranteed to be generated unique because we need to
    #       trim the ones generated in the xcodeproj extension.
    #
    # @note Implementation detail: as objects usually are created serially this
    #       method creates a batch of UUID and stores the not colliding ones,
    #       so the search for collisions with known UUIDS (a performance
    #       bottleneck) is performed is performed less often.
    #
    # @return [String] A UUID unique to the project.
    #
    def generate_uuid
      while @available_uuids.empty?
        generate_available_uuid_list
      end
      @available_uuids.shift
    end

    # @return [Array<String>] the list of all the generated UUIDs.
    #
    #   Used for checking new UUIDs for duplicates with UUIDs already generated
    #   but used for objects which are not yet part of the `objects` hash but
    #   which might be added at a later time.
    #
    attr_reader :generated_uuids

    # Pre-generates the given number of UUIDs. Useful for optimizing
    # performance when the rough number of objects that will be created is
    # known in advance.
    #
    # @param [Integer] count
    #   the number of UUIDs that should be generated.
    #
    # @note This method might generated a minor number of uniques UUIDs than
    #       the given count, because some might be duplicated a thus will be
    #       discarded.
    #
    # @return [void]
    #
    def generate_available_uuid_list(count = 100)
      new_uuids = (0..count).map { Xcodeproj.generate_uuid }
      uniques = (new_uuids - (@generated_uuids + uuids))
      @generated_uuids += uniques
      @available_uuids += uniques
    end

    ## CONVENIENCE METHODS #####################################################

    # @!group Convenience accessors

    # @return [Array<AbstractObject>] all the objects of the project.
    #
    def objects
      objects_by_uuid.values
    end

    # @return [Array<String>] all the UUIDs of the project.
    #
    def uuids
      objects_by_uuid.keys
    end

    # @return [Array<AbstractObject>] all the objects of the project with a
    #   given isa.
    #
    def list_by_class(klass)
      objects.select { |o| o.class == klass }
    end

    # @return [PBXGroup] the main top-level group.
    #
    def main_group
      root_object.main_group
    end

    # @return [ObjectList<PBXGroup>] a list of all the groups in the
    #   project.
    #
    def groups
      main_group.groups
    end

    # Returns a group at the given subpath relative to the main group.
    #
    # @example
    #   frameworks = project['Frameworks']
    #   frameworks.name #=> 'Frameworks'
    #   main_group.children.include? frameworks #=> True
    #
    # @param [String] group_path
    #
    # @return [PBXGroup] the group at the given subpath.
    #
    def [](group_path)
      main_group[group_path]
    end

    # @return [ObjectList<PBXFileReference>] a list of all the files in the
    #   project.
    #
    def files
      objects.select { |obj| obj.class == PBXFileReference }
    end

    # @return [ObjectList<PBXNativeTarget>] A list of all the targets in the
    #   project.
    #
    def targets
      root_object.targets
    end

    # @return [PBXGroup] The group which holds the product file references.
    #
    def products_group
      root_object.product_ref_group
    end

    # @return [ObjectList<PBXFileReference>] A list of the product file
    #   references.
    #
    def products
      products_group.children
    end

    # @return [PBXGroup] the `Frameworks` group creating it if necessary.
    #
    def frameworks_group
      main_group['Frameworks'] || new_group('Frameworks')
    end

    # @return [ObjectList<XCBuildConfiguration>] A list of project wide
    #   build configurations.
    #
    def build_configurations
      root_object.build_configuration_list.build_configurations
    end

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



    # @!group Convenience methods for generating objects

    # Creates a new file reference at the given subpath of the main group.
    #
    # @param (see PBXGroup#new_file)
    #
    # @return [PBXFileReference] the new file.
    #
    def new_file(path, sub_group_path = nil)
      main_group.new_file(path, sub_group_path)
    end

    # Creates a new group at the given subpath of the main group.
    #
    # @param (see PBXGroup#new_group)
    #
    # @return [PBXGroup] the new group.
    #
    def new_group(name, path = nil)
      main_group.new_group(name, path)
    end

    # Adds a file reference for a system framework to the project.
    #
    # The file reference can then be added to the build files of a
    # {PBXFrameworksBuildPhase}.
    #
    # @example
    #
    #     framework = project.add_system_framework('QuartzCore')
    #
    #     target = project.targets.first
    #     build_phase = target.frameworks_build_phases.first
    #     build_phase.files << framework.buildFiles.new
    #
    # @param [String] name        The name of a framework in the SDK System
    #                             directory.
    # @return [PBXFileReference]  The file reference object.
    #
    def add_system_framework(name)
      path = "System/Library/Frameworks/#{name}.framework"
      if file = frameworks_group.files.first { |f| f.path == path }
        file
      else
        framework_ref = frameworks_group.new_file(path)
        framework_ref.name = "#{name}.framework"
        framework_ref.source_tree = 'SDKROOT'
        framework_ref
      end
    end

    # @return [PBXNativeTarget] Creates a new target and adds it to the
    #   project.
    #
    #   The target is configured for the given platform and its file reference
    #   it is added to the {products_group}.
    #
    #   The target is pre-populated with common build phases, and all the
    #   Frameworks of the project are added to to its Frameworks phase.
    #
    # @todo Adding all the Frameworks is required by CocoaPods and should be
    #       performed there.
    #
    # @param [Symbol] type
    #   the type of target.
    #   Can be `:application`, `:dynamic_library` or `:static_library`.
    #
    # @param [String] name
    #   the name of the static library product.
    #
    # @param [Symbol] platform
    #   the platform of the static library.
    #   Can be `:ios` or `:osx`.
    #
    def new_target(type, name, platform)
      add_system_framework(platform == :ios ? 'Foundation' : 'Cocoa')

      # Target
      target = new(PBXNativeTarget)
      targets << target
      target.name = name
      target.product_name = name
      target.product_type = Constants::PRODUCT_TYPE_UTI[type]
      target.build_configuration_list = configuration_list(platform)

      # Product
      product = products_group.new_static_library(name)
      target.product_reference = product

      # Build phases
      target.build_phases << new(PBXSourcesBuildPhase)
      frameworks_phase = new(PBXFrameworksBuildPhase)
      frameworks_group.files.each { |framework| frameworks_phase.add_file_reference(framework) }
      target.build_phases << frameworks_phase

      target
    end

    # Returns a new configuration list, populated with release and debug
    # configurations with common build settings for the given platform.
    #
    # @param [Symbol] platform
    #   the platform for the configuration list, can be `:ios` or `:osx`.
    #
    # @return [XCConfigurationList] the generated configuration list.
    #
    def configuration_list(platform)
      cl = new(XCConfigurationList)
      cl.default_configuration_is_visible = '0'
      cl.default_configuration_name = 'Release'

      release_conf = new(XCBuildConfiguration)
      release_conf.name = 'Release'
      release_conf.build_settings = configuration_list_settings(platform, :release)

      debug_conf = new(XCBuildConfiguration)
      debug_conf.name = 'Debug'
      debug_conf.build_settings = configuration_list_settings(platform, :debug)

      cl.build_configurations << release_conf
      cl.build_configurations << debug_conf
      cl
    end

    # Returns the common build settings for a given platform and configuration
    # name.
    #
    # @param [Symbol] platform
    #   the platform for the build settings, can be `:ios` or `:osx`.
    #
    # @param [Symbol] name
    #   the name of the build configuration, can be `:release` or `:debug`.
    #
    # @return [Hash] The common build settings
    #
    def configuration_list_settings(platform, name)
      common_settings = Constants::COMMON_BUILD_SETTINGS
      bs = common_settings[:all].dup
      bs = bs.merge(common_settings[name])
      bs = bs.merge(common_settings[platform])
      bs = bs.merge(common_settings[[platform, name]])
      bs
    end

  end
end