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
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
# @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
#--------------------------------------#
......
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