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 ...@@ -4,7 +4,13 @@ To install or update CocoaPods see this [guide](http://docs.cocoapods.org/guides
## Master ## 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 * The `pod push` has been removed as it has been deprecated in favour of `pod
repo push` in CocoaPods 0.33. repo push` in CocoaPods 0.33.
...@@ -15,6 +21,26 @@ To install or update CocoaPods see this [guide](http://docs.cocoapods.org/guides ...@@ -15,6 +21,26 @@ To install or update CocoaPods see this [guide](http://docs.cocoapods.org/guides
[Marius Rackwitz](https://github.com/mrackwitz) [Marius Rackwitz](https://github.com/mrackwitz)
[#2461](https://github.com/CocoaPods/CocoaPods/issues/2461) [#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 ##### Bug Fixes
* Do not try to clone spec-repos in `/`. * Do not try to clone spec-repos in `/`.
......
...@@ -12,12 +12,13 @@ gemspec ...@@ -12,12 +12,13 @@ gemspec
group :development do group :development do
cp_gem 'claide', 'CLAide' cp_gem 'claide', 'CLAide'
cp_gem 'cocoapods-core', 'Core' cp_gem 'cocoapods-core', 'Core', 'resolver'
cp_gem 'cocoapods-downloader', 'cocoapods-downloader' cp_gem 'cocoapods-downloader', 'cocoapods-downloader'
cp_gem 'cocoapods-plugins', 'cocoapods-plugins' cp_gem 'cocoapods-plugins', 'cocoapods-plugins'
cp_gem 'cocoapods-trunk', 'cocoapods-trunk' cp_gem 'cocoapods-trunk', 'cocoapods-trunk'
cp_gem 'cocoapods-try', 'cocoapods-try' cp_gem 'cocoapods-try', 'cocoapods-try'
cp_gem 'xcodeproj', 'Xcodeproj' cp_gem 'xcodeproj', 'Xcodeproj'
cp_gem 'molinillo', 'Molinillo'
gem 'bacon' gem 'bacon'
gem 'mocha' gem 'mocha'
......
...@@ -7,8 +7,8 @@ GIT ...@@ -7,8 +7,8 @@ GIT
GIT GIT
remote: https://github.com/CocoaPods/Core.git remote: https://github.com/CocoaPods/Core.git
revision: 99748b5b9fa8fe6541218f1752c0379589c7a6ee revision: 802401562948abf9f4f417ace4cd8e9bb8b08859
branch: master branch: resolver
specs: specs:
cocoapods-core (0.34.4) cocoapods-core (0.34.4)
activesupport (>= 3.2.15) activesupport (>= 3.2.15)
...@@ -16,6 +16,13 @@ GIT ...@@ -16,6 +16,13 @@ GIT
json_pure (~> 1.8) json_pure (~> 1.8)
nap (~> 0.8.0) nap (~> 0.8.0)
GIT
remote: https://github.com/CocoaPods/Molinillo.git
revision: 3e27234148022ba1455a9228e7b710314204d4c5
branch: master
specs:
molinillo (0.0.1)
GIT GIT
remote: https://github.com/CocoaPods/Xcodeproj.git remote: https://github.com/CocoaPods/Xcodeproj.git
revision: ff7c70da5f5082eef47bb248012b69c76f6c6e54 revision: ff7c70da5f5082eef47bb248012b69c76f6c6e54
...@@ -71,6 +78,7 @@ PATH ...@@ -71,6 +78,7 @@ PATH
colored (~> 1.2) colored (~> 1.2)
escape (~> 0.0.4) escape (~> 0.0.4)
json_pure (~> 1.8) json_pure (~> 1.8)
molinillo (~> 0)
nap (~> 0.8) nap (~> 0.8)
open4 (~> 1.3) open4 (~> 1.3)
xcodeproj (~> 0.19.4) xcodeproj (~> 0.19.4)
...@@ -174,6 +182,7 @@ DEPENDENCIES ...@@ -174,6 +182,7 @@ DEPENDENCIES
mime-types (< 2.0) mime-types (< 2.0)
mocha mocha
mocha-on-bacon mocha-on-bacon
molinillo!
prettybacon prettybacon
pry pry
rake rake
......
...@@ -34,6 +34,7 @@ Gem::Specification.new do |s| ...@@ -34,6 +34,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'cocoapods-plugins', '~> 0.3.1' s.add_runtime_dependency 'cocoapods-plugins', '~> 0.3.1'
s.add_runtime_dependency 'cocoapods-try', '~> 0.4.1' s.add_runtime_dependency 'cocoapods-try', '~> 0.4.1'
s.add_runtime_dependency 'cocoapods-trunk', '~> 0.3.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 'colored', '~> 1.2'
s.add_runtime_dependency 'escape', '~> 0.0.4' s.add_runtime_dependency 'escape', '~> 0.0.4'
......
require 'colored' require 'colored'
require 'claide' require 'claide'
require 'molinillo/errors'
module Molinillo
class ResolverError
include CLAide::InformativeError
end
end
module Pod module Pod
class PlainInformative class PlainInformative
......
...@@ -8,6 +8,8 @@ module Pod ...@@ -8,6 +8,8 @@ module Pod
autoload :SandboxAnalyzer, 'cocoapods/installer/analyzer/sandbox_analyzer' 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. # @return [Sandbox] The sandbox where the Pods should be installed.
# #
attr_reader :sandbox attr_reader :sandbox
...@@ -149,17 +151,13 @@ module Pod ...@@ -149,17 +151,13 @@ module Pod
pods_state = nil pods_state = nil
UI.section 'Finding Podfile changes' do UI.section 'Finding Podfile changes' do
pods_by_state = lockfile.detect_changes_with_podfile(podfile) 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 = SpecsState.new(pods_by_state)
pods_state.print pods_state.print
end end
pods_state pods_state
else else
state = SpecsState.new state = SpecsState.new
state.added.concat(podfile.dependencies.map(&:root_name).uniq) state.added.concat(podfile.dependencies.map(&:name).uniq)
state state
end end
end end
...@@ -231,21 +229,17 @@ module Pod ...@@ -231,21 +229,17 @@ module Pod
# is in update mode, to prevent it from upgrading the Pods that weren't # is in update mode, to prevent it from upgrading the Pods that weren't
# changed in the {Podfile}. # changed in the {Podfile}.
# #
# @return [Array<Dependency>] the dependencies generate by the lockfile # @return [Molinillo::DependencyGraph<Dependency>] the dependencies
# that prevent the resolver to update a Pod. # generated by the lockfile that prevent the resolver to update
# a Pod.
# #
def generate_version_locking_dependencies def generate_version_locking_dependencies
if update_mode == :all if update_mode == :all || !lockfile
[] LockingDependencyAnalyzer.unlocked_dependency_graph
else else
locking_pods = result.podfile_state.unchanged pods_to_update = result.podfile_state.changed + result.podfile_state.deleted
if update_mode == :selected pods_to_update += update[:pods] if update_mode == :selected
# If selected Pods should been updated, filter them out of the list LockingDependencyAnalyzer.generate_version_locking_dependencies(lockfile, pods_to_update)
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
end end
end end
...@@ -281,9 +275,9 @@ module Pod ...@@ -281,9 +275,9 @@ module Pod
if update_mode == :selected if update_mode == :selected
pods_to_fetch += update[:pods] pods_to_fetch += update[:pods]
end end
deps_to_fetch = deps_with_external_source.select { |dep| pods_to_fetch.include?(dep.root_name) } 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.root_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.root_name).nil? || !dep.external_source[:local].nil? || !dep.external_source[:path].nil? || !sandbox.pod_dir(dep.root_name).directory? } 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 end
unless deps_to_fetch.empty? unless deps_to_fetch.empty?
...@@ -352,7 +346,7 @@ module Pod ...@@ -352,7 +346,7 @@ module Pod
# @!group Analysis internal products # @!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. # that prevent the resolver to update a Pod.
# #
attr_reader :locked_dependencies attr_reader :locked_dependencies
...@@ -604,7 +598,7 @@ module Pod ...@@ -604,7 +598,7 @@ module Pod
# #
class SpecsState class SpecsState
# @param [Hash{Symbol=>String}] pods_by_state # @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`). # (`:added`, `:removed`, `:changed` or `:unchanged`).
# #
def initialize(pods_by_state = nil) def initialize(pods_by_state = nil)
...@@ -656,12 +650,9 @@ module Pod ...@@ -656,12 +650,9 @@ module Pod
# @param [Symbol] # @param [Symbol]
# the state of the Pod. # the state of the Pod.
# #
# @raise If there is an attempt to add the name of a subspec.
#
# @return [void] # @return [void]
# #
def add_name(name, state) def add_name(name, state)
raise '[Bug] Attempt to add subspec to the pods state' if name.include?('/')
send(state) << name send(state) << name
end end
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
This diff is collapsed.
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 ...@@ -219,6 +219,7 @@ module Pod
# @return [Nil] if the podspec is not stored. # @return [Nil] if the podspec is not stored.
# #
def specification_path(name) def specification_path(name)
name = Specification.root_name(name)
path = specifications_root + "#{name}.podspec" path = specifications_root + "#{name}.podspec"
if path.exist? if path.exist?
path path
...@@ -378,7 +379,7 @@ module Pod ...@@ -378,7 +379,7 @@ module Pod
end end
# @return [Hash{String=>String}] The path of the Pods with a local source # @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`) # @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 ...@@ -226,13 +226,13 @@ module Pod
it 'cats the given podspec' do it 'cats the given podspec' do
lambda { command('spec', 'cat', 'AFNetworking').run }.should.not.raise 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 end
it 'cats the first podspec from all podspecs' do it 'cats the first podspec from all podspecs' do
UI.next_input = "1\n" UI.next_input = "1\n"
run_command('spec', 'cat', '--show-all', 'AFNetworking') 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
end end
...@@ -292,7 +292,7 @@ module Pod ...@@ -292,7 +292,7 @@ module Pod
it 'returns the path of the specification with the given name' do it 'returns the path of the specification with the given name' do
path = @sut.send(:get_path_of_spec, 'AFNetworking') 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
end end
......
...@@ -48,7 +48,7 @@ module Pod ...@@ -48,7 +48,7 @@ module Pod
output = UI.output output = UI.output
output.should.include? 'Author: Robbie Hanson' output.should.include? 'Author: Robbie Hanson'
output.should.include? 'License: BSD' 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? 'Watchers: 318'
output.should.include? 'Forks: 42' output.should.include? 'Forks: 42'
output.should.include? 'Pushed: more than a year ago' output.should.include? 'Pushed: more than a year ago'
......
...@@ -13,7 +13,7 @@ def create_analyzer ...@@ -13,7 +13,7 @@ def create_analyzer
end end
hash = {} 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['DEPENDENCIES'] = %w(JSONKit NUI SVPullToRefresh)
hash['SPEC CHECKSUMS'] = {} hash['SPEC CHECKSUMS'] = {}
hash['COCOAPODS'] = Pod::VERSION hash['COCOAPODS'] = Pod::VERSION
...@@ -52,10 +52,10 @@ module Pod ...@@ -52,10 +52,10 @@ module Pod
it 'computes the state of the Podfile respect to the Lockfile' do it 'computes the state of the Podfile respect to the Lockfile' do
state = @analyzer.analyze.podfile_state state = @analyzer.analyze.podfile_state
state.added.should == %w(AFNetworking libextobjc) state.added.should == %w(AFNetworking libextobjc/EXTKeyPathCoding)
state.changed.should == ['JSONKit'] state.changed.should == %w()
state.unchanged.should == ['SVPullToRefresh'] state.unchanged.should == %w(JSONKit SVPullToRefresh)
state.deleted.should == ['NUI'] state.deleted.should == %w(NUI)
end end
#--------------------------------------# #--------------------------------------#
...@@ -131,13 +131,35 @@ module Pod ...@@ -131,13 +131,35 @@ module Pod
it 'locks the version of the dependencies which did not change in the Podfile' do it 'locks the version of the dependencies which did not change in the Podfile' do
@analyzer.analyze @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 end
it 'does not lock the dependencies in update mode' do it 'does not lock the dependencies in update mode' do
@analyzer.update = true @analyzer.update = true
@analyzer.analyze @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 end
#--------------------------------------# #--------------------------------------#
......
This diff is collapsed.
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