lib/cocoapods-core/lockfile.rb
module Pod
# The Lockfile stores information about the pods that were installed by
# CocoaPods.
#
# It is used in combination with the Podfile to resolve the exact version of
# the Pods that should be installed (i.e. to prevent `pod install` from
# upgrading dependencies).
#
# Moreover it is used as a manifest of an installation to detect which Pods
# need to be installed or removed.
#
class Lockfile
# @todo The symbols should be converted to a String and back to symbol
# when reading (EXTERNAL SOURCES Download options)
# @return [String] the hash used to initialize the Lockfile.
#
attr_reader :internal_data
# @param [Hash] hash
# a hash representation of the Lockfile.
#
def initialize(hash)
@internal_data = hash
end
# Loads a lockfile form the given path.
#
# @note This method returns nil if the given path doesn't exists.
#
# @raise If there is a syntax error loading the YAML data.
#
# @param [Pathname] path
# the path where the lockfile is serialized.
#
# @return [Lockfile] a new lockfile.
#
def self.from_file(path)
return nil unless path.exist?
hash = YAMLHelper.load_file(path)
unless hash && hash.is_a?(Hash)
raise Informative, "Invalid Lockfile in `#{path}`"
end
lockfile = Lockfile.new(hash)
lockfile.defined_in_file = path
lockfile
end
# @return [String] the file where the Lockfile is serialized.
#
attr_accessor :defined_in_file
# @return [Boolean] Whether the Podfiles are equal.
#
def ==(other)
other && to_hash == other.to_hash
end
# @return [String] a string representation suitable for debugging.
#
def inspect
"#<#{self.class}>"
end
#-------------------------------------------------------------------------#
# !@group Accessing the Data
public
# @return [Array<String>] the names of the installed Pods.
#
def pod_names
generate_pod_names_and_versions unless @pod_names
@pod_names
end
# Returns the version of the given Pod.
#
# @param [String] pod_name The name of the Pod (root name of the specification).
#
# @return [Version] The version of the pod.
#
# @return [Nil] If there is no version stored for the given name.
#
def version(pod_name)
version = pod_versions[pod_name]
return version if version
root_name = pod_versions.keys.find do |name|
Specification.root_name(name) == pod_name
end
pod_versions[root_name]
end
# Returns the source of the given Pod.
#
# @param [String] pod_name The name of the Pod (root name of the specification).
#
# @return [String] The source of the pod.
#
# @return [Nil] If there is no source stored for the given name.
#
def spec_repo(pod_name)
spec_repos_by_pod[pod_name]
end
# Returns the checksum for the given Pod.
#
# @param [String] name The name of the Pod (root name of the specification).
#
# @return [String] The checksum of the specification for the given Pod.
#
# @return [Nil] If there is no checksum stored for the given name.
#
def checksum(name)
checksum_data[name]
end
# @return [Array<Dependency>] the dependencies of the Podfile used for the
# last installation.
#
# @note It includes only the dependencies explicitly required in the
# podfile and not those triggered by the Resolver.
def dependencies
unless @dependencies
data = internal_data['DEPENDENCIES'] || []
@dependencies = data.map do |string|
dep = Dependency.from_string(string)
dep.external_source = external_sources_data[dep.root_name]
dep
end
end
@dependencies
end
# Returns pod names grouped by the spec repo they were sourced from.
#
# @return [Hash<String, Array<String>>] A hash, where the keys are spec
# repo source URLs (or names), and the values are arrays of pod names.
#
# @note It does not include pods that come from "external sources".
#
def pods_by_spec_repo
@pods_by_spec_repo ||= internal_data['SPEC REPOS'] || {}
end
# Generates a dependency that requires the exact version of the Pod with the
# given name.
#
# @param [String] name
# the name of the Pod
#
# @note The generated dependencies used are by the Resolver from
# upgrading a Pod during an installation.
#
# @raise If there is no version stored for the given name.
#
# @return [Array<Dependency>] the generated dependency.
#
def dependencies_to_lock_pod_named(name)
deps = dependencies.select { |d| d.root_name == name }
if deps.empty?
raise StandardError, "Attempt to lock the `#{name}` Pod without a " \
'known dependency.'
end
deps.map do |dep|
version = version(dep.name)
locked_dependency = dep.dup
locked_dependency.specific_version = version
locked_dependency
end
end
# Returns the specific checkout options for the external source of the pod
# with the given name.
#
# @example Output
# {:commit => "919903db28535c3f387c4bbaa6a3feae4428e993"
# :git => "https://github.com/luisdelarosa/AFRaptureXMLRequestOperation.git"}
#
# @return [Hash] a hash of the checkout options for the external source of
# the pod with the given name.
#
# @param [String] name
# the name of the Pod.
#
def checkout_options_for_pod_named(name)
checkout_options_data[name]
end
# @return [Version] The version of CocoaPods which generated this lockfile.
#
def cocoapods_version
Version.new(internal_data['COCOAPODS'])
end
#--------------------------------------#
# !@group Accessing the internal data.
private
# @return [Array<String, Hash{String => Array[String]}>] the pods installed
# and their dependencies.
#
def generate_pod_names_and_versions
@pod_names = []
@pod_versions = {}
return unless pods = internal_data['PODS']
pods.each do |pod|
pod = pod.keys.first unless pod.is_a?(String)
name, version = Spec.name_and_version_from_string(pod)
@pod_names << name
@pod_versions[name] = version
end
end
# @return [Hash{String => Hash}] a hash where the name of the pods are the
# keys and the values are the external source hash the dependency
# that required the pod.
#
def external_sources_data
@external_sources_data ||= internal_data['EXTERNAL SOURCES'] || {}
end
# @return [Hash{String => Hash}] a hash where the name of the pods are the
# keys and the values are a hash of specific checkout options.
#
def checkout_options_data
@checkout_options_data ||= internal_data['CHECKOUT OPTIONS'] || {}
end
# @return [Hash{String => Version}] a Hash containing the name of the root
# specification of the installed Pods as the keys and their
# corresponding {Version} as the values.
#
def pod_versions
generate_pod_names_and_versions unless @pod_versions
@pod_versions
end
# @return [Hash{String => Version}] A Hash containing the checksums of the
# specification by the name of their root.
#
def checksum_data
internal_data['SPEC CHECKSUMS'] || {}
end
# @return [Hash{String => String}] A hash containing the spec repo used for the specification
# by the name of the root spec.
#
def spec_repos_by_pod
@spec_repos_by_pod ||= pods_by_spec_repo.each_with_object({}) do |(spec_repo, pods), spec_repos_by_pod|
pods.each do |pod|
spec_repos_by_pod[pod] = spec_repo
end
end
end
#-------------------------------------------------------------------------#
# !@group Comparison with a Podfile
public
# 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 external options.
# - removed: Pods that were removed form the Podfile.
# - unchanged: Pods that are still compatible with Podfile.
#
# @param [Podfile] podfile
# the podfile that should be analyzed.
#
# @return [Hash{Symbol=>Array[Strings]}] a hash where pods are grouped
# by the state in which they are.
#
# @todo Why do we look for compatibility instead of just comparing if the
# two dependencies are equal?
#
def detect_changes_with_podfile(podfile)
result = {}
[:added, :changed, :removed, :unchanged].each { |k| result[k] = [] }
installed_deps = {}
dependencies.each do |dep|
name = dep.root_name
installed_deps[name] ||= dependencies_to_lock_pod_named(name)
end
installed_deps = installed_deps.values.flatten(1).group_by(&:name)
podfile_dependencies = podfile.dependencies
podfile_dependencies_by_name = podfile_dependencies.group_by(&:name)
all_dep_names = (dependencies + podfile_dependencies).map(&:name).uniq
all_dep_names.each do |name|
installed_dep = installed_deps[name]
installed_dep &&= installed_dep.first
podfile_dep = podfile_dependencies_by_name[name]
podfile_dep &&= podfile_dep.first
if installed_dep.nil? then key = :added
elsif podfile_dep.nil? then key = :removed
elsif podfile_dep.compatible?(installed_dep) then key = :unchanged
else key = :changed
end
result[key] << name
end
result
end
#-------------------------------------------------------------------------#
# !@group Serialization
public
# Writes the Lockfile to the given path.
#
# @param [Pathname] path
# the path where the lockfile should be saved.
#
# @return [void]
#
def write_to_disk(path)
path.dirname.mkpath unless path.dirname.exist?
self.defined_in_file = path
# rubocop:disable Lint/RescueException
# rubocop:disable Lint/HandleExceptions
begin
existing = Lockfile.from_file(path)
return if existing == self
rescue Exception
end
path.open('w') { |f| f.write(to_yaml) }
# rubocop:enable Lint/HandleExceptions
# rubocop:enable Lint/RescueException
end
# @return [Hash{String=>Array,Hash,String}] a hash representation of the
# Lockfile.
#
# @example Output
#
# {
# 'PODS' => [ { BananaLib (1.0) => [monkey (< 1.0.9, ~> 1.0.1)] },
# "JSONKit (1.4)",
# "monkey (1.0.8)"]
# 'DEPENDENCIES' => [ "BananaLib (~> 1.0)",
# "JSONKit (from `path/JSONKit.podspec`)" ],
# 'EXTERNAL SOURCES' => { "JSONKit" => { :podspec => path/JSONKit.podspec } },
# 'SPEC CHECKSUMS' => { "BananaLib" => "439d9f683377ecf4a27de43e8cf3bce6be4df97b",
# "JSONKit", "92ae5f71b77c8dec0cd8d0744adab79d38560949" },
# 'PODFILE CHECKSUM' => "439d9f683377ecf4a27de43e8cf3bce6be4df97b",
# 'COCOAPODS' => "0.17.0"
# }
#
#
def to_hash
hash = {}
internal_data.each do |key, value|
hash[key] = value unless value.nil? || value.empty?
end
hash
end
# @return [Array<String>] The order in which the hash keys should appear in
# a serialized Lockfile.
#
HASH_KEY_ORDER = [
'PODS',
'DEPENDENCIES',
'SPEC REPOS',
'EXTERNAL SOURCES',
'CHECKOUT OPTIONS',
'SPEC CHECKSUMS',
'PODFILE CHECKSUM',
'COCOAPODS',
].map(&:freeze).freeze
# @return [String] the YAML representation of the Lockfile, used for
# serialization.
#
# @note Empty root keys are discarded.
#
# @note The YAML string is prettified.
#
def to_yaml
YAMLHelper.convert_hash(to_hash, HASH_KEY_ORDER, "\n\n")
end
#-------------------------------------------------------------------------#
class << self
# !@group Generation
public
# Generates a hash representation of the Lockfile generated from a given
# Podfile and the list of resolved Specifications. This representation is
# suitable for serialization.
#
# @param [Podfile] podfile
# the podfile that should be used to generate the lockfile.
#
# @param [Array<Specification>] specs
# an array containing the podspec that were generated by
# resolving the given podfile.
#
# @return [Lockfile] a new lockfile.
#
def generate(podfile, specs, checkout_options, spec_repos = {})
hash = {
'PODS' => generate_pods_data(specs),
'DEPENDENCIES' => generate_dependencies_data(podfile),
'SPEC REPOS' => generate_spec_repos(spec_repos),
'EXTERNAL SOURCES' => generate_external_sources_data(podfile),
'CHECKOUT OPTIONS' => checkout_options,
'SPEC CHECKSUMS' => generate_checksums(specs),
'PODFILE CHECKSUM' => podfile.checksum,
'COCOAPODS' => CORE_VERSION,
}
Lockfile.new(hash)
end
#--------------------------------------#
private
# !@group Private helpers
# Generates the list of the installed Pods and their dependencies.
#
# @note The dependencies of iOS and OS X version of the same pod are
# merged.
#
# @todo Specifications should be stored per platform, otherwise they
# list dependencies which actually might not be used.
#
# @return [Array<Hash,String>] the generated data.
#
# @example Output
# [ {"BananaLib (1.0)"=>["monkey (< 1.0.9, ~> 1.0.1)"]},
# "monkey (1.0.8)" ]
#
#
def generate_pods_data(specs)
pods_and_deps_merged = specs.reduce({}) do |result, spec|
name = spec.to_s
result[name] ||= []
result[name].concat(spec.all_dependencies.map(&:to_s))
result
end
pod_and_deps = pods_and_deps_merged.map do |name, deps|
deps.empty? ? name : { name => YAMLHelper.sorted_array(deps.uniq) }
end
YAMLHelper.sorted_array(pod_and_deps)
end
# Generates the list of the dependencies of the Podfile.
#
# @example Output
# [ "BananaLib (~> 1.0)",
# "JSONKit (from `path/JSONKit.podspec')" ]
#
# @return [Array] the generated data.
#
def generate_dependencies_data(podfile)
YAMLHelper.sorted_array(podfile.dependencies.map(&:to_s))
end
# Generates the hash of spec repo sources used in the Podfile.
#
# @example Output
# { "https://github.com/cocoapods/cocoapods.git" => ["Alamofire", "Moya"] }
#
def generate_spec_repos(spec_repos)
output = Hash.new {|h, k| h[k] = Array.new(0)}
spec_repos.each do |source, specs|
next unless source
next if specs.empty?
key = source.url || source.name
# save `trunk` as 'trunk' so that the URL itself can be changed without lockfile churn
key = Pod::TrunkSource::TRUNK_REPO_NAME if source.name == Pod::TrunkSource::TRUNK_REPO_NAME
value = specs.map { |s| s.root.name }
if output.has_key?(key)
value = value + output[key]
end
if value.length > 0
output[key] = YAMLHelper.sorted_array(value.uniq)
end
end
output.compact
end
# Generates the information of the external sources.
#
# @example Output
# { "JSONKit"=>{:podspec=>"path/JSONKit.podspec"} }
#
# @return [Hash] a hash where the keys are the names of the pods and
# the values store the external source hashes of each
# dependency.
#
def generate_external_sources_data(podfile)
deps = podfile.dependencies.select(&:external?)
deps = deps.sort { |d, other| d.name <=> other.name }
sources = {}
deps.each { |d| sources[d.root_name] = d.external_source }
sources
end
# Generates the relative to the checksum of the specifications.
#
# @example Output
# {
# "BananaLib"=>"9906b267592664126923875ce2c8d03824372c79",
# "JSONKit"=>"92ae5f71b77c8dec0cd8d0744adab79d38560949"
# }
#
# @return [Hash] a hash where the keys are the names of the root
# specifications and the values are the SHA1 digest of the
# podspec file.
#
def generate_checksums(specs)
checksums = {}
specs.select(&:defined_in_file).each do |spec|
checksums[spec.root.name] = spec.checksum
end
checksums
end
end
end
end