Commit 4c8c6d58 authored by Samuel E. Giddins's avatar Samuel E. Giddins

Merge pull request #4846 from CocoaPods/yavuz/imp/localPodLocking

[Analyzer] Unlock local dependencies only if their specification changes
parents e0402551 3b2c9f3f
......@@ -20,6 +20,16 @@ To install release candidates run `[sudo] gem install cocoapods --pre`
##### Bug Fixes
* Development pods will no longer be implicitly unlocked. This makes CocoaPods
to respect constraints related to dependencies of development pods in the lockfile.
If you change constraints of a dependency of your development pod, and want to
override the locked version, you will have to use `pod update ${DEPENDENCY_NAME}` manually.
[Muhammed Yavuz Nuzumlalı](https://github.com/manuyavuz)
[#4211](https://github.com/CocoaPods/CocoaPods/issues/4211)
[#4577](https://github.com/CocoaPods/CocoaPods/issues/4577)
[#4580](https://github.com/CocoaPods/CocoaPods/issues/4580)
* Fixes paths passed for resources bundles in the copy resources script.
[Marius Rackwitz](https://github.com/mrackwitz)
[#4954](https://github.com/CocoaPods/CocoaPods/pull/4954)
......
......@@ -68,10 +68,11 @@ module Pod
verify_platforms_specified!
end
@result.podfile_state = generate_podfile_state
@locked_dependencies = generate_version_locking_dependencies
store_existing_checkout_options
fetch_external_sources if allow_fetches
@locked_dependencies = generate_version_locking_dependencies
@result.specs_by_target = validate_platforms(resolve_dependencies)
@result.specifications = generate_specifications
@result.targets = generate_targets
......@@ -417,8 +418,11 @@ module Pod
else
pods_to_update = result.podfile_state.changed + result.podfile_state.deleted
pods_to_update += update[:pods] if update_mode == :selected
pods_to_update += podfile.dependencies.select(&:local?).map(&:name)
LockingDependencyAnalyzer.generate_version_locking_dependencies(lockfile, pods_to_update)
local_pod_names = podfile.dependencies.select(&:local?).map(&:root_name)
pods_to_unlock = local_pod_names.reject do |pod_name|
sandbox.specification(pod_name).checksum == lockfile.checksum(pod_name)
end
LockingDependencyAnalyzer.generate_version_locking_dependencies(lockfile, pods_to_update, pods_to_unlock)
end
end
......
......@@ -13,11 +13,23 @@ module Pod
# is in update mode, to prevent it from upgrading the Pods that weren't
# changed in the {Podfile}.
#
# @param [Lockfile] lockfile the lockfile containing dependency constraints
#
# @param [Array<String>] pods_to_update
# List of pod names which needs to be updated because installer is
# in update mode for these pods. Pods in this list and all their recursive dependencies
# will not be included in generated dependency graph
#
# @param [Array<String>] pods_to_unlock
# List of pod names whose version constraints will be removed from the generated dependency graph.
# Recursive dependencies of the pods won't be affected. This is currently used to force local pods
# to be evaluated again whenever checksum of the specification of the local pods changes.
#
# @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)
def self.generate_version_locking_dependencies(lockfile, pods_to_update, pods_to_unlock = [])
dependency_graph = Molinillo::DependencyGraph.new
if lockfile
......@@ -29,7 +41,7 @@ module Pod
pods = lockfile.to_hash['PODS'] || []
pods.each do |pod|
add_to_dependency_graph(pod, [], dependency_graph)
add_to_dependency_graph(pod, [], dependency_graph, pods_to_unlock)
end
pods_to_update = pods_to_update.flat_map do |u|
......@@ -56,20 +68,23 @@ module Pod
private
def self.add_child_vertex_to_graph(dependency_string, parents, dependency_graph)
def self.add_child_vertex_to_graph(dependency_string, parents, dependency_graph, pods_to_unlock)
dependency = Dependency.from_string(dependency_string)
if pods_to_unlock.include?(dependency.root_name)
dependency = Dependency.new(dependency.name)
end
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)
def self.add_to_dependency_graph(object, parents, dependency_graph, pods_to_unlock)
case object
when String
add_child_vertex_to_graph(object, parents, dependency_graph)
add_child_vertex_to_graph(object, parents, dependency_graph, pods_to_unlock)
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) }
dependency = add_child_vertex_to_graph(key, parents, dependency_graph, pods_to_unlock)
value.each { |v| add_to_dependency_graph(v, [dependency.name], dependency_graph, pods_to_unlock) }
end
end
end
......
......@@ -104,13 +104,7 @@ module Pod
def search_for(dependency)
@search ||= {}
@search[dependency] ||= begin
requirement = Requirement.new(dependency.requirement.as_list << requirement_for_locked_pod_named(dependency.name))
find_cached_set(dependency).
all_specifications.
select { |s| requirement.satisfied_by? s.version }.
map { |s| s.subspec_by_name(dependency.name, false) }.
compact.
reverse
specifications_for_dependency(dependency, [requirement_for_locked_pod_named(dependency.name)])
end
@search[dependency].dup
end
......@@ -279,6 +273,27 @@ module Pod
# @!group Private helpers
# Returns available specifications which satisfy requirements of given dependency
# and additional requirements.
#
# @param [Dependency] dependency
# The dependency whose requirements will be satisfied.
#
# @param [Array<Requirement>] additional_requirements
# List of additional requirements which should also be satisfied.
#
# @return [Array<Specification>] List of specifications satisfying given requirements.
#
def specifications_for_dependency(dependency, additional_requirements = [])
requirement = Requirement.new(dependency.requirement.as_list + additional_requirements)
find_cached_set(dependency).
all_specifications.
select { |s| requirement.satisfied_by? s.version }.
map { |s| s.subspec_by_name(dependency.name, false) }.
compact.
reverse
end
# @return [Set] Loads or returns a previously initialized set for the Pod
# of the given dependency.
#
......@@ -371,7 +386,9 @@ module Pod
case error
when Molinillo::VersionConflict
error.conflicts.each do |name, conflict|
local_pod_parent = conflict.requirement_trees.flatten.reverse.find(&:local?)
lockfile_reqs = conflict.requirements[name_for_locking_dependency_source]
if lockfile_reqs && lockfile_reqs.last && lockfile_reqs.last.prerelease? && !conflict.existing
message = 'Due to the previous naïve CocoaPods resolver, ' \
"you were using a pre-release version of `#{name}`, " \
......@@ -380,6 +397,12 @@ module Pod
'version requirement to your Podfile ' \
"(e.g. `pod '#{name}', '#{lockfile_reqs.map(&:requirement).join("', '")}'`) " \
"or revert to a stable version by running `pod update #{name}`."
elsif local_pod_parent && !specifications_for_dependency(conflict.requirement).empty?
# Conflict was caused by a requirement from a local dependency.
# Tell user to use `pod update`.
message << "\n\nIt seems like you've changed the constraints of dependency `#{name}` " \
"inside your development pod `#{local_pod_parent.name}`.\nYou should run `pod update #{name}` to apply " \
"changes you've made."
elsif (conflict.possibility && conflict.possibility.version.prerelease?) &&
(conflict.requirement && !(
conflict.requirement.prerelease? ||
......
......@@ -439,6 +439,57 @@ module Pod
version.to_s.should == '2.4.1'
end
it 'unlocks only local pod when specification checksum changes' do
sandbox = config.sandbox
local_spec = Specification.from_hash('name' => 'LocalPod', 'version' => '1.1', 'dependencies' => { 'Expecta' => ['~> 0.0'] })
sandbox.stubs(:specification).with('LocalPod').returns(local_spec)
podfile = Podfile.new do
platform :ios, '8.0'
project 'SampleProject/SampleProject'
target 'SampleProject' do
pod 'LocalPod', :path => '../'
end
end
hash = {}
hash['PODS'] = ['Expecta (0.2.0)', { 'LocalPod (1.0)' => ['Expecta (~> 0.0)'] }]
hash['DEPENDENCIES'] = ['LocalPod (from `../`)']
hash['EXTERNAL SOURCES'] = { 'LocalPod' => { :path => '../' } }
hash['SPEC CHECKSUMS'] = { 'LocalPod' => 'DUMMY_CHECKSUM' }
hash['COCOAPODS'] = Pod::VERSION
lockfile = Pod::Lockfile.new(hash)
analyzer = Installer::Analyzer.new(sandbox, podfile, lockfile)
analyzer.analyze(false).specifications.
find { |s| s.name == 'LocalPod' }.
version.to_s.should == '1.1'
analyzer.analyze(false).specifications.
find { |s| s.name == 'Expecta' }.
version.to_s.should == '0.2.0'
end
it 'raises if change in local pod specification conflicts with lockfile' do
sandbox = config.sandbox
local_spec = Specification.from_hash('name' => 'LocalPod', 'version' => '1.0', 'dependencies' => { 'Expecta' => ['0.2.2'] })
sandbox.stubs(:specification).with('LocalPod').returns(local_spec)
podfile = Podfile.new do
platform :ios, '8.0'
project 'SampleProject/SampleProject'
target 'SampleProject' do
pod 'LocalPod', :path => '../'
end
end
hash = {}
hash['PODS'] = ['Expecta (0.2.0)', { 'LocalPod (1.0)' => ['Expecta (=0.2.0)'] }]
hash['DEPENDENCIES'] = ['LocalPod (from `../`)']
hash['EXTERNAL SOURCES'] = { 'LocalPod' => { :path => '../' } }
hash['SPEC CHECKSUMS'] = {}
hash['COCOAPODS'] = Pod::VERSION
lockfile = Pod::Lockfile.new(hash)
analyzer = Installer::Analyzer.new(sandbox, podfile, lockfile)
should.raise(Informative) do
analyzer.analyze(false)
end.message.should.match /You should run `pod update Expecta`/
end
#--------------------------------------#
it 'takes into account locked implicit dependencies' do
......
......@@ -8,7 +8,7 @@ def generate_lockfile(lockfile_version: Pod::VERSION)
hash = {}
hash['PODS'] = []
hash['DEPENDENCIES'] = []
hash['SPEC CHECKSUMS'] = []
hash['SPEC CHECKSUMS'] = {}
hash['COCOAPODS'] = lockfile_version
Pod::Lockfile.new(hash)
end
......
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