Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 275 lines (245 sloc) 8.82 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
require 'digest'

module Pod
  class Lockfile

    # @return [Lockfile] Returns the Lockfile saved in path.
    # Returns {nil} If the file can't be loaded.
    #
    def self.from_file(path)
      return nil unless path.exist?
      begin
        hash = YAML.load(File.open(path))
      rescue Exception => e
        raise Informative, "Podfile.lock syntax error: #{e.inspect}"
      end
      lockfile = Lockfile.new(hash)
      lockfile.defined_in_file = path
      lockfile
    end

    # @return [Lockfile] Generates a lockfile from a {Podfile} and the
    # list of {Specifications} that were installed.
    #
    def self.generate(podfile, specs)
      Lockfile.new(generate_hash_from_podfile(podfile, specs))
    end

    # @return [String] The file where this Lockfile is defined.
    #
    attr_accessor :defined_in_file

    # @return [String] The hash used to initialize the Lockfile.
    #
    attr_reader :to_hash

    # @param [Hash] hash A Hash representation of a Lockfile.
    #
    def initialize(hash)
      @to_hash = hash
    end

    # @return [Array<String, Hash{String => Array[String]}>] The pods installed
    # and their dependencies.
    #
    def pods
      @pods ||= to_hash['PODS'] || []
    end

    # @return [Array<Dependency>] The Podfile dependencies used during the last
    # install.
    #
    def dependencies
      @dependencies ||= to_hash['DEPENDENCIES'].map { |dep| dependency_from_string(dep) } || []
    end

    # @return [Hash{String => Hash}] A hash where the name of the pods are
    # the keys and the values are the parameters of an {AbstractExternalSource}
    # of the dependency that required the pod.
    #
    def external_sources
      @external_sources ||= to_hash["EXTERNAL SOURCES"] || {}
    end

    # @return [Array<String>] The names of the installed Pods.
    #
    def pods_names
      @pods_names ||= pods.map do |pod|
        pod = pod.keys.first unless pod.is_a?(String)
        name_and_version_for_pod(pod)[0]
      end
    end

    # @return [Hash{String => Version}] A Hash containing the name
    # of the installed Pods as the keys and their corresponding {Version}
    # as the values.
    #
    def pods_versions
      unless @pods_versions
        @pods_versions = {}
        pods.each do |pod|
          pod = pod.keys.first unless pod.is_a?(String)
          name, version = name_and_version_for_pod(pod)
          @pods_versions[name] = version
        end
      end
      @pods_versions
    end

    # @return [Dependency] A dependency that describes the exact installed version
    # of a Pod.
    #
    def dependency_for_installed_pod_named(name)
      version = pods_versions[name]
      raise Informative, "Attempt to lock a Pod without an known version." unless version
      dependency = Dependency.new(name, version)
      if external_source = external_sources[name]
        dependency.external_source = Dependency::ExternalSources.from_params(dependency.name, external_source)
      end
      dependency
    end

    # @param [String] The string that describes a {Specification} generated
    # from {Specification#to_s}.
    #
    # @example Strings examples
    # "libPusher"
    # "libPusher (1.0)"
    # "libPusher (HEAD based on 1.0)"
    # "RestKit/JSON"
    #
    # @return [String, Version] The name and the version of a
    # pod.
    #
    def name_and_version_for_pod(string)
      match_data = string.match(/(\S*) \((.*)\)/)
      name = match_data[1]
      vers = Version.from_string(match_data[2])
      [name, vers]
    end

    # @param [String] The string that describes a {Dependency} generated
    # from {Dependency#to_s}.
    #
    # @example Strings examples
    # "libPusher"
    # "libPusher (= 1.0)"
    # "libPusher (~> 1.0.1)"
    # "libPusher (> 1.0, < 2.0)"
    # "libPusher (HEAD)"
    # "libPusher (from `www.example.com')"
    # "libPusher (defined in Podfile)"
    # "RestKit/JSON"
    #
    # @return [Dependency] The dependency described by the string.
    #
    def dependency_from_string(string)
      match_data = string.match(/(\S*)( (.*))?/)
      name = match_data[1]
      version = match_data[2]
      version = version.gsub(/[()]/,'') if version
      case version
      when nil
        Dependency.new(name)
      when /defined in Podfile/
        # @TODO: store the whole spec?, the version?
        Dependency.new(name)
      when /from `(.*)'/
        external_source_info = external_sources[name]
        Dependency.new(name, external_source_info)
      when /HEAD/
        # @TODO: find a way to serialize from the Downloader the information
        # necessary to restore a head version.
        Dependency.new(name, :head)
      else
        Dependency.new(name, version)
      end
    end

    # Analyzes the {Lockfile} and detects any changes applied to the {Podfile}
    # since the last installation.
    #
    # For each Pod, it detects one state among the following:
    #
    # - added: Pods that weren't present in the Podfile.
    # - changed: Pods that were present in the Podfile but changed:
    # - Pods whose version is not compatible anymore with Podfile,
    # - Pods that changed their head or external options.
    # - removed: Pods that were removed form the Podfile.
    # - unchanged: Pods that are still compatible with Podfile.
    #
    # @TODO: detect changes for inline dependencies?
    #
    # @return [Hash{Symbol=>Array[Strings]}] A hash where pods are grouped
    # by the state in which they are.
    #
    def detect_changes_with_podfile(podfile)
      previous_podfile_deps = dependencies.map(&:name)
      user_installed_pods = pods_names.reject { |name| !previous_podfile_deps.include?(name) }
      deps_to_install = podfile.dependencies.dup

      result = {}
      result[:added] = []
      result[:changed] = []
      result[:removed] = []
      result[:unchanged] = []

      user_installed_pods.each do |pod_name|
        dependency = deps_to_install.find { |d| d.name == pod_name }
        deps_to_install.delete(dependency)
        version = pods_versions[pod_name]
        external_source = Dependency::ExternalSources.from_params(pod_name, external_sources[pod_name])

        if dependency.nil?
          result[:removed] << pod_name
        elsif !dependency.match_version?(version) || dependency.external_source != external_source
          result[:changed] << pod_name
        else
          result[:unchanged] << pod_name
        end
      end

      deps_to_install.each do |dependency|
        result[:added] << dependency.name
      end
      result
    end

    # @return [void] Writes the Lockfile to {#path}.
    #
    def write_to_disk(path)
      path.dirname.mkpath unless path.dirname.exist?
      File.open(path, 'w') {|f| f.write(to_yaml) }
      defined_in_file = path
    end

    # @return [String] A string useful to represent the Lockfile in a message
    # presented to the user.
    #
    def to_s
      "Podfile.lock"
    end

    # @return [String] The YAML representation of the Lockfile, used for
    # serialization.
    #
    def to_yaml
      to_hash.to_yaml.gsub(/^--- ?\n/,"").gsub(/^([A-Z])/,"\n\\1")
    end

    # @return [Hash] The Hash representation of the Lockfile generated from
    # a given Podfile and the list of resolved Specifications.
    #
    def self.generate_hash_from_podfile(podfile, specs)
      hash = {}

      # Get list of [name, dependencies] pairs.
      pod_and_deps = specs.map do |spec|
        [spec.to_s, spec.dependencies.map(&:to_s).sort]
      end.uniq

      # Merge dependencies of iOS and OS X version of the same pod.
      tmp = {}
      pod_and_deps.each do |name, deps|
        if tmp[name]
          tmp[name].concat(deps).uniq!
        else
          tmp[name] = deps
        end
      end
      pod_and_deps = tmp.sort_by(&:first).map do |name, deps|
        deps.empty? ? name : { name => deps }
      end
      hash["PODS"] = pod_and_deps

      hash["DEPENDENCIES"] = podfile.dependencies.map{ |d| d.to_s }.sort

      external_sources = {}
      deps = podfile.dependencies.select(&:external?).sort{ |d, other| d.name <=> other.name}
      deps.each{ |d| external_sources[d.name] = d.external_source.params }
      hash["EXTERNAL SOURCES"] = external_sources unless external_sources.empty?

      checksums = {}
      specs.select { |spec| !spec.defined_in_file.nil? }.each do |spec|
        checksum = Digest::SHA1.hexdigest(File.read(spec.defined_in_file))
        checksum = checksum.encode('UTF-8') if checksum.respond_to?(:encode)
        checksums[spec.name] = checksum
      end
      hash["SPEC CHECKSUMS"] = checksums unless checksums.empty?
      hash["COCOAPODS"] = VERSION
      hash
    end
  end
end

Something went wrong with that request. Please try again.