Re-resolve when lockfile is invalid

Move the check for unmet dependencies in lockfile just in time to be
able to re-resolve if unmet dependencies are found.
This commit is contained in:
David Rodriguez 2023-09-30 21:01:36 +02:00 committed by Hiroshi SHIBATA
parent 7e51cadc2e
commit cbf2e133c1
No known key found for this signature in database
GPG Key ID: F9CF13417264FAC2
4 changed files with 69 additions and 83 deletions

View File

@ -149,7 +149,7 @@ module Bundler
@dependency_changes = converge_dependencies
@local_changes = converge_locals
@missing_lockfile_dep = check_missing_lockfile_dep
check_lockfile
end
def gem_version_promoter
@ -478,7 +478,7 @@ module Bundler
private :sources
def nothing_changed?
!@source_changes && !@dependency_changes && !@new_platform && !@path_changes && !@local_changes && !@missing_lockfile_dep && !@unlocking_bundler
!@source_changes && !@dependency_changes && !@new_platform && !@path_changes && !@local_changes && !@missing_lockfile_dep && !@unlocking_bundler && !@invalid_lockfile_dep
end
def no_resolve_needed?
@ -630,6 +630,7 @@ module Bundler
[@local_changes, "the gemspecs for git local gems changed"],
[@missing_lockfile_dep, "your lock file is missing \"#{@missing_lockfile_dep}\""],
[@unlocking_bundler, "an update to the version of Bundler itself was requested"],
[@invalid_lockfile_dep, "your lock file has an invalid dependency \"#{@invalid_lockfile_dep}\""],
].select(&:first).map(&:last).join(", ")
end
@ -684,24 +685,38 @@ module Bundler
!sources_with_changes.each {|source| @unlock[:sources] << source.name }.empty?
end
def check_missing_lockfile_dep
all_locked_specs = @locked_specs.map(&:name) << "bundler"
def check_lockfile
@invalid_lockfile_dep = nil
@missing_lockfile_dep = nil
missing = @locked_specs.select do |s|
s.dependencies.any? {|dep| !all_locked_specs.include?(dep.name) }
locked_names = @locked_specs.map(&:name)
missing = []
invalid = []
@locked_specs.each do |s|
s.dependencies.each do |dep|
next if dep.name == "bundler"
missing << s unless locked_names.include?(dep.name)
invalid << s if @locked_specs.none? {|spec| dep.matches_spec?(spec) }
end
end
if missing.any?
@locked_specs.delete(missing)
return missing.first.name
@missing_lockfile_dep = missing.first.name
elsif !@dependency_changes
@missing_lockfile_dep = current_dependencies.find do |d|
@locked_specs[d.name].empty? && d.name != "bundler"
end&.name
end
return if @dependency_changes
if invalid.any?
@locked_specs.delete(invalid)
current_dependencies.find do |d|
@locked_specs[d.name].empty? && d.name != "bundler"
end&.name
@invalid_lockfile_dep = invalid.first.name
end
end
def converge_paths

View File

@ -91,38 +91,12 @@ module Bundler
install_serially
end
check_for_unmet_dependencies
handle_error if failed_specs.any?
@specs
ensure
worker_pool&.stop
end
def check_for_unmet_dependencies
unmet_dependencies = @specs.map do |s|
[
s,
s.dependencies.reject {|dep| @specs.any? {|spec| dep.matches_spec?(spec.spec) } },
]
end.reject {|a| a.last.empty? }
return if unmet_dependencies.empty?
warning = []
warning << "Your lockfile doesn't include a valid resolution."
warning << "You can fix this by regenerating your lockfile or manually editing the bad locked gems to a version that satisfies all dependencies."
warning << "The unmet dependencies are:"
unmet_dependencies.each do |spec, unmet_spec_dependencies|
unmet_spec_dependencies.each do |unmet_spec_dependency|
found = @specs.find {|s| s.name == unmet_spec_dependency.name && !unmet_spec_dependency.matches_spec?(s.spec) }
warning << "* #{unmet_spec_dependency}, dependency of #{spec.full_name}, unsatisfied by #{found.full_name}"
end
end
Bundler.ui.warn(warning.join("\n"))
end
private
def failed_specs

View File

@ -1,46 +0,0 @@
# frozen_string_literal: true
require "bundler/installer/parallel_installer"
RSpec.describe Bundler::ParallelInstaller do
let(:installer) { instance_double("Installer") }
let(:all_specs) { [] }
let(:size) { 1 }
let(:standalone) { false }
let(:force) { false }
subject { described_class.new(installer, all_specs, size, standalone, force) }
context "when the spec set is not a valid resolution" do
let(:all_specs) do
[
build_spec("cucumber", "4.1.0") {|s| s.runtime "diff-lcs", "< 1.4" },
build_spec("diff-lcs", "1.4.4"),
].flatten
end
it "prints a warning" do
expect(Bundler.ui).to receive(:warn).with(<<-W.strip)
Your lockfile doesn't include a valid resolution.
You can fix this by regenerating your lockfile or manually editing the bad locked gems to a version that satisfies all dependencies.
The unmet dependencies are:
* diff-lcs (< 1.4), dependency of cucumber-4.1.0, unsatisfied by diff-lcs-1.4.4
W
subject.check_for_unmet_dependencies
end
end
context "when the spec set is a valid resolution" do
let(:all_specs) do
[
build_spec("cucumber", "4.1.0") {|s| s.runtime "diff-lcs", "< 1.4" },
build_spec("diff-lcs", "1.3"),
].flatten
end
it "doesn't print a warning" do
expect(Bundler.ui).not_to receive(:warn)
subject.check_for_unmet_dependencies
end
end
end

View File

@ -1101,4 +1101,47 @@ RSpec.describe "bundle install with gem sources" do
expect(err).to include("Could not find compatible versions")
end
end
context "when a lockfile has unmet dependencies, and the Gemfile has no resolution" do
before do
build_repo4 do
build_gem "aaa", "0.2.0" do |s|
s.add_dependency "zzz", "< 0.2.0"
end
build_gem "zzz", "0.2.0"
end
gemfile <<~G
source "#{file_uri_for(gem_repo4)}"
gem "aaa"
gem "zzz"
G
lockfile <<~L
GEM
remote: #{file_uri_for(gem_repo4)}/
specs:
aaa (0.2.0)
zzz (< 0.2.0)
zzz (0.2.0)
PLATFORMS
#{lockfile_platforms}
DEPENDENCIES
aaa!
zzz!
BUNDLED WITH
#{Bundler::VERSION}
L
end
it "does not install, but raises a resolution error" do
bundle "install", :raise_on_error => false
expect(err).to include("Could not find compatible versions")
end
end
end