diff --git a/Manifest.txt b/Manifest.txt index f32bd361afa0..68a545439680 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -80,6 +80,7 @@ bundler/lib/bundler/gem_helpers.rb bundler/lib/bundler/gem_tasks.rb bundler/lib/bundler/gem_version_promoter.rb bundler/lib/bundler/graph.rb +bundler/lib/bundler/incomplete_specification.rb bundler/lib/bundler/index.rb bundler/lib/bundler/injector.rb bundler/lib/bundler/inline.rb diff --git a/bundler/lib/bundler.rb b/bundler/lib/bundler.rb index 0be01d18088e..6f5b605988ee 100644 --- a/bundler/lib/bundler.rb +++ b/bundler/lib/bundler.rb @@ -53,6 +53,7 @@ module Bundler autoload :GemHelpers, File.expand_path("bundler/gem_helpers", __dir__) autoload :GemVersionPromoter, File.expand_path("bundler/gem_version_promoter", __dir__) autoload :Graph, File.expand_path("bundler/graph", __dir__) + autoload :IncompleteSpecification, File.expand_path("bundler/incomplete_specification", __dir__) autoload :Index, File.expand_path("bundler/index", __dir__) autoload :Injector, File.expand_path("bundler/injector", __dir__) autoload :Installer, File.expand_path("bundler/installer", __dir__) diff --git a/bundler/lib/bundler/definition.rb b/bundler/lib/bundler/definition.rb index 4fca763bcce5..ea65cbd06222 100644 --- a/bundler/lib/bundler/definition.rb +++ b/bundler/lib/bundler/definition.rb @@ -138,13 +138,13 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti @unlock[:gems] ||= @dependencies.map(&:name) else eager_unlock = expand_dependencies(@unlock[:gems] || [], true) - @unlock[:gems] = @locked_specs.for(eager_unlock, false, false).map(&:name) + @unlock[:gems] = @locked_specs.for(eager_unlock, false, platforms).map(&:name) end @dependency_changes = converge_dependencies @local_changes = converge_locals - @locked_specs_incomplete_for_platform = !@locked_specs.for(requested_dependencies & expand_dependencies(locked_dependencies), true, true) + @reresolve = nil @requires = compute_requires end @@ -263,11 +263,8 @@ def resolve Bundler.ui.debug("Found no changes, using resolution from the lockfile") SpecSet.new(filter_specs(@locked_specs, @dependencies.select {|dep| @locked_specs[dep].any? })) else - last_resolve = converge_locked_specs - # Run a resolve against the locally available gems Bundler.ui.debug("Found changes from the lockfile, re-resolving dependencies because #{change_reason}") - expanded_dependencies = expand_dependencies(dependencies + metadata_dependencies, true) - Resolver.resolve(expanded_dependencies, source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve, platforms) + @reresolve = reresolve end end end @@ -456,7 +453,7 @@ def most_specific_locked_platform private :sources def nothing_changed? - !@source_changes && !@dependency_changes && !@new_platform && !@path_changes && !@local_changes && !@locked_specs_incomplete_for_platform + !@source_changes && !@dependency_changes && !@new_platform && !@path_changes && !@local_changes end def unlocking? @@ -465,8 +462,14 @@ def unlocking? private + def reresolve + last_resolve = converge_locked_specs + expanded_dependencies = expand_dependencies(dependencies + metadata_dependencies, true) + Resolver.resolve(expanded_dependencies, source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve, platforms) + end + def filter_specs(specs, deps) - SpecSet.new(specs).for(expand_dependencies(deps, true), false, false) + SpecSet.new(specs).for(expand_dependencies(deps, true), false, platforms) end def materialize(dependencies) @@ -486,6 +489,17 @@ def materialize(dependencies) raise GemNotFound, "Could not find #{missing_specs.map(&:full_name).join(", ")} in any of the sources" end + if @reresolve.nil? + incomplete_specs = specs.incomplete_specs + + if incomplete_specs.any? + Bundler.ui.debug("The lockfile does not have all gems needed for the current platform though, Bundler will still re-resolve dependencies") + @unlock[:gems].concat(incomplete_specs.map(&:name)) + @resolve = reresolve + specs = resolve.materialize(dependencies) + end + end + unless specs["bundler"].any? bundler = sources.metadata_source.specs.search(Gem::Dependency.new("bundler", VERSION)).last specs["bundler"] = bundler @@ -533,7 +547,6 @@ def change_reason [@new_platform, "you added a new platform to your gemfile"], [@path_changes, "the gemspecs for path gems changed"], [@local_changes, "the gemspecs for git local gems changed"], - [@locked_specs_incomplete_for_platform, "the lockfile does not have all gems needed for the current platform"], ].select(&:first).map(&:last).join(", ") end @@ -709,7 +722,7 @@ def converge_specs(specs) # if we won't need the source (according to the lockfile), # don't error if the path/git source isn't available next if specs. - for(requested_dependencies, false, true). + for(requested_dependencies, false). none? {|locked_spec| locked_spec.source == s.source } raise diff --git a/bundler/lib/bundler/incomplete_specification.rb b/bundler/lib/bundler/incomplete_specification.rb new file mode 100644 index 000000000000..6d0b9b901c06 --- /dev/null +++ b/bundler/lib/bundler/incomplete_specification.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Bundler + class IncompleteSpecification + attr_reader :name, :platform + + def initialize(name, platform) + @name = name + @platform = platform + end + end +end diff --git a/bundler/lib/bundler/resolver.rb b/bundler/lib/bundler/resolver.rb index 2285114c5708..db0312488c8d 100644 --- a/bundler/lib/bundler/resolver.rb +++ b/bundler/lib/bundler/resolver.rb @@ -21,7 +21,7 @@ def self.resolve(requirements, source_requirements = {}, base = [], gem_version_ base = SpecSet.new(base) unless base.is_a?(SpecSet) resolver = new(source_requirements, base, gem_version_promoter, additional_base_requirements, platforms) result = resolver.start(requirements) - SpecSet.new(SpecSet.new(result).for(requirements.reject {|dep| dep.name.end_with?("\0") })) + SpecSet.new(SpecSet.new(result).for(requirements.reject {|dep| dep.name.end_with?("\0") }, false, platforms)) end def initialize(source_requirements, base, gem_version_promoter, additional_base_requirements, platforms) diff --git a/bundler/lib/bundler/spec_set.rb b/bundler/lib/bundler/spec_set.rb index 0dfaed980771..9e03cbc1d75e 100644 --- a/bundler/lib/bundler/spec_set.rb +++ b/bundler/lib/bundler/spec_set.rb @@ -11,28 +11,29 @@ def initialize(specs) @specs = specs end - def for(dependencies, check = false, match_current_platform = false) - handled = [] - deps = dependencies.dup + def for(dependencies, check = false, platforms = [nil]) + # dep.name => [list, of, deps] + handled = ["bundler"].product(platforms).map {|k| [k, true] }.to_h + deps = dependencies.map(&:name).product(platforms) specs = [] loop do break unless dep = deps.shift - next if handled.any? {|d| d.name == dep.name && (match_current_platform || d.__platform == dep.__platform) } || dep.name == "bundler" + next if handled.key?(dep) - handled << dep + # use a hash here to ensure constant lookup time in the `any?` call above + handled[dep] = true - specs_for_dep = spec_for_dependency(dep, match_current_platform) + specs_for_dep = spec_for_dependency(*dep) if specs_for_dep.any? specs.concat(specs_for_dep) specs_for_dep.first.dependencies.each do |d| next if d.type == :development - d = DepProxy.get_proxy(d, dep.__platform) unless match_current_platform - deps << d + deps << [d.name, dep[1]] end elsif check - return false + specs << IncompleteSpecification.new(*dep) end end @@ -40,9 +41,7 @@ def for(dependencies, check = false, match_current_platform = false) specs << spec end - specs.uniq! unless match_current_platform - - check ? true : specs + specs end def [](key) @@ -69,7 +68,7 @@ def to_hash end def materialize(deps) - materialized = self.for(deps, false, true) + materialized = self.for(deps, true).uniq materialized.map! do |s| next s unless s.is_a?(LazySpecification) @@ -97,6 +96,10 @@ def missing_specs @specs.select {|s| s.is_a?(LazySpecification) } end + def incomplete_specs + @specs.select {|s| s.is_a?(IncompleteSpecification) } + end + def merge(set) arr = sorted.dup set.each do |set_spec| @@ -171,12 +174,12 @@ def tsort_each_node @specs.sort_by(&:name).each {|s| yield s } end - def spec_for_dependency(dep, match_current_platform) - specs_for_platforms = lookup[dep.name] - if match_current_platform + def spec_for_dependency(name, platform) + specs_for_platforms = lookup[name] + if platform.nil? GemHelpers.select_best_platform_match(specs_for_platforms.select {|s| Gem::Platform.match_spec?(s) }, Bundler.local_platform) else - GemHelpers.select_best_platform_match(specs_for_platforms, dep.__platform) + GemHelpers.select_best_platform_match(specs_for_platforms, platform) end end diff --git a/bundler/spec/runtime/setup_spec.rb b/bundler/spec/runtime/setup_spec.rb index 033102f4e3bb..4e48aba6b686 100644 --- a/bundler/spec/runtime/setup_spec.rb +++ b/bundler/spec/runtime/setup_spec.rb @@ -635,6 +635,7 @@ def clean_load_path(lp) ruby "require '#{system_gem_path("gems/bundler-9.99.9.beta1/lib/bundler.rb")}'; Bundler.setup", :env => { "DEBUG" => "1" } expect(out).to include("Found no changes, using resolution from the lockfile") + expect(out).not_to include("lockfile does not have all gems needed for the current platform") expect(err).to be_empty end