Skip to content

Commit cb2ea29

Browse files
rwstaunermatzbot
authored andcommitted
[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 (<old version>) 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. ruby/rubygems@bdfd8d4f55
1 parent 2d894ae commit cb2ea29

2 files changed

Lines changed: 106 additions & 4 deletions

File tree

lib/bundler/lockfile_generator.rb

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,40 @@ def add_section(name, value)
103103
end
104104

105105
def bundler_checksum
106+
# `.dev` versions and `SKIP_BUNDLER_CHECKSUM` are deliberate opt-outs (used
107+
# by Bundler/RubyGems' own development and release tasks): never record a
108+
# checksum for Bundler itself in those cases.
106109
return [] if Bundler.gem_version.to_s.end_with?(".dev") || ENV["SKIP_BUNDLER_CHECKSUM"]
107110

108111
bundler_spec = definition.sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last
109-
return [] unless File.exist?(bundler_spec.cache_file)
110112

111-
require "rubygems/package"
113+
# Record a fresh checksum from the locally cached gem when it's available.
114+
# When it isn't (e.g. a fresh checkout/CI that never downloaded the bundler
115+
# gem), fall back to whatever checksum is already locked rather than
116+
# dropping it, so the entry stays consistent across environments.
117+
if File.exist?(bundler_spec.cache_file)
118+
require "rubygems/package"
119+
120+
package = Gem::Package.new(bundler_spec.cache_file)
121+
definition.sources.metadata_source.checksum_store.register(bundler_spec, Checksum.from_gem_package(package))
122+
elsif bundled_with_changing?
123+
# We can't compute a fresh checksum (the bundler gem isn't cached) and the
124+
# BUNDLED WITH version is changing. Keeping the previously locked checksum
125+
# would leave a `bundler (<old version>) sha256=...` entry that no longer
126+
# matches the new BUNDLED WITH version, so drop it instead.
127+
return []
128+
end
112129

113-
package = Gem::Package.new(bundler_spec.cache_file)
114-
definition.sources.metadata_source.checksum_store.register(bundler_spec, Checksum.from_gem_package(package))
130+
return [] if definition.sources.metadata_source.checksum_store.missing?(bundler_spec)
115131

116132
[definition.sources.metadata_source.checksum_store.to_lock(bundler_spec)]
117133
end
134+
135+
def bundled_with_changing?
136+
locked_gems = definition.locked_gems
137+
return false unless locked_gems
138+
139+
locked_gems.bundler_version != definition.bundler_version_to_lock
140+
end
118141
end
119142
end

spec/bundler/commands/update_spec.rb

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,6 +1791,85 @@
17911791
expect(out).to include("Using bundler 9.0.0")
17921792
end
17931793

1794+
it "preserves the locked bundler checksum when re-locking without the bundler gem cached" do
1795+
system_gems "bundler-9.0.0"
1796+
1797+
build_repo4 do
1798+
build_gem "myrack", "1.0"
1799+
build_gem "weakling", "0.0.3"
1800+
1801+
build_bundler "9.0.0"
1802+
end
1803+
1804+
install_gemfile <<-G
1805+
source "https://gem.repo4"
1806+
gem "myrack"
1807+
G
1808+
1809+
system_gems "bundler-9.0.0", path: local_gem_path
1810+
bundle :update, bundler: "9.0.0", verbose: true
1811+
1812+
# Sanity check: the lockfile now records the bundler checksum.
1813+
expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/)
1814+
1815+
# Simulate a machine where the bundler gem is not present in the cache
1816+
# (e.g. a fresh CI checkout that never downloaded bundler-9.0.0.gem).
1817+
FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")]
1818+
FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")]
1819+
1820+
# Force a re-resolution / lockfile rewrite.
1821+
install_gemfile <<-G
1822+
source "https://gem.repo4"
1823+
gem "myrack"
1824+
gem "weakling"
1825+
G
1826+
1827+
# The bundler checksum must survive the rewrite, since it was already locked.
1828+
expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/)
1829+
end
1830+
1831+
it "drops the locked bundler checksum when the bundler version changes and the gem isn't cached" do
1832+
system_gems "bundler-9.0.0"
1833+
1834+
build_repo4 do
1835+
build_gem "myrack", "1.0"
1836+
1837+
build_bundler "9.0.0"
1838+
build_bundler "9.9.9"
1839+
end
1840+
1841+
install_gemfile <<-G
1842+
source "https://gem.repo4"
1843+
gem "myrack"
1844+
G
1845+
1846+
system_gems "bundler-9.0.0", path: local_gem_path
1847+
bundle :update, bundler: "9.0.0", verbose: true
1848+
1849+
# Sanity check: the lockfile records the bundler 9.0.0 checksum and is
1850+
# locked to bundler 9.0.0.
1851+
expect(lockfile).to match(/^ bundler \(9\.0\.0\) sha256=/)
1852+
expect(lockfile).to match(/BUNDLED WITH\n\s+9\.0\.0\n/)
1853+
1854+
# Simulate a machine where the bundler gem is not present in the cache
1855+
# (e.g. a fresh CI checkout), so a fresh checksum can't be computed.
1856+
FileUtils.rm_f Dir[local_gem_path("cache", "bundler-9.0.0.gem")]
1857+
FileUtils.rm_f Dir[system_gem_path("cache", "bundler-9.0.0.gem")]
1858+
1859+
# Change the locked bundler version. `bundle lock --update --bundler` rewrites
1860+
# the BUNDLED WITH section without switching the running bundler, so the gem
1861+
# whose checksum is locked (9.0.0) is no longer the version being locked.
1862+
bundle "lock --update --bundler 9.9.9", verbose: true
1863+
1864+
# The BUNDLED WITH version was bumped...
1865+
expect(lockfile).to match(/BUNDLED WITH\n\s+9\.9\.9\n/)
1866+
1867+
# ...so the stale `bundler (9.0.0)` checksum must be dropped rather than kept,
1868+
# otherwise we'd lock a checksum that no longer matches the BUNDLED WITH
1869+
# version (and that we can't recompute since the gem isn't cached).
1870+
expect(lockfile).not_to match(/^ bundler \(/)
1871+
end
1872+
17941873
it "prints an error when trying to update bundler in frozen mode" do
17951874
system_gems "bundler-9.0.0"
17961875

0 commit comments

Comments
 (0)