Skip to content

Commit 8edd350

Browse files
composerinteraliamatzbot
authored andcommitted
[rubygems/rubygems] Avoid crashing with a corrupted lockfile
I did a bad thing (script that edits the Gemfile.lock directly) and ended up with a Gemfile.lock that was completely missing some indirect dependencies. While this is my fault and an error is reasonable, I noticed that the error got progressively less friendly in recent versions of bundler. Something similar came up in rubygems/rubygems#6210, and this commit would have helped with that case as well (although we've already handled this a different way with ruby#6219). Details: --- Back on Bundler 2.2.23, a corrupt lockfile like this would cause a helpful error: ``` Unable to find a spec satisfying minitest (>= 5.1) in the set. Perhaps the lockfile is corrupted? ``` Bundler 2.3.26 gave a helpful warning: ``` Warning: Your lockfile was created by an old Bundler that left some things out. Because of the missing DEPENDENCIES, we can only install gems one at a time, instead of installing 16 at a time. You can fix this by adding the missing gems to your Gemfile, running bundle install, and then removing the gems from your Gemfile. The missing gems are: * minitest depended upon by activesupport ``` But then continued on and crashed while trying to report the unmet dependency: ``` --- ERROR REPORT TEMPLATE ------------------------------------------------------- NoMethodError: undefined method `full_name' for nil:NilClass lib/bundler/installer/parallel_installer.rb:127:in `block (2 levels) in check_for_unmet_dependencies' ... ``` Bundler 2.4.0 and up crash as above when jobs=1, but crash even harder when run in parallel: ``` --- ERROR REPORT TEMPLATE ------------------------------------------------------- fatal: No live threads left. Deadlock? 3 threads, 3 sleeps current:0x00007fa6b6704660 main thread:0x00007fa6b6704660 * #<Thread:0x000000010833b130 sleep_forever> rb_thread_t:0x00007fa6b6704660 native:0x0000000108985600 int:0 * #<Thread:0x0000000108dea630@Parallel Installer Worker #0 tmp/1/gems/system/gems/bundler-2.5.0.dev/lib/bundler/worker.rb:90 sleep_forever> rb_thread_t:0x00007fa6b67f67c0 native:0x0000700009a62000 int:0 * #<Thread:0x0000000108dea4a0@Parallel Installer Worker #1 tmp/1/gems/system/gems/bundler-2.5.0.dev/lib/bundler/worker.rb:90 sleep_forever> rb_thread_t:0x00007fa6b67f63c0 native:0x0000700009c65000 int:0 <internal:thread_sync>:18:in `pop' tmp/1/gems/system/gems/bundler-2.5.0.dev/lib/bundler/worker.rb:42:in `deq' ... ``` Changes --- This commit fixes the confusing thread deadlock crash by detecting if dependencies are missing such that we'll never be able to enqueue. When that happens we treat it as a failure so the install can finish. That gets us back to the `NoMethodError`, which this commit fixes by using a different warning in the case where no spec is found. rubygems/rubygems@d73001a21d
1 parent 0a8faab commit 8edd350

File tree

3 files changed

+55
-3
lines changed

3 files changed

+55
-3
lines changed

lib/bundler/installer/parallel_installer.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ def dependencies_installed?(all_specs)
4747
dependencies.all? {|d| installed_specs.include? d.name }
4848
end
4949

50+
# Check whether spec's dependencies are missing, which can indicate a
51+
# corrupted lockfile
52+
def dependencies_missing?(all_specs)
53+
spec_names = all_specs.map(&:name)
54+
dependencies.any? {|d| !spec_names.include? d.name }
55+
end
56+
5057
# Represents only the non-development dependencies, the ones that are
5158
# itself and are in the total list.
5259
def dependencies
@@ -115,7 +122,12 @@ def check_for_unmet_dependencies
115122

116123
unmet_dependencies.each do |spec, unmet_spec_dependencies|
117124
unmet_spec_dependencies.each do |unmet_spec_dependency|
118-
warning << "* #{unmet_spec_dependency}, dependency of #{spec.full_name}, unsatisfied by #{@specs.find {|s| s.name == unmet_spec_dependency.name && !unmet_spec_dependency.matches_spec?(s.spec) }.full_name}"
125+
found = @specs.find {|s| s.name == unmet_spec_dependency.name && !unmet_spec_dependency.matches_spec?(s.spec) }
126+
if found
127+
warning << "* #{unmet_spec_dependency}, dependency of #{spec.full_name}, unsatisfied by #{found.full_name}"
128+
else
129+
warning << "* #{unmet_spec_dependency}, dependency of #{spec.full_name} but missing from lockfile"
130+
end
119131
end
120132
end
121133

@@ -212,6 +224,8 @@ def enqueue_specs
212224
if spec.dependencies_installed? @specs
213225
spec.state = :enqueued
214226
worker_pool.enq spec
227+
elsif spec.dependencies_missing? @specs
228+
spec.state = :failed
215229
end
216230
end
217231
end

spec/bundler/lock/lockfile_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,44 @@
12211221
and include("Either installing with `--full-index` or running `bundle update rack_middleware` should fix the problem.")
12221222
end
12231223

1224+
it "errors gracefully on a corrupt lockfile" do
1225+
build_repo4 do
1226+
build_gem "minitest-bisect", "1.6.0" do |s|
1227+
s.add_dependency "path_expander", "~> 1.1"
1228+
end
1229+
1230+
build_gem "path_expander", "1.1.1"
1231+
end
1232+
1233+
gemfile <<~G
1234+
source "#{file_uri_for(gem_repo4)}"
1235+
gem "minitest-bisect"
1236+
G
1237+
1238+
# Corrupt lockfile (completely missing path_expander)
1239+
lockfile <<~L
1240+
GEM
1241+
remote: #{file_uri_for(gem_repo4)}/
1242+
specs:
1243+
minitest-bisect (1.6.0)
1244+
1245+
PLATFORMS
1246+
#{lockfile_platforms}
1247+
1248+
DEPENDENCIES
1249+
minitest-bisect
1250+
1251+
BUNDLED WITH
1252+
#{Bundler::VERSION}
1253+
L
1254+
1255+
cache_gems "minitest-bisect-1.6.0", "path_expander-1.1.1", :gem_repo => gem_repo4
1256+
bundle :install, :raise_on_error => false
1257+
1258+
expect(err).not_to include("ERROR REPORT TEMPLATE")
1259+
expect(err).to include("path_expander (~> 1.1), dependency of minitest-bisect-1.6.0 but missing from lockfile")
1260+
end
1261+
12241262
it "auto-heals when the lockfile is missing specs" do
12251263
build_repo4 do
12261264
build_gem "minitest-bisect", "1.6.0" do |s|

spec/bundler/support/helpers.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,14 +414,14 @@ def realworld_system_gems(*gems)
414414
end
415415
end
416416

417-
def cache_gems(*gems)
417+
def cache_gems(*gems, gem_repo: gem_repo1)
418418
gems = gems.flatten
419419

420420
FileUtils.rm_rf("#{bundled_app}/vendor/cache")
421421
FileUtils.mkdir_p("#{bundled_app}/vendor/cache")
422422

423423
gems.each do |g|
424-
path = "#{gem_repo1}/gems/#{g}.gem"
424+
path = "#{gem_repo}/gems/#{g}.gem"
425425
raise "OMG `#{path}` does not exist!" unless File.exist?(path)
426426
FileUtils.cp(path, "#{bundled_app}/vendor/cache")
427427
end

0 commit comments

Comments
 (0)