Files
CocoaPods/lib/cocoapods/resolver.rb

223 lines
7.8 KiB
Ruby

require 'colored'
module Pod
class Resolver
include Config::Mixin
# @return [Bool] Whether the resolver should find the pods to install or
# the pods to update.
#
attr_accessor :update_mode
# @return [Bool] Whether the resolver should update the external specs
# in the resolution process.
#
attr_accessor :update_external_specs
# @return [Podfile] The Podfile used by the resolver.
#
attr_reader :podfile
# @return [Lockfile] The Lockfile used by the resolver.
#
attr_reader :lockfile
# @return [Sandbox] The Sandbox used by the resolver to find external
# dependencies.
#
attr_reader :sandbox
# @return [Array<Strings>] The name of the pods that have an
# external source.
#
attr_reader :pods_from_external_sources
# @return [Array<Set>] A cache of the sets used to resolve the dependencies.
#
attr_reader :cached_sets
# @return [Source::Aggregate] A cache of the sources needed to find the
# podspecs.
#
attr_reader :cached_sources
# @return [Hash{Podfile::TargetDefinition => Array<Specification>}]
# Returns the resolved specifications grouped by target.
#
attr_reader :specs_by_target
def initialize(podfile, lockfile, sandbox)
@podfile = podfile
@lockfile = lockfile
@sandbox = sandbox
@update_external_specs = true
@cached_sets = {}
@cached_sources = Source::Aggregate.new
end
# Identifies the specifications that should be installed according whether
# the resolver is in update mode or not.
#
# @return [Hash{Podfile::TargetDefinition => Array<Specification>}] specs_by_target
#
def resolve
@cached_specs = {}
@specs_by_target = {}
@pods_from_external_sources = []
@pods_to_lock = []
if @lockfile
@pods_by_state = @lockfile.detect_changes_with_podfile(podfile)
UI.section "Finding added, modified or removed dependencies:" do
marks = {:added => "A".green, :changed => "M".yellow, :removed => "R".red, :unchanged => "-" }
@pods_by_state.each do |symbol, pod_names|
pod_names.each do |pod_name|
UI.message("#{marks[symbol]} #{pod_name}", '',2)
end
end
end if config.verbose?
@pods_to_lock = (lockfile.pods_names - @pods_by_state[:added] - @pods_by_state[:changed] - @pods_by_state[:removed]).uniq
end
unless config.skip_repo_update?
UI.section 'Updating spec repositories' do
Command::Repo.new(Command::ARGV.new(["update"])).run
end if !@lockfile || !(@pods_by_state[:added] + @pods_by_state[:changed]).empty? || update_mode
end
@podfile.target_definitions.values.each do |target_definition|
UI.section "Resolving dependencies for target `#{target_definition.name}' (#{target_definition.platform})" do
@loaded_specs = []
find_dependency_specs(@podfile, target_definition.dependencies, target_definition)
@specs_by_target[target_definition] = @cached_specs.values_at(*@loaded_specs).sort_by(&:name)
end
end
@cached_specs.values.sort_by(&:name)
@specs_by_target
end
# @return [Array<Specification>] The specifications loaded by the resolver.
#
def specs
@cached_specs.values.uniq
end
# @return [Bool] Whether a pod should be installed/reinstalled.
#
def should_install?(name)
pods_to_install.include? name
end
# @return [Array<Strings>] The name of the pods that should be
# installed/reinstalled.
#
def pods_to_install
unless @pods_to_install
if lockfile
@pods_to_install = specs.select do |spec|
spec.version != lockfile.pods_versions[spec.pod_name]
end.map(&:name)
if update_mode
@pods_to_install += specs.select do |spec|
spec.version.head? || pods_from_external_sources.include?(spec.pod_name)
end.map(&:name)
end
@pods_to_install += @pods_by_state[:added] + @pods_by_state[:changed]
else
@pods_to_install = specs.map(&:name)
end
end
@pods_to_install
end
# @return [Array<Strings>] The name of the pods that were installed
# but don't have any dependency anymore. The name of the Pods are
# stripped from subspecs.
#
def removed_pods
return [] unless lockfile
unless @removed_pods
previously_installed = lockfile.pods_names.map { |pod_name| pod_name.split('/').first }
installed = specs.map { |spec| spec.name.split('/').first }
@removed_pods = previously_installed - installed
end
@removed_pods
end
private
# @return [Set] The cached set for a given dependency.
#
def find_cached_set(dependency, platform)
set_name = dependency.name.split('/').first
@cached_sets[set_name] ||= begin
if dependency.specification
Specification::Set::External.new(dependency.specification)
elsif external_source = dependency.external_source
if update_mode && update_external_specs
# Always update external sources in update mode.
specification = external_source.specification_from_external(@sandbox, platform)
else
# Don't update external sources in install mode if not needed.
specification = external_source.specification_from_sandbox(@sandbox, platform)
end
set = Specification::Set::External.new(specification)
if dependency.subspec_dependency?
@cached_sets[dependency.top_level_spec_name] ||= set
end
set
else
@cached_sources.search(dependency)
end
end
end
# Resolves the dependencies of a specification and stores them in @cached_specs
#
# @param [Specification] dependent_specification
# @param [Array<Dependency>] dependencies
# @param [TargetDefinition] target_definition
#
# @return [void]
#
def find_dependency_specs(dependent_specification, dependencies, target_definition)
dependencies.each do |dependency|
# Replace the dependency with a more specific one if the pod is already installed.
if !update_mode && @pods_to_lock.include?(dependency.name)
dependency = lockfile.dependency_for_installed_pod_named(dependency.name)
end
UI.message("- #{dependency}", '', 2) do
set = find_cached_set(dependency, target_definition.platform)
set.required_by(dependency, dependent_specification.to_s)
# Ensure we don't resolve the same spec twice for one target
unless @loaded_specs.include?(dependency.name)
spec = set.specification_by_name(dependency.name)
@pods_from_external_sources << spec.pod_name if dependency.external?
@loaded_specs << spec.name
@cached_specs[spec.name] = spec
# Configure the specification
spec.activate_platform(target_definition.platform)
spec.version.head = dependency.head?
# And recursively load the dependencies of the spec.
find_dependency_specs(spec, spec.dependencies, target_definition) if spec.dependencies
end
validate_platform(spec || @cached_specs[dependency.name], target_definition)
end
end
end
# Ensures that a spec is compatible with the platform of a target.
#
# @raises If the spec is not supported by the target.
#
def validate_platform(spec, target)
unless spec.available_platforms.any? { |platform| target.platform.supports?(platform) }
raise Informative, "[!] The platform of the target `#{target.name}' (#{target.platform}) is not compatible with `#{spec}' which has a minimun requirement of #{spec.available_platforms.join(' - ')}.".red
end
end
end
end