From cb2ea293c6030e9585aa86fbd7b8a3170edd34e2 Mon Sep 17 00:00:00 2001 From: Randy Stauner Date: Mon, 29 Jun 2026 15:05:28 -0700 Subject: [PATCH] [ruby/rubygems] Preserve the locked Bundler checksum when the gem isn't cached `bundler_checksum` recomputed the Bundler self-checksum from the locally cached gem on every lockfile write and emitted nothing when that gem was absent. As a result an existing `bundler (x.y.z) sha256=...` entry was silently dropped whenever the lockfile was rewritten on a machine that never downloaded the Bundler gem (e.g. a fresh checkout or CI), making the entry flicker in and out depending on the environment. Fall back to the checksum already locked in the lockfile (parsed into the metadata source's checksum store and merged during source convergence) when a fresh one can't be computed, so the entry stays consistent across environments. A cached gem whose digest disagrees with the locked value still raises ChecksumMismatchError. Guard that fallback so it only applies when the `BUNDLED WITH` version isn't changing (`locked_gems.bundler_version == bundler_version_to_lock`). When the locked Bundler version is being bumped and we can't compute a fresh checksum (the gem isn't cached), drop the entry instead of keeping it. Otherwise we'd lock a `bundler () sha256=...` checksum that no longer matches the new `BUNDLED WITH` version, e.g. after `bundle lock --update --bundler` rewrites the section without switching the running Bundler. The `.dev` and SKIP_BUNDLER_CHECKSUM opt-outs are unchanged: they remain hard 'do not record' signals used by Bundler/RubyGems' own development and release tasks. https://github.com/ruby/rubygems/commit/bdfd8d4f55 --- lib/bundler/lockfile_generator.rb | 31 +++++++++-- spec/bundler/commands/update_spec.rb | 79 ++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/lib/bundler/lockfile_generator.rb b/lib/bundler/lockfile_generator.rb index 2a3ad2248058ff..8ed50139642040 100644 --- a/lib/bundler/lockfile_generator.rb +++ b/lib/bundler/lockfile_generator.rb @@ -103,17 +103,40 @@ def add_section(name, value) end def bundler_checksum + # `.dev` versions and `SKIP_BUNDLER_CHECKSUM` are deliberate opt-outs (used + # by Bundler/RubyGems' own development and release tasks): never record a + # checksum for Bundler itself in those cases. return [] if Bundler.gem_version.to_s.end_with?(".dev") || ENV["SKIP_BUNDLER_CHECKSUM"] bundler_spec = definition.sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last - return [] unless File.exist?(bundler_spec.cache_file) - require "rubygems/package" + # Record a fresh checksum from the locally cached gem when it's available. + # When it isn't (e.g. a fresh checkout/CI that never downloaded the bundler + # gem), fall back to whatever checksum is already locked rather than + # dropping it, so the entry stays consistent across environments. + if File.exist?(bundler_spec.cache_file) + require "rubygems/package" + + package = Gem::Package.new(bundler_spec.cache_file) + definition.sources.metadata_source.checksum_store.register(bundler_spec, Checksum.from_gem_package(package)) + elsif bundled_with_changing? + # We can't compute a fresh checksum (the bundler gem isn't cached) and the + # BUNDLED WITH version is changing. Keeping the previously locked checksum + # would leave a `bundler () sha256=...` entry that no longer + # matches the new BUNDLED WITH version, so drop it instead. + return [] + end - package = Gem::Package.new(bundler_spec.cache_file) - definition.sources.metadata_source.checksum_store.register(bundler_spec, Checksum.from_gem_package(package)) + return [] if definition.sources.metadata_source.checksum_store.missing?(bundler_spec) [definition.sources.metadata_source.checksum_store.to_lock(bundler_spec)] end + + def bundled_with_changing? + locked_gems = definition.locked_gems + return false unless locked_gems + + locked_gems.bundler_version != definition.bundler_version_to_lock + end end end diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb index 907f81d756ff7e..e838468a2771eb 100644 --- a/spec/bundler/commands/update_spec.rb +++ b/spec/bundler/commands/update_spec.rb @@ -1791,6 +1791,85 @@ expect(out).to include("Using bundler 9.0.0") end + it "preserves the locked bundler checksum when re-locking without the bundler gem cached" do + system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + build_gem "weakling", "0.0.3" + + build_bundler "9.0.0" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + system_gems "bundler-9.0.0", path: local_gem_path + bundle :update, bundler: "9.0.0", verbose: true + + # Sanity check: the lockfile now records the bundler checksum. + expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) + + # Simulate a machine where the bundler gem is not present in the cache + # (e.g. a fresh CI checkout that never downloaded bundler-9.0.0.gem). + FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")] + FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")] + + # Force a re-resolution / lockfile rewrite. + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + gem "weakling" + G + + # The bundler checksum must survive the rewrite, since it was already locked. + expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) + end + + it "drops the locked bundler checksum when the bundler version changes and the gem isn't cached" do + system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "9.0.0" + build_bundler "9.9.9" + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + system_gems "bundler-9.0.0", path: local_gem_path + bundle :update, bundler: "9.0.0", verbose: true + + # Sanity check: the lockfile records the bundler 9.0.0 checksum and is + # locked to bundler 9.0.0. + expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/) + expect(lockfile).to match(/BUNDLED WITH\n\s+9\.0\.0\n/) + + # Simulate a machine where the bundler gem is not present in the cache + # (e.g. a fresh CI checkout), so a fresh checksum can't be computed. + FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")] + FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")] + + # Change the locked bundler version. `bundle lock --update --bundler` rewrites + # the BUNDLED WITH section without switching the running bundler, so the gem + # whose checksum is locked (9.0.0) is no longer the version being locked. + bundle "lock --update --bundler 9.9.9", verbose: true + + # The BUNDLED WITH version was bumped... + expect(lockfile).to match(/BUNDLED WITH\n\s+9\.9\.9\n/) + + # ...so the stale `bundler (9.0.0)` checksum must be dropped rather than kept, + # otherwise we'd lock a checksum that no longer matches the BUNDLED WITH + # version (and that we can't recompute since the gem isn't cached). + expect(lockfile).not_to match(/^ bundler \(/) + end + it "prints an error when trying to update bundler in frozen mode" do system_gems "bundler-9.0.0"