lib/xcodeproj/config.rb



module Xcodeproj

  # This class holds the data for a Xcode build settings file (xcconfig) and
  # serializes it.
  #
  class Config

    require 'set'

    # @return [Hash{String => String}] The attributes of the settings file
    #   excluding frameworks, weak_framework and libraries.
    #
    attr_accessor :attributes

    # @return [Array<String>] The list of the frameworks required by this
    #   settings file.
    #
    attr_accessor :frameworks

    # @return [Array<String>] The list of the *weak* frameworks required by
    #   this settings file.
    #
    attr_accessor :weak_frameworks

    # @return [Array<String>] The list of the libraries required by this
    #   settings file.
    #
    attr_accessor :libraries

    # @return [Array] The list of the configuration files included by this
    #   configuration file (`#include "SomeConfig"`).
    #
    attr_accessor :includes

    # Returns a new instance of Config
    #
    # @param [Hash, File, String] xcconfig_hash_or_file  Initial data.
    #
    def initialize(xcconfig_hash_or_file = {})
      @attributes = {}
      @includes = []
      @frameworks, @weak_frameworks, @libraries = Set.new, Set.new, Set.new
      merge!(extract_hash(xcconfig_hash_or_file))
    end


    #@! group Serialization

    # Serializes the internal data in the xcconfig format.
    #
    # @example
    #
    #   config = Config.new('PODS_ROOT' => '"$(SRCROOT)/Pods"', 'OTHER_LDFLAGS' => '-lxml2')
    #   config.to_s # => "PODS_ROOT = \"$(SRCROOT)/Pods\"\nOTHER_LDFLAGS = -lxml2"
    #
    # @return [String]  The serialized internal data.
    def to_s
      to_hash.map { |key, value| "#{key} = #{value}" }.join("\n")
    end

    # @return [void] Writes the serialized representation of the internal data
    # to the given path.
    #
    # @param [Pathname] pathname  The file that the data should be written to.
    #
    def save_as(pathname)
      pathname.open('w') { |file| file << to_s }
    end

    # @return [Hash] The hash reppresentation of the framework. The hash
    # includes the frameworks, the weak frameworks and the libraries in the
    # `Other Linker Flags` (`OTHER_LDFLAGS`).
    #
    def to_hash
      hash = @attributes.dup
      flags = hash['OTHER_LDFLAGS'] || ''
      flags = flags.dup.strip
      flags << libraries.to_a.sort.reduce('')  {| memo, l | memo << " -l#{l}" }
      flags << frameworks.to_a.sort.reduce('') {| memo, f | memo << " -framework #{f}" }
      flags << weak_frameworks.to_a.sort.reduce('') {| memo, f | memo << " -weak_framework #{f}" }
      hash['OTHER_LDFLAGS'] = flags.strip
      hash.delete('OTHER_LDFLAGS') if flags.strip.empty?
      hash
    end



    #@! group Merging

    # Merges the given xcconfig hash or Config into the internal data.
    #
    # If a key in the given hash already exists, in the internal data, then its
    # value is appended to the value in the internal data.
    #
    # @example
    #
    #   config = Config.new('PODS_ROOT' => '"$(SRCROOT)/Pods"', 'OTHER_LDFLAGS' => '-lxml2')
    #   config.merge!('OTHER_LDFLAGS' => '-lz', 'HEADER_SEARCH_PATHS' => '"$(PODS_ROOT)/Headers"')
    #   config.to_hash # => { 'PODS_ROOT' => '"$(SRCROOT)/Pods"', 'OTHER_LDFLAGS' => '-lxml2 -lz', 'HEADER_SEARCH_PATHS' => '"$(PODS_ROOT)/Headers"' }
    #
    # @param [Hash, Config] xcconfig  The data to merge into the internal data.
    #
    def merge!(xcconfig)
      if xcconfig.is_a? Config
        @attributes.merge!(xcconfig.attributes) { |key, v1, v2| "#{v1} #{v2}" }
        @libraries.merge   xcconfig.libraries
        @frameworks.merge  xcconfig.frameworks
        @weak_frameworks.merge  xcconfig.weak_frameworks
      else
      @attributes.merge!(xcconfig.to_hash) { |key, v1, v2| "#{v1} #{v2}" }
      # Parse frameworks and libraries. Then remove the from the linker flags
      flags = @attributes['OTHER_LDFLAGS']
      return unless flags

      frameworks = flags.scan(/-framework\s+([^\s]+)/).map { |m| m[0] }
      weak_frameworks = flags.scan(/-weak_framework\s+([^\s]+)/).map { |m| m[0] }
      libraries  = flags.scan(/-l ?([^\s]+)/).map { |m| m[0] }
      @frameworks.merge frameworks
      @weak_frameworks.merge weak_frameworks
      @libraries.merge libraries

      new_flags = flags.dup
      frameworks.each {|f| new_flags.gsub!("-framework #{f}", "") }
      weak_frameworks.each {|f| new_flags.gsub!("-weak_framework #{f}", "") }
      libraries.each  {|l| new_flags.gsub!("-l#{l}", ""); new_flags.gsub!("-l #{l}", "") }
      @attributes['OTHER_LDFLAGS'] = new_flags.gsub("\w*", ' ').strip
      end
    end
    alias_method :<<, :merge!

    def merge(config)
      self.dup.tap { |x|x.merge!(config) }
    end

    def dup
      Xcodeproj::Config.new(self.to_hash.dup)
    end


    #@! group Object methods

    def inspect
      to_hash.inspect
    end

    def ==(other)
      other.respond_to?(:to_hash) && other.to_hash == self.to_hash
    end

    private

    def extract_hash(argument)
      if argument.respond_to? :read
        hash_from_file_content(argument.read)
      elsif File.readable? argument.to_s
        hash_from_file_content(File.read(argument))
      else
        argument
      end
    end

    def hash_from_file_content(raw_string)
      hash = {}
      raw_string.split("\n").each do |line|
        uncommented_line = strip_comment(line)
        if include = extract_include(uncommented_line)
          @includes.push include
        else
          key, value = extract_key_value(uncommented_line)
          hash[key] = value if key
        end
      end
      hash
    end

    def strip_comment(line)
      line.partition('//').first
    end

    def extract_include(line)
      regexp = /#include\s*"(.+)"/
      match = line.match(regexp)
      match[1] if match
    end

    def extract_key_value(line)
      key, value = line.split('=', 2)
      if key && value
        [key.strip, value.strip]
      else
        []
      end
    end

  end
end