Commit 0a6e8a17 authored by Samuel E. Giddins's avatar Samuel E. Giddins

Merge pull request #2637 from CocoaPods/resolver

Molinillo
parents 28b95e88 d0d72ea7
......@@ -4,7 +4,13 @@ To install or update CocoaPods see this [guide](http://docs.cocoapods.org/guides
## Master
##### Enhacements
##### Breaking
* Attempts to resolve circular dependencies will now raise an exception.
[Samuel Giddins](https://github.com/segiddins)
[Molinillo#6](https://github.com/CocoaPods/Molinillo/issues/6)
##### Enhancements
* The `pod push` has been removed as it has been deprecated in favour of `pod
repo push` in CocoaPods 0.33.
......@@ -15,6 +21,26 @@ To install or update CocoaPods see this [guide](http://docs.cocoapods.org/guides
[Marius Rackwitz](https://github.com/mrackwitz)
[#2461](https://github.com/CocoaPods/CocoaPods/issues/2461)
* The `Resolver` has been completely rewritten to use
[Molinillo](https://github.com/CocoaPods/Molinillo), an iterative dependency
resolution algorithm that automatically resolves version conflicts.
The order in which dependencies are declared in the `Podfile` no longer has
any effect on the resolution process.
[Samuel Giddins](https://github.com/segiddins)
[#978](https://github.com/CocoaPods/CocoaPods/issues/978)
[#2002](https://github.com/CocoaPods/CocoaPods/issues/2002)
* Implicit dependencies are now locked, so simply running `pod install` will not
cause them to be updated when they shouldn't be.
[Samuel Giddins](https://github.com/segiddins)
[#2318](https://github.com/CocoaPods/CocoaPods/issues/2318)
[#2506](https://github.com/CocoaPods/CocoaPods/issues/2506)
* Pre-release versions are only considered in the resolution process when there
are dependencies that explicitly reference pre-release requirements.
[Samuel Giddins](https://github.com/segiddins)
[#1489](https://github.com/CocoaPods/CocoaPods/issues/1489)
##### Bug Fixes
* Do not try to clone spec-repos in `/`.
......
......@@ -12,12 +12,13 @@ gemspec
group :development do
cp_gem 'claide', 'CLAide'
cp_gem 'cocoapods-core', 'Core'
cp_gem 'cocoapods-core', 'Core', 'resolver'
cp_gem 'cocoapods-downloader', 'cocoapods-downloader'
cp_gem 'cocoapods-plugins', 'cocoapods-plugins'
cp_gem 'cocoapods-trunk', 'cocoapods-trunk'
cp_gem 'cocoapods-try', 'cocoapods-try'
cp_gem 'xcodeproj', 'Xcodeproj'
cp_gem 'molinillo', 'Molinillo'
gem 'bacon'
gem 'mocha'
......
......@@ -7,8 +7,8 @@ GIT
GIT
remote: https://github.com/CocoaPods/Core.git
revision: 99748b5b9fa8fe6541218f1752c0379589c7a6ee
branch: master
revision: 802401562948abf9f4f417ace4cd8e9bb8b08859
branch: resolver
specs:
cocoapods-core (0.34.4)
activesupport (>= 3.2.15)
......@@ -16,6 +16,13 @@ GIT
json_pure (~> 1.8)
nap (~> 0.8.0)
GIT
remote: https://github.com/CocoaPods/Molinillo.git
revision: 3e27234148022ba1455a9228e7b710314204d4c5
branch: master
specs:
molinillo (0.0.1)
GIT
remote: https://github.com/CocoaPods/Xcodeproj.git
revision: ff7c70da5f5082eef47bb248012b69c76f6c6e54
......@@ -71,6 +78,7 @@ PATH
colored (~> 1.2)
escape (~> 0.0.4)
json_pure (~> 1.8)
molinillo (~> 0)
nap (~> 0.8)
open4 (~> 1.3)
xcodeproj (~> 0.19.4)
......@@ -174,6 +182,7 @@ DEPENDENCIES
mime-types (< 2.0)
mocha
mocha-on-bacon
molinillo!
prettybacon
pry
rake
......
......@@ -34,6 +34,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'cocoapods-plugins', '~> 0.3.1'
s.add_runtime_dependency 'cocoapods-try', '~> 0.4.1'
s.add_runtime_dependency 'cocoapods-trunk', '~> 0.3.1'
s.add_runtime_dependency 'molinillo', '~> 0'
s.add_runtime_dependency 'colored', '~> 1.2'
s.add_runtime_dependency 'escape', '~> 0.0.4'
......
require 'colored'
require 'claide'
require 'molinillo/errors'
module Molinillo
class ResolverError
include CLAide::InformativeError
end
end
module Pod
class PlainInformative
......
......@@ -8,6 +8,8 @@ module Pod
autoload :SandboxAnalyzer, 'cocoapods/installer/analyzer/sandbox_analyzer'
autoload :LockingDependencyAnalyzer, 'cocoapods/installer/analyzer/locking_dependency_analyzer'
# @return [Sandbox] The sandbox where the Pods should be installed.
#
attr_reader :sandbox
......@@ -149,17 +151,13 @@ module Pod
pods_state = nil
UI.section 'Finding Podfile changes' do
pods_by_state = lockfile.detect_changes_with_podfile(podfile)
pods_by_state.dup.each do |state, full_names|
pods = full_names.map { |fn| Specification.root_name(fn) }.uniq
pods_by_state[state] = pods
end
pods_state = SpecsState.new(pods_by_state)
pods_state.print
end
pods_state
else
state = SpecsState.new
state.added.concat(podfile.dependencies.map(&:root_name).uniq)
state.added.concat(podfile.dependencies.map(&:name).uniq)
state
end
end
......@@ -231,21 +229,17 @@ module Pod
# is in update mode, to prevent it from upgrading the Pods that weren't
# changed in the {Podfile}.
#
# @return [Array<Dependency>] the dependencies generate by the lockfile
# that prevent the resolver to update a Pod.
# @return [Molinillo::DependencyGraph<Dependency>] the dependencies
# generated by the lockfile that prevent the resolver to update
# a Pod.
#
def generate_version_locking_dependencies
if update_mode == :all
[]
if update_mode == :all || !lockfile
LockingDependencyAnalyzer.unlocked_dependency_graph
else
locking_pods = result.podfile_state.unchanged
if update_mode == :selected
# If selected Pods should been updated, filter them out of the list
locking_pods = locking_pods.select { |pod| !update[:pods].include?(pod) }
end
locking_pods.map do |pod|
lockfile.dependencies_to_lock_pod_named(pod)
end.flatten
pods_to_update = result.podfile_state.changed + result.podfile_state.deleted
pods_to_update += update[:pods] if update_mode == :selected
LockingDependencyAnalyzer.generate_version_locking_dependencies(lockfile, pods_to_update)
end
end
......@@ -281,9 +275,9 @@ module Pod
if update_mode == :selected
pods_to_fetch += update[:pods]
end
deps_to_fetch = deps_with_external_source.select { |dep| pods_to_fetch.include?(dep.root_name) }
deps_to_fetch_if_needed = deps_with_external_source.select { |dep| result.podfile_state.unchanged.include?(dep.root_name) }
deps_to_fetch += deps_to_fetch_if_needed.select { |dep| sandbox.specification(dep.root_name).nil? || !dep.external_source[:local].nil? || !dep.external_source[:path].nil? || !sandbox.pod_dir(dep.root_name).directory? }
deps_to_fetch = deps_with_external_source.select { |dep| pods_to_fetch.include?(dep.name) }
deps_to_fetch_if_needed = deps_with_external_source.select { |dep| result.podfile_state.unchanged.include?(dep.name) }
deps_to_fetch += deps_to_fetch_if_needed.select { |dep| sandbox.specification(dep.name).nil? || !dep.external_source[:local].nil? || !dep.external_source[:path].nil? || !sandbox.pod_dir(dep.name).directory? }
end
unless deps_to_fetch.empty?
......@@ -352,7 +346,7 @@ module Pod
# @!group Analysis internal products
# @return [Array<Dependency>] the dependencies generate by the lockfile
# @return [Molinillo::DependencyGraph<Dependency>] the dependencies generated by the lockfile
# that prevent the resolver to update a Pod.
#
attr_reader :locked_dependencies
......@@ -604,7 +598,7 @@ module Pod
#
class SpecsState
# @param [Hash{Symbol=>String}] pods_by_state
# The **root** name of the pods grouped by their state
# The name of the pods grouped by their state
# (`:added`, `:removed`, `:changed` or `:unchanged`).
#
def initialize(pods_by_state = nil)
......@@ -656,12 +650,9 @@ module Pod
# @param [Symbol]
# the state of the Pod.
#
# @raise If there is an attempt to add the name of a subspec.
#
# @return [void]
#
def add_name(name, state)
raise '[Bug] Attempt to add subspec to the pods state' if name.include?('/')
send(state) << name
end
end
......
require 'molinillo/dependency_graph'
module Pod
class Installer
class Analyzer
# Generates dependencies that require the specific version of the Pods
# that haven't changed in the {Lockfile}.
module LockingDependencyAnalyzer
# Generates dependencies that require the specific version of the Pods
# that haven't changed in the {Lockfile}.
#
# These dependencies are passed to the {Resolver}, unless the installer
# is in update mode, to prevent it from upgrading the Pods that weren't
# changed in the {Podfile}.
#
# @return [Molinillo::DependencyGraph<Dependency>] the dependencies
# generated by the lockfile that prevent the resolver to update
# a Pod.
#
def self.generate_version_locking_dependencies(lockfile, pods_to_update)
dependency_graph = Molinillo::DependencyGraph.new
if lockfile
explicit_dependencies = lockfile.to_hash['DEPENDENCIES'] || []
explicit_dependencies.each do |string|
dependency = Dependency.new(string)
dependency_graph.add_root_vertex(dependency.name, nil)
end
pods = lockfile.to_hash['PODS'] || []
pods.each do |pod|
add_to_dependency_graph(pod, [], dependency_graph)
end
pods_to_update.each { |u| dependency_graph.detach_vertex_named(u) }
end
dependency_graph
end
# Generates a completely 'unlocked' dependency graph.
#
# @return [Molinillo::DependencyGraph<Dependency>] an empty dependency
# graph
#
def self.unlocked_dependency_graph
Molinillo::DependencyGraph.new
end
private
def self.add_child_vertex_to_graph(dependency_string, parents, dependency_graph)
dependency = Dependency.from_string(dependency_string)
dependency_graph.add_child_vertex(dependency.name, parents.empty? ? dependency : nil, parents, nil)
dependency
end
def self.add_to_dependency_graph(object, parents, dependency_graph)
case object
when String
add_child_vertex_to_graph(object, parents, dependency_graph)
when Hash
object.each do |key, value|
dependency = add_child_vertex_to_graph(key, parents, dependency_graph)
value.each { |v| add_to_dependency_graph(v, [dependency.name], dependency_graph) }
end
end
end
end
end
end
end
require 'molinillo'
require 'cocoapods/resolver/lazy_specification'
module Pod
# The resolver is responsible of generating a list of specifications grouped
# by target for a given Podfile.
#
# @todo Its current implementation is naive, in the sense that it can't do full
# automatic resolves like Bundler:
# [how-does-bundler-bundle](http://patshaughnessy.net/2011/9/24/how-does-bundler-bundle)
#
# @todo Another limitation is that the order of the dependencies matter. The
# current implementation could create issues, for example, if a
# specification is loaded for a target definition and later for another
# target is set in head mode. The first specification will not be in head
# mode.
#
#
class Resolver
# @return [Sandbox] the Sandbox used by the resolver to find external
# dependencies.
......@@ -58,23 +50,12 @@ module Pod
# definition.
#
def resolve
@cached_sets = {}
@cached_specs = {}
@specs_by_target = {}
target_definitions = podfile.target_definition_list
target_definitions.each do |target|
title = "Resolving dependencies for target `#{target.name}' " \
"(#{target.platform})"
UI.section(title) do
@loaded_specs = []
find_dependency_specs(podfile, target.dependencies, target)
specs = cached_specs.values_at(*@loaded_specs).sort_by(&:name)
specs_by_target[target] = specs
end
end
dependencies = @podfile.target_definition_list.map(&:dependencies).flatten
@cached_sets = {}
@activated = Molinillo::Resolver.new(self, self).resolve(dependencies, locked_dependencies)
specs_by_target
rescue Molinillo::ResolverError => e
raise Informative, e.message
end
# @return [Hash{Podfile::TargetDefinition => Array<Specification>}]
......@@ -82,113 +63,235 @@ module Pod
#
# @note The returned specifications can be subspecs.
#
attr_reader :specs_by_target
def specs_by_target
@specs_by_target ||= begin
specs_by_target = {}
podfile.target_definition_list.each do |target|
specs = target.dependencies.map(&:name).map do |name|
node = @activated.vertex_named(name)
(node.recursive_successors << node).to_a
end
specs_by_target[target] = specs.
flatten.
map(&:payload).
uniq.
sort_by(&:name).
each do |spec|
validate_platform(spec, target)
sandbox.store_head_pod(spec.name) if spec.version.head
end
end
specs_by_target
end
end
#-------------------------------------------------------------------------#
private
public
# !@ Resolution context
# @!group Specification Provider
# @return [Hash<String => Set>] A cache that keeps tracks of the sets
# loaded by the resolution process.
include Molinillo::SpecificationProvider
# Returns (and caches) the specification that satisfy the given dependency.
#
# @note Sets store the resolved dependencies and return the highest
# available specification found in the sources. This is done
# globally and not per target definition because there can be just
# one Pod installation, so different version of the same Pods for
# target definitions are not allowed.
# @return [Array<Specification>] the specifications that satisfy the given
# `dependency`.
#
attr_accessor :cached_sets
# @param [Dependency] dependency the dependency that is being searched for.
#
def search_for(dependency)
@search ||= {}
@search[dependency] ||= begin
specs = find_cached_set(dependency).
all_specifications.
select { |s| dependency.requirement.satisfied_by? s.version }.
map { |s| s.subspec_by_name(dependency.name, false) }
# @return [Hash<String => Specification>] The loaded specifications grouped
# by name.
specs.
reverse.
each { |s| s.version.head = dependency.head? }
end
@search[dependency].dup
end
# Returns the dependencies of `specification`.
#
# @return [Array<Specification>] all dependencies of `specification`.
#
# @param [Specification] specification the specification whose own
# dependencies are being asked for.
#
attr_accessor :cached_specs
def dependencies_for(specification)
specification.all_dependencies.map do |dependency|
if dependency.root_name == Specification.root_name(specification.name)
Dependency.new(dependency.name, specification.version)
else
dependency
end
end
end
#-------------------------------------------------------------------------#
# Returns the name for the given `dependency`.
#
# @return [String] the name for the given `dependency`.
#
# @param [Dependency] dependency the dependency whose name is being
# queried.
#
def name_for(dependency)
dependency.name
end
private
# @return [String] the user-facing name for a {Podfile}.
#
def name_for_explicit_dependency_source
'Podfile'
end
# @!group Private helpers
# @return [String] the user-facing name for a {Lockfile}.
#
def name_for_locking_dependency_source
'Podfile.lock'
end
# Resolves recursively the dependencies of a specification and stores them
# in the @cached_specs ivar.
# Determines whether the given `requirement` is satisfied by the given
# `spec`, in the context of the current `activated` dependency graph.
#
# @param [Podfile, Specification, #to_s] dependent_spec
# the specification whose dependencies are being resolved. Used
# only for UI purposes.
# @return [Boolean] whether `requirement` is satisfied by `spec` in the
# context of the current `activated` dependency graph.
#
# @param [Array<Dependency>] dependencies
# the dependencies of the specification.
# @param [Dependency] requirement the dependency in question.
#
# @param [TargetDefinition] target_definition
# the target definition that owns the specification.
# @param [Molinillo::DependencyGraph] activated the current dependency
# graph in the resolution process.
#
# @note If there is a locked dependency with the same name of a
# given dependency the locked one is used in place of the
# dependency of the specification. In this way it is possible to
# prevent the update of the version of installed pods not changed
# in the Podfile.
# @param [Specification] spec the specification in question.
#
# @note The recursive process checks if a dependency has already been
# loaded to prevent an infinite loop.
def requirement_satisfied_by?(requirement, activated, spec)
existing_vertices = activated.vertices.values.select do |v|
Specification.root_name(v.name) == requirement.root_name
end
existing = existing_vertices.map(&:payload).compact.first
requirement_satisfied =
if existing
existing.version == spec.version && requirement.requirement.satisfied_by?(spec.version)
else
requirement.requirement.satisfied_by? spec.version
end
requirement_satisfied && !(spec.version.prerelease? && existing_vertices.flat_map(&:requirements).none?(&:prerelease?))
end
# Sort dependencies so that the ones that are easiest to resolve are first.
# Easiest to resolve is (usually) defined by:
# 1) Is this dependency already activated?
# 2) How relaxed are the requirements?
# 3) Are there any conflicts for this dependency?
# 4) How many possibilities are there to satisfy this dependency?
#
# @note The set class merges all (of all the target definitions) the
# dependencies and thus it keeps track of whether it is in head
# mode or from an external source because {Dependency#merge}
# preserves this information.
# @return [Array<Dependency>] the sorted dependencies.
#
# @return [void]
# @param [Array<Dependency>] dependencies the unsorted dependencies.
#
def find_dependency_specs(dependent_spec, dependencies, target_definition)
dependencies.each do |dependency|
locked_dep = locked_dependencies.find { |ld| ld.name == dependency.name }
dependency = locked_dep if locked_dep
# @param [Molinillo::DependencyGraph] activated the dependency graph of
# currently activated specs.
#
# @param [{String => Array<Conflict>}] conflicts the current conflicts.
#
def sort_dependencies(dependencies, activated, conflicts)
dependencies.sort_by do |dependency|
name = name_for(dependency)
[
activated.vertex_named(name).payload ? 0 : 1,
dependency.prerelease? ? 0 : 1,
conflicts[name] ? 0 : 1,
search_for(dependency).count,
]
end
end
UI.message("- #{dependency}", '', 2) do
set = find_cached_set(dependency, dependent_spec)
set.required_by(dependency, dependent_spec.to_s)
#-------------------------------------------------------------------------#
if (paths = set.specification_paths_for_version(set.required_version)).length > 1
UI.warn "Found multiple specifications for #{dependency}:\n" \
"- #{paths.join("\n")}"
end
public
unless @loaded_specs.include?(dependency.name)
spec = set.specification.subspec_by_name(dependency.name)
@loaded_specs << spec.name
cached_specs[spec.name] = spec
validate_platform(spec, target_definition)
if dependency.head? || sandbox.head_pod?(spec.name)
spec.version.head = true
sandbox.store_head_pod(spec.name)
end
# @!group Resolver UI
spec_dependencies = spec.all_dependencies(target_definition.platform)
find_dependency_specs(spec, spec_dependencies, target_definition)
end
end
end
include Molinillo::UI
include Config::Mixin
# The UI object the resolver should use for displaying user-facing output.
#
# @return [UserInterface] the normal CocoaPods UI object.
#
def output
UI
end
# Called before resolution starts. We print out `Resolving dependencies` in
# the analyzer, so here we just want to print out a starting `.` in verbose
# mode.
#
# @return [Void]
#
def before_resolution
UI.print '.' if config.verbose
end
# Called after resolution ends. We don't want to {#indicate_progress}
# unless in verbose mode, so we only use the default implementation then.
#
# @return [Void]
#
def after_resolution
super if config.verbose
end
# Called during resolution to indicate progress.
# We only use the default implementation in verbose mode.
#
# @return [Void]
#
def indicate_progress
super if config.verbose
end
#-------------------------------------------------------------------------#
private
# !@ Resolution context
# @return [Hash<String => Set>] A cache that keeps tracks of the sets
# loaded by the resolution process.
#
# @note Sets store the resolved dependencies and return the highest
# available specification found in the sources. This is done
# globally and not per target definition because there can be just
# one Pod installation, so different version of the same Pods for
# target definitions are not allowed.
#
attr_accessor :cached_sets
#-------------------------------------------------------------------------#
private
# @!group Private helpers
# @return [Set] Loads or returns a previously initialized set for the Pod
# of the given dependency.
#
# @param [Dependency] dependency
# The dependency for which the set is needed.
#
# @param [#to_s] dependent_spec
# the specification whose dependencies are being resolved. Used
# only for UI purposes.
#
# @return [Set] the cached set for a given dependency.
#
def find_cached_set(dependency, dependent_spec)
def find_cached_set(dependency)
name = dependency.root_name
unless cached_sets[name]
if dependency.external_source
spec = sandbox.specification(dependency.root_name)
spec = sandbox.specification(name)
unless spec
raise StandardError, '[Bug] Unable to find the specification ' \
"for `#{dependency}`."
......@@ -199,8 +302,7 @@ module Pod
end
cached_sets[name] = set
unless set
raise Informative, 'Unable to find a specification for ' \
"`#{dependency}` depended upon by #{dependent_spec}."
raise Molinillo::NoSuchDependencyError.new(dependency) # rubocop:disable Style/RaiseArgs
end
end
cached_sets[name]
......
module Pod
class Specification
class Set
class LazySpecification < BasicObject
attr_reader :name, :version, :source
def initialize(name, version, source)
@name = name
@version = version
@source = source
end
def method_missing(method, *args, &block)
specification.send(method, *args, &block)
end
def subspec_by_name(name = nil, raise_if_missing = true)
if !name || name == self.name
self
else
specification.subspec_by_name(name, raise_if_missing)
end
end
def specification
@specification ||= source.specification(name, version)
end
end
class External
def all_specifications
[specification]
end
end
def all_specifications
@all_specifications ||= begin
sources_by_version = {}
versions_by_source.each do |source, versions|
versions.each { |v| (sources_by_version[v] ||= []) << source }
sources_by_version
end
duplicate_versions = sources_by_version.select { |_version, sources| sources.count > 1 }
duplicate_versions.each do |version, sources|
UI.warn "Found multiple specifications for `#{name} (#{version})`:\n" +
sources.
map { |s| s.specification_path(name, version) }.
map { |v| "- #{v}" }.join("\n")
end
versions_by_source.map do |source, versions|
versions.map { |version| LazySpecification.new(name, version, source) }
end.flatten
end
end
end
end
end
......@@ -219,6 +219,7 @@ module Pod
# @return [Nil] if the podspec is not stored.
#
def specification_path(name)
name = Specification.root_name(name)
path = specifications_root + "#{name}.podspec"
if path.exist?
path
......@@ -378,7 +379,7 @@ module Pod
end
# @return [Hash{String=>String}] The path of the Pods with a local source
# grouped by their name.
# grouped by their root name.
#
# @todo Rename (e.g. `pods_with_local_path`)
#
......
Subproject commit ae87cdaa7f0c96ad246ddd804a08bc4260018548
Subproject commit afc3bdb824e79fe2f99361aa9b4525cda4cc95d1
Subproject commit b0f4e85738f1f911e32e2914b39e9acdce7c7ed9
Subproject commit b6e1c2429964b4e47b344958572930e15ffbbcea
......@@ -226,13 +226,13 @@ module Pod
it 'cats the given podspec' do
lambda { command('spec', 'cat', 'AFNetworking').run }.should.not.raise
UI.output.should.include fixture('spec-repos/master/Specs/AFNetworking/2.3.1/AFNetworking.podspec.json').read
UI.output.should.include fixture('spec-repos/master/Specs/AFNetworking/2.4.1/AFNetworking.podspec.json').read
end
it 'cats the first podspec from all podspecs' do
UI.next_input = "1\n"
run_command('spec', 'cat', '--show-all', 'AFNetworking')
UI.output.should.include fixture('spec-repos/master/Specs/AFNetworking/2.3.1/AFNetworking.podspec.json').read
UI.output.should.include fixture('spec-repos/master/Specs/AFNetworking/2.4.1/AFNetworking.podspec.json').read
end
end
......@@ -292,7 +292,7 @@ module Pod
it 'returns the path of the specification with the given name' do
path = @sut.send(:get_path_of_spec, 'AFNetworking')
path.should == fixture('spec-repos') + 'master/Specs/AFNetworking/2.3.1/AFNetworking.podspec.json'
path.should == fixture('spec-repos') + 'master/Specs/AFNetworking/2.4.1/AFNetworking.podspec.json'
end
end
......
......@@ -48,7 +48,7 @@ module Pod
output = UI.output
output.should.include? 'Author: Robbie Hanson'
output.should.include? 'License: BSD'
output.should.include? 'Platform: iOS - OS X'
output.should.include? 'Platform: iOS 5.0 - OS X 10.7'
output.should.include? 'Watchers: 318'
output.should.include? 'Forks: 42'
output.should.include? 'Pushed: more than a year ago'
......
......@@ -13,7 +13,7 @@ def create_analyzer
end
hash = {}
hash['PODS'] = ['JSONKit (1.4)', 'NUI (0.2.0)', 'SVPullToRefresh (0.4)']
hash['PODS'] = ['JSONKit (1.5pre)', 'NUI (0.2.0)', 'SVPullToRefresh (0.4)']
hash['DEPENDENCIES'] = %w(JSONKit NUI SVPullToRefresh)
hash['SPEC CHECKSUMS'] = {}
hash['COCOAPODS'] = Pod::VERSION
......@@ -52,10 +52,10 @@ module Pod
it 'computes the state of the Podfile respect to the Lockfile' do
state = @analyzer.analyze.podfile_state
state.added.should == %w(AFNetworking libextobjc)
state.changed.should == ['JSONKit']
state.unchanged.should == ['SVPullToRefresh']
state.deleted.should == ['NUI']
state.added.should == %w(AFNetworking libextobjc/EXTKeyPathCoding)
state.changed.should == %w()
state.unchanged.should == %w(JSONKit SVPullToRefresh)
state.deleted.should == %w(NUI)
end
#--------------------------------------#
......@@ -131,13 +131,35 @@ module Pod
it 'locks the version of the dependencies which did not change in the Podfile' do
@analyzer.analyze
@analyzer.send(:locked_dependencies).map(&:to_s).should == ['SVPullToRefresh (= 0.4)']
@analyzer.send(:locked_dependencies).map(&:payload).map(&:to_s).
should == ['JSONKit (= 1.5pre)', 'SVPullToRefresh (= 0.4)']
end
it 'does not lock the dependencies in update mode' do
@analyzer.update = true
@analyzer.analyze
@analyzer.send(:locked_dependencies).map(&:to_s).should == []
@analyzer.send(:locked_dependencies).to_a.map(&:payload).should == []
end
#--------------------------------------#
it 'takes into account locked implicit dependencies' do
podfile = Podfile.new do
platform :ios, '8.0'
xcodeproj 'SampleProject/SampleProject'
pod 'ARAnalytics/Mixpanel'
end
hash = {}
hash['PODS'] = ['ARAnalytics/CoreIOS (2.8.0)', {'ARAnalytics/Mixpanel (2.8.0)' => ['ARAnlytics/CoreIOS', 'Mixpanel']}, 'Mixpanel (2.5.1)']
hash['DEPENDENCIES'] = %w(ARAnalytics/Mixpanel)
hash['SPEC CHECKSUMS'] = {}
hash['COCOAPODS'] = Pod::VERSION
lockfile = Pod::Lockfile.new(hash)
analyzer = Installer::Analyzer.new(config.sandbox, podfile, lockfile)
analyzer.analyze.specifications.
find { |s| s.name == 'Mixpanel' }.
version.to_s.should == '2.5.1'
end
#--------------------------------------#
......
require File.expand_path('../../spec_helper', __FILE__)
def dependency_graph_from_array(locked_dependencies)
locked_dependencies.reduce(Molinillo::DependencyGraph.new) do |graph, dep|
graph.add_root_vertex(dep.name, dep)
graph
end
end
def empty_graph
Molinillo::DependencyGraph.new
end
module Pod
describe Resolver do
describe 'In general' do
......@@ -8,7 +19,7 @@ module Pod
platform :ios
pod 'BlocksKit', '1.5.2'
end
locked_deps = [Dependency.new('BlocksKit', '1.5.2')]
locked_deps = dependency_graph_from_array([Dependency.new('BlocksKit', '1.5.2')])
@resolver = Resolver.new(config.sandbox, @podfile, locked_deps, SourcesManager.all)
end
......@@ -21,7 +32,8 @@ module Pod
end
it 'returns the locked dependencies' do
@resolver.locked_dependencies.should == [Dependency.new('BlocksKit', '1.5.2')]
@resolver.locked_dependencies.
should == dependency_graph_from_array([Dependency.new('BlocksKit', '1.5.2')])
end
#--------------------------------------#
......@@ -55,11 +67,20 @@ module Pod
platform :ios
pod 'Reachability', :podspec => podspec
end
resolver = Resolver.new(config.sandbox, podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, podfile, empty_graph, SourcesManager.all)
resolver.resolve
specs = resolver.specs_by_target.values.flatten
specs.map(&:to_s).should == ['Reachability (3.0.0)']
end
it 'resolves an empty podfile' do
@podfile = Podfile.new do
platform :ios
end
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should == []
end
end
#-------------------------------------------------------------------------#
......@@ -70,7 +91,7 @@ module Pod
platform :ios, '6.0'
pod 'BlocksKit', '1.5.2'
end
@resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
@resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
end
it 'cross resolves dependencies' do
......@@ -80,11 +101,53 @@ module Pod
pod 'AFQuickLookView', '= 0.1.0' # requires 'AFNetworking', '>= 0.9.0'
end
resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should == ['AFNetworking (0.9.1)', 'AFQuickLookView (0.1.0)']
end
it 'resolves basic conflicts' do
@podfile = Podfile.new do
platform :ios, '7.0'
pod 'RestKit' # latest version (0.23.3) requires 'AFNetworking', '~> 1.3.0'
pod 'AFNetworking', '~> 1.2.0'
end
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should == ['AFNetworking (1.2.1)', 'RestKit (0.20.1)',
'RestKit/Core (0.20.1)', 'RestKit/CoreData (0.20.1)',
'RestKit/Network (0.20.1)', 'RestKit/ObjectMapping (0.20.1)',
'RestKit/Support (0.20.1)', 'SOCKit (1.1)', 'TransitionKit (1.1.0)']
end
it 'resolves three-way conflicts' do
@podfile = Podfile.new do
platform :ios, '7.0'
pod 'AFAmazonS3Client' # latest version (2.0.0) requires 'AFNetworking', '~> 2.0'
pod 'CargoBay' # latest version (2.1.0) requires 'AFNetworking', '~> 2.2'
pod 'AFOAuth2Client' # latest version (0.1.2) requires 'AFNetworking', '~> 1.3'
end
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should == ['AFAmazonS3Client (1.0.1)', 'AFNetworking (1.3.4)',
'AFOAuth2Client (0.1.2)', 'CargoBay (1.0.0)']
end
it 'uses a Podfile requirement even when a previously declared ' \
'dependency has a different requirement' do
@podfile = Podfile.new do
platform :ios, '7.0'
pod 'InstagramKit' # latest version (3.5.0) requires 'AFNetworking', '~> 2.0'
pod 'AFNetworking', '2.0.1'
end
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:root).map(&:to_s).uniq.sort
specs.should == ['AFNetworking (2.0.1)', 'InstagramKit (3.5.0)']
end
it 'holds the context state, such as cached specification sets' do
@resolver.resolve
cached_sets = @resolver.send(:cached_sets)
......@@ -107,7 +170,7 @@ module Pod
@resolver.resolve
end.message
message.should.match /Unable to find a specification/
message.should.match /`Windows` depended upon by BlocksKit/
message.should.match /`Windows` depended upon by `BlocksKit`/
end
it 'does not raise if all dependencies are supported by the platform of the target definition' do
......@@ -119,7 +182,7 @@ module Pod
platform :ios, '7.0'
pod 'RestKit', '0.10.3'
end
resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
resolver.resolve.values.flatten.map(&:name).sort.should == %w(
FileMD5Hash
ISO8601DateFormatter
......@@ -142,6 +205,24 @@ module Pod
)
end
it 'handles pre-release dependencies with subspecs' do
@podfile = Podfile.new do
platform :ios, '7.0'
pod 'RestKit', '0.20.0-rc1'
end
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
resolver.resolve.values.flatten.map(&:to_s).sort.should == [
'AFNetworking (1.1.0)',
'RestKit (0.20.0-rc1)',
'RestKit/Core (0.20.0-rc1)',
'RestKit/CoreData (0.20.0-rc1)',
'RestKit/Network (0.20.0-rc1)',
'RestKit/ObjectMapping (0.20.0-rc1)',
'RestKit/Support (0.20.0-rc1)',
'SOCKit (1.1)',
]
end
it 'handles correctly subspecs from external sources' do
@podfile = Podfile.new do
platform :ios
......@@ -158,7 +239,7 @@ module Pod
end
end
config.sandbox.expects(:specification).with('MainSpec').returns(spec)
resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:name).sort
specs.should == %w(
MainSpec/FirstSubSpec MainSpec/FirstSubSpec/SecondSubSpec
......@@ -171,7 +252,7 @@ module Pod
pod 'FileMD5Hash'
pod 'JSONKit', :head
end
resolver = Resolver.new(config.sandbox, podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, podfile, empty_graph, SourcesManager.all)
filemd5hash, jsonkit = resolver.resolve.values.first.sort_by(&:name)
filemd5hash.version.should.not.be.head
jsonkit.version.should.be.head
......@@ -179,15 +260,41 @@ module Pod
config.sandbox.head_pod?('JSONKit').should.be.true
end
it 'raises if it finds two conflicting dependencies' do
it 'raises if it finds two conflicting explicit dependencies' do
podfile = Podfile.new do
platform :ios
pod 'JSONKit', '1.4'
pod 'JSONKit', '1.5pre'
end
resolver = Resolver.new(config.sandbox, podfile, [], SourcesManager.all)
e = lambda { resolver.resolve }.should.raise Pod::Informative
resolver = Resolver.new(config.sandbox, podfile, empty_graph, SourcesManager.all)
e = lambda { resolver.resolve }.should.raise Informative
e.message.should.match(/Unable to satisfy the following requirements/)
e.message.should.match(/`JSONKit \(= 1.4\)` required by `Podfile`/)
e.message.should.match(/`JSONKit \(= 1.5pre\)` required by `Podfile`/)
end
it 'raises if it finds two conflicting dependencies' do
podfile = Podfile.new do
platform :ios
pod 'RestKit', '0.23.3' # dependends on AFNetworking ~> 1.3.0
pod 'AFNetworking', '> 2'
end
resolver = Resolver.new(config.sandbox, podfile, empty_graph, SourcesManager.all)
e = lambda { resolver.resolve }.should.raise Informative
e.message.should.match(/Unable to satisfy the following requirements/)
e.message.should.match(/`AFNetworking \(~> 1.3.0\)` required by `RestKit\/Network \(.*\)`/)
e.message.should.match(/`AFNetworking \(> 2\)` required by `Podfile`/)
end
it 'raises if no such version of a dependency exists' do
podfile = Podfile.new do
platform :ios
pod 'AFNetworking', '3.0.1'
end
resolver = Resolver.new(config.sandbox, podfile, empty_graph, SourcesManager.all)
e = lambda { resolver.resolve }.should.raise Informative
e.message.should.match(/Unable to satisfy the following requirements/)
e.message.should.match(/`AFNetworking \(= 3.0.1\)` required by `Podfile`/)
end
it 'takes into account locked dependencies' do
......@@ -195,45 +302,16 @@ module Pod
platform :ios
pod 'JSONKit', '<= 1.5pre'
end
resolver = Resolver.new(config.sandbox, podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, podfile, empty_graph, SourcesManager.all)
version = resolver.resolve.values.flatten.first.version
version.to_s.should == '1.5pre'
locked_deps = [Dependency.new('JSONKit', '= 1.4')]
locked_deps = dependency_graph_from_array([Dependency.new('JSONKit', '= 1.4')])
resolver = Resolver.new(config.sandbox, podfile, locked_deps, SourcesManager.all)
version = resolver.resolve.values.flatten.first.version
version.to_s.should == '1.4'
end
xit 'takes into account locked implicit dependencies' do
podfile = Podfile.new do
platform :ios, '8.0'
pod 'ARAnalytics/Mixpanel'
end
lockfile_yaml = <<-EOS
PODS:
- ARAnalytics/CoreIOS (2.8.0)
- ARAnalytics/Mixpanel (2.8.0):
- ARAnalytics/CoreIOS
- Mixpanel
- Mixpanel (2.5.1)
DEPENDENCIES:
- ARAnalytics/Mixpanel
SPEC CHECKSUMS:
ARAnalytics: 93c5b65989145f88f4d45e262612eac277b0c219
Mixpanel: 0115466ba70fd12e67ac4d3d071408dd1d489657
COCOAPODS: 0.33.1
EOS
lockfile = Lockfile.new(YAMLHelper.load_string(lockfile_yaml))
resolver = Resolver.new(config.sandbox, podfile, lockfile.dependencies, SourcesManager.all)
resolver.resolve.values.first.
find { |s| s.name == 'Mixpanel' }.
version.to_s.should == '2.5.1'
end
it 'consults all sources when finding a matching spec' do
podfile = Podfile.new do
platform :ios
......@@ -241,13 +319,13 @@ COCOAPODS: 0.33.1
end
file = fixture('spec-repos/test_repo/JSONKit/999.999.999/JSONKit.podspec')
sources = SourcesManager.sources(%w(master test_repo))
resolver = Resolver.new(config.sandbox, podfile, [], sources)
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '999.999.999'
spec.defined_in_file.should == file
sources = SourcesManager.sources(%w(test_repo master))
resolver = Resolver.new(config.sandbox, podfile, [], sources)
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '999.999.999'
resolver.resolve.values.flatten.first.defined_in_file.should == file
......@@ -260,13 +338,13 @@ COCOAPODS: 0.33.1
pod 'JSONKit', '1.4'
end
sources = SourcesManager.sources(%w(master test_repo))
resolver = Resolver.new(config.sandbox, podfile, [], sources)
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '1.4'
spec.defined_in_file.should == fixture('spec-repos/master/Specs/JSONKit/1.4/JSONKit.podspec.json')
sources = SourcesManager.sources(%w(test_repo master))
resolver = Resolver.new(config.sandbox, podfile, [], sources)
resolver = Resolver.new(config.sandbox, podfile, empty_graph, sources)
spec = resolver.resolve.values.flatten.first
spec.version.to_s.should == '1.4'
resolver.resolve.values.flatten.first.defined_in_file.should == fixture('spec-repos/test_repo/JSONKit/1.4/JSONKit.podspec')
......@@ -285,90 +363,90 @@ COCOAPODS: 0.33.1
pod 'AFNetworking', '1.0RC3'
end
resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should == ['AFNetworking (1.0RC3)']
end
xit 'resolves to latest minor version even when explicitly requesting pre-release versions when using ~>' do
it 'resolves to latest minor version even when explicitly requesting pre-release versions when using ~>' do
@podfile = Podfile.new do
platform :ios, '6.0'
pod 'AFNetworking', '~> 1.0RC3'
end
resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should != ['AFNetworking (1.0RC3)']
specs.should == ['AFNetworking (1.2.0)']
specs.should == ['AFNetworking (1.3.4)']
end
xit 'does not resolve to a pre-release version implicitly when matching exact version' do
it 'does not resolve to a pre-release version implicitly when matching exact version' do
@podfile = Podfile.new do
platform :ios, '6.0'
pod 'AFNetworking', '1.0'
end
resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should != ['AFNetworking (1.0RC3)']
specs.should == ['AFNetworking (1.0)']
end
xit 'does not resolve to a pre-release version implicitly when using <' do
it 'does not resolve to a pre-release version implicitly when using <' do
@podfile = Podfile.new do
platform :ios, '6.0'
pod 'AFNetworking', '< 1.0'
end
resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should != ['AFNetworking (1.0RC3)']
specs.should == ['AFNetworking (0.10.1)']
end
xit 'does not resolve to a pre-release version implicitly when using <=' do
it 'does not resolve to a pre-release version implicitly when using <=' do
@podfile = Podfile.new do
platform :ios, '6.0'
pod 'AFNetworking', '<= 1.0'
end
resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should != ['AFNetworking (1.0RC3)']
specs.should == ['AFNetworking (1.0)']
end
xit 'does not resolve to a pre-release version implicitly when using >' do
it 'does not resolve to a pre-release version implicitly when using >' do
@podfile = Podfile.new do
platform :ios, '6.0'
pod 'AFNetworking', '> 1.0', '< 1.3'
end
resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should != ['AFNetworking (1.0RC3)']
specs.should == ['AFNetworking (1.2.1)']
end
xit 'does not resolve to a pre-release version implicitly when using >=' do
it 'does not resolve to a pre-release version implicitly when using >=' do
@podfile = Podfile.new do
platform :ios, '6.0'
pod 'AFNetworking', '>= 1.0', '< 1.3'
end
resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should != ['AFNetworking (1.0RC3)']
specs.should == ['AFNetworking (1.2.1)']
end
xit 'does not resolve to a pre-release version implicitly when using ~>' do
it 'does not resolve to a pre-release version implicitly when using ~>' do
@podfile = Podfile.new do
platform :ios, '6.0'
pod 'AFNetworking', '~> 1.0', '< 1.3'
end
resolver = Resolver.new(config.sandbox, @podfile, [], SourcesManager.all)
resolver = Resolver.new(config.sandbox, @podfile, empty_graph, SourcesManager.all)
specs = resolver.resolve.values.flatten.map(&:to_s).sort
specs.should != ['AFNetworking (1.0RC3)']
specs.should == ['AFNetworking (1.2.1)']
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment