Skip to content

Commit 3fe9b33

Browse files
hsbtclaude
andcommitted
Pin lockfile dependencies to the version their section resolved
The parser removed in #9564 turned a `!` entry in the DEPENDENCIES section into a `= <version>` requirement. Bundler's LockfileParser records the pinned source on the dependency and leaves the requirement at `>= 0`, so `gem install -g` could resolve a newer published version over the locked GIT or PATH one. Keeping the version as each section is read pins prereleases too, and lets a PATH section pin to the gemspec on disk the way the old parser did. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 823fc61 commit 3fe9b33

2 files changed

Lines changed: 104 additions & 4 deletions

File tree

lib/rubygems/request_set.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,8 @@ def load_lockfile(lock_file) # :nodoc:
349349

350350
parser = Bundler::LockfileParser.new(File.read(lock_file), lockfile_path: lock_file)
351351

352+
locked_versions = {}
353+
352354
parser.specs.group_by(&:source).each do |source, specs|
353355
case source
354356
when Bundler::Source::Rubygems
@@ -357,6 +359,7 @@ def load_lockfile(lock_file) # :nodoc:
357359
lock_set = Gem::Resolver::LockSet.new(remotes)
358360
specs.each do |spec|
359361
added = lock_set.add(spec.name, spec.version.to_s, spec.platform)
362+
locked_versions[spec.name] ||= spec.version
360363
spec.dependencies.each do |dep|
361364
added.each {|s| s.add_dependency dep }
362365
end
@@ -373,21 +376,33 @@ def load_lockfile(lock_file) # :nodoc:
373376
source.revision,
374377
source.submodules || false
375378
)
379+
locked_versions[spec.name] ||= spec.version
376380
spec.dependencies.each {|dep| git_spec.add_dependency dep }
377381
end
378382
@sets << git_set
379383
when Bundler::Source::Path
380384
vendor_set = Gem::Resolver::VendorSet.new
381385
specs.each do |spec|
382386
loaded = vendor_set.add_vendor_gem(spec.name, source.path.to_s)
387+
locked_versions[spec.name] ||= loaded.version
383388
spec.dependencies.each {|dep| loaded.dependencies << dep }
384389
end
385390
@sets << vendor_set
386391
end
387392
end
388393

389394
parser.dependencies.each_value do |dep|
390-
gem dep.name, *dep.requirement.as_list
395+
requirements = dep.requirement.as_list
396+
397+
# A dependency the lockfile ties to a source replaces whatever it asks
398+
# for with the version that source resolved, the way the parser this
399+
# replaced did. For a PATH section that is the version of the gemspec on
400+
# disk, not the one the lockfile records.
401+
if dep.source && (version = locked_versions[dep.name])
402+
requirements = [version]
403+
end
404+
405+
gem dep.name, *requirements
391406
end
392407
ensure
393408
Bundler.instance_variable_set(:@root, previous_root) if defined?(previous_root)

test/rubygems/test_gem_request_set.rb

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ def test_load_gemdeps_with_lockfile_gem_section
354354
a (1)
355355
b (1)
356356
a (~> 1.0)
357+
b (3-x86_64-linux)
357358
358359
PLATFORMS
359360
#{Gem::Platform::RUBY}
@@ -365,9 +366,23 @@ def test_load_gemdeps_with_lockfile_gem_section
365366

366367
rs.load_gemdeps "gem.deps.rb"
367368

369+
assert_equal [dep("b")], rs.dependencies
370+
368371
lock_set = rs.sets.find {|set| Gem::Resolver::LockSet === set }
369372
refute_nil lock_set, "LockSet should be created from GEM section"
370-
assert_equal %w[a-1 b-1], lock_set.specs.map(&:full_name).sort
373+
assert_equal %w[a-1 b-1 b-3], lock_set.specs.map(&:full_name).sort
374+
375+
expected = [
376+
Gem::Platform::RUBY,
377+
Gem::Platform::RUBY,
378+
Gem::Platform.new("x86_64-linux"),
379+
]
380+
381+
assert_equal expected, lock_set.specs.sort_by(&:full_name).map(&:platform)
382+
383+
spec = lock_set.specs.find {|s| s.full_name == "b-1" }
384+
385+
assert_equal [dep("a", "~> 1.0")], spec.dependencies
371386
end
372387

373388
def test_load_gemdeps_with_lockfile_git_section
@@ -383,7 +398,9 @@ def test_load_gemdeps_with_lockfile_git_section
383398
remote: git://example/a.git
384399
revision: deadbeef
385400
specs:
386-
a (1)
401+
a (2)
402+
b (>= 3)
403+
c
387404
388405
PLATFORMS
389406
#{Gem::Platform::RUBY}
@@ -395,9 +412,42 @@ def test_load_gemdeps_with_lockfile_git_section
395412

396413
rs.load_gemdeps "gem.deps.rb"
397414

415+
assert_equal [dep("a", "= 2")], rs.dependencies
416+
398417
git_set = rs.sets.find {|set| Gem::Resolver::GitSet === set }
399418
refute_nil git_set, "GitSet should be created from GIT section"
400-
assert_includes git_set.specs.keys, "a"
419+
assert_equal %w[a-2], git_set.specs.values.map(&:full_name)
420+
421+
assert_equal [dep("b", ">= 3"), dep("c")],
422+
git_set.specs.values.first.dependencies
423+
end
424+
425+
def test_load_gemdeps_with_lockfile_git_section_prerelease
426+
rs = Gem::RequestSet.new
427+
428+
File.open "gem.deps.rb", "w" do |io|
429+
io.puts 'gem "a", :git => "git://example/a.git"'
430+
end
431+
432+
File.open "gem.deps.rb.lock", "w" do |io|
433+
io.puts <<~LOCKFILE
434+
GIT
435+
remote: git://example/a.git
436+
revision: deadbeef
437+
specs:
438+
a (1.0.0.pre1)
439+
440+
PLATFORMS
441+
#{Gem::Platform::RUBY}
442+
443+
DEPENDENCIES
444+
a!
445+
LOCKFILE
446+
end
447+
448+
rs.load_gemdeps "gem.deps.rb"
449+
450+
assert_equal [dep("a", "= 1.0.0.pre1")], rs.dependencies
401451
end
402452

403453
def test_load_gemdeps_with_lockfile_path_section
@@ -426,11 +476,46 @@ def test_load_gemdeps_with_lockfile_path_section
426476

427477
rs.load_gemdeps "gem.deps.rb"
428478

479+
assert_equal [dep("a", "= 1")], rs.dependencies
480+
429481
vendor_set = rs.sets.find {|set| Gem::Resolver::VendorSet === set }
430482
refute_nil vendor_set, "VendorSet should be created from PATH section"
431483
assert_equal %w[a-1], vendor_set.specs.values.map(&:full_name)
432484
end
433485

486+
def test_load_gemdeps_with_lockfile_path_section_newer_than_lockfile
487+
_, _, directory = vendor_gem "a", 2
488+
489+
rs = Gem::RequestSet.new
490+
491+
File.open "gem.deps.rb", "w" do |io|
492+
io.puts "gem \"a\", :path => #{directory.inspect}"
493+
end
494+
495+
File.open "gem.deps.rb.lock", "w" do |io|
496+
io.puts <<~LOCKFILE
497+
PATH
498+
remote: #{directory}
499+
specs:
500+
a (1)
501+
502+
PLATFORMS
503+
#{Gem::Platform::RUBY}
504+
505+
DEPENDENCIES
506+
a!
507+
LOCKFILE
508+
end
509+
510+
rs.load_gemdeps "gem.deps.rb"
511+
512+
assert_equal [dep("a", "= 2")], rs.dependencies
513+
514+
vendor_set = rs.sets.find {|set| Gem::Resolver::VendorSet === set }
515+
assert_equal %w[a-2], vendor_set.specs.values.map(&:full_name)
516+
assert_equal %w[a-2], vendor_set.find_all(dep("a", "= 2")).map(&:full_name)
517+
end
518+
434519
def test_load_gemdeps_with_missing_lockfile
435520
rs = Gem::RequestSet.new
436521

0 commit comments

Comments
 (0)