Skip to content

Commit a6d8f25

Browse files
committed
Remove the short-name content-addressed gem after a wide-named identical gem has been installed
1 parent 973616b commit a6d8f25

6 files changed

Lines changed: 196 additions & 3 deletions

File tree

lib/bundler/rubygems_gem_installer.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ def install
6868

6969
run_post_install_hooks
7070

71+
if Gem::ContentAddress.widened?(spec.content_address)
72+
remove_stale_matching_gems
73+
end
74+
7175
spec
7276
end
7377

lib/rubygems/content_address.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ def self.match?(value)
3434
value.is_a?(String) && PATTERN.match?(value)
3535
end
3636

37+
##
38+
# Whether +value+ is a valid content address longer than the default.
39+
40+
def self.widened?(value)
41+
match?(value) && value.length > DEFAULT_LENGTH
42+
end
43+
3744
##
3845
# Whether +value+ is a well-formed Ruby ABI ("X.Y").
3946

lib/rubygems/installer.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
require_relative "package"
1212
require_relative "ext"
1313
require_relative "user_interaction"
14+
require_relative "uninstaller"
1415

1516
##
1617
# The installer installs the files contained in the .gem into the Gem.home.
@@ -318,6 +319,10 @@ def install
318319

319320
run_post_install_hooks
320321

322+
if Gem::ContentAddress.widened?(spec.content_address)
323+
remove_stale_matching_gems
324+
end
325+
321326
spec
322327
rescue Errno::EACCES => e
323328
# Permission denied - /path/to/foo
@@ -1000,6 +1005,41 @@ def incompatible_abi_install?
10001005
Gem::ContentAddress.content_addressed?(spec) && spec.ruby_abi != Gem.ruby_abi
10011006
end
10021007

1008+
def remove_stale_matching_gems
1009+
require "digest"
1010+
incoming_sha = Digest::SHA256.file(gem).hexdigest
1011+
1012+
canonical_gem_home = File.exist?(gem_home) ? File.realpath(gem_home) : gem_home
1013+
1014+
spec_dirs = [
1015+
File.join(canonical_gem_home, "specifications"),
1016+
Gem::SpecificationRecord.specification_dir_for(spec, canonical_gem_home),
1017+
].uniq
1018+
1019+
cleanup_specs = spec_dirs.flat_map do |dir|
1020+
Gem::Util.glob_files_in_dir("*.gemspec", dir).filter_map do |path|
1021+
Gem::Specification.load(path)
1022+
end
1023+
end
1024+
1025+
cleanup_specs.each do |installed_spec|
1026+
next unless installed_spec.name == spec.name
1027+
next unless installed_spec.version == spec.version
1028+
next unless installed_spec.platform == spec.platform
1029+
next unless installed_spec.ruby_abi == spec.ruby_abi
1030+
next unless installed_spec.content_address&.length == Gem::ContentAddress::DEFAULT_LENGTH
1031+
next unless spec.content_address.start_with?(installed_spec.content_address)
1032+
next unless File.exist?(installed_spec.cache_file)
1033+
installed_sha = Digest::SHA256.file(installed_spec.cache_file).hexdigest
1034+
next unless installed_sha == incoming_sha
1035+
1036+
uninstaller = Gem::Uninstaller.new(nil, install_dir: canonical_gem_home)
1037+
uninstaller.remove(installed_spec)
1038+
FileUtils.rm_f installed_spec.build_info_file
1039+
Gem::Specification.remove_spec(installed_spec) unless @install_dir
1040+
end
1041+
end
1042+
10031043
def assign_content_address
10041044
address = @package.content_address
10051045
expected = options[:content_address]

spec/install/gemfile/content_addressable_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
short_cache = Dir[default_bundle_path("cache", "mygem-1.0-*.gem").to_s].first
8181
short_address = File.basename(short_cache, ".gem").rpartition("-").last
8282
short_gem_dir = default_bundle_path("gems", "mygem-1.0-#{short_address}")
83-
short_gemspec = default_bundle_path("specifications", "mygem-1.0-#{short_address}.gemspec")
83+
short_gemspec = default_bundle_path("specifications", current_abi, "mygem-1.0-#{short_address}.gemspec")
8484

8585
expect(short_gem_dir).to exist
8686
expect(short_gemspec).to exist
@@ -101,7 +101,7 @@
101101
bundle "install --redownload", artifice: "compact_index_v2", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
102102

103103
expect(default_bundle_path("gems", "mygem-1.0-#{widened_address}")).to exist
104-
expect(default_bundle_path("specifications", "mygem-1.0-#{widened_address}.gemspec")).to exist
104+
expect(default_bundle_path("specifications", current_abi, "mygem-1.0-#{widened_address}.gemspec")).to exist
105105
expect(default_bundle_path("cache", "mygem-1.0-#{widened_address}.gem")).to exist
106106
expect(short_gem_dir).not_to exist
107107
expect(short_gemspec).not_to exist
@@ -147,7 +147,7 @@
147147

148148
expect(lockfile).to include("mygem (1.0-x86_64-linux) #{widened_address}")
149149
expect(default_bundle_path("gems", "mygem-1.0-#{widened_address}")).to exist
150-
expect(default_bundle_path("specifications", "mygem-1.0-#{widened_address}.gemspec")).to exist
150+
expect(default_bundle_path("specifications", current_abi, "mygem-1.0-#{widened_address}.gemspec")).to exist
151151
expect(default_bundle_path("cache", "mygem-1.0-#{widened_address}.gem")).to exist
152152
expect(lockfile).not_to match(/mygem \(1\.0-x86_64-linux\) #{short_address}\s/)
153153
end

test/rubygems/test_gem_content_address.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ def test_match_rejects_uppercase
2626
refute Gem::ContentAddress.match?("ABCDEF12")
2727
end
2828

29+
def test_widened
30+
refute Gem::ContentAddress.widened?("a" * 8)
31+
assert Gem::ContentAddress.widened?("a" * 9)
32+
refute Gem::ContentAddress.widened?(nil)
33+
refute Gem::ContentAddress.widened?("not-an-address")
34+
end
35+
2936
def test_valid_ruby_abi
3037
assert Gem::ContentAddress.valid_ruby_abi?("3.4")
3138
assert Gem::ContentAddress.valid_ruby_abi?("10.0")

test/rubygems/test_gem_installer.rb

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,6 +1391,141 @@ def test_reinstalling_content_addressed_gem_is_idempotent
13911391
assert_equal 1, Dir[File.join(@gemhome, "specifications", "3.4", "a-2*.gemspec")].size
13921392
end
13931393

1394+
def test_reinstalling_a_content_addressed_gem_with_a_widened_sha_removes_the_old_gem
1395+
install_home = @gemhome
1396+
1397+
# Exercise cleanup through a symlinked gem home when supported.
1398+
if symlink_supported?
1399+
install_home = File.join(@tempdir, "symlinked_gemhome")
1400+
FileUtils.ln_s(@gemhome, install_home)
1401+
end
1402+
1403+
source_spec, a_gem = util_gem("a", 2) do |spec|
1404+
spec.required_ruby_version = "~> 3.4.0"
1405+
spec.platform = "x86_64-linux"
1406+
end
1407+
FileUtils.rm_rf source_spec.gem_dir
1408+
1409+
digest = Digest::SHA256.file(a_gem).hexdigest
1410+
address = digest[0, 8]
1411+
dir = File.dirname(a_gem)
1412+
filename = File.join(dir, "a-2-#{address}.gem")
1413+
FileUtils.cp a_gem, filename
1414+
installer = Gem::Installer.at filename, install_dir: install_home, force: true
1415+
1416+
# Make the short installation create a build info file
1417+
# To later ensure they are also removed
1418+
installer.build_args = ["--example"]
1419+
short_spec = installer.install
1420+
short_build_info_file = short_spec.build_info_file
1421+
assert_path_exist short_build_info_file
1422+
1423+
short_extension_dir = short_spec.extension_dir
1424+
FileUtils.mkdir_p short_extension_dir
1425+
assert_path_exist short_extension_dir
1426+
1427+
widened_address = digest[0, 12]
1428+
widened_filename = File.join(dir, "a-2-#{widened_address}.gem")
1429+
FileUtils.cp a_gem, widened_filename
1430+
installer2 = Gem::Installer.at widened_filename, install_dir: install_home, force: true
1431+
spec2 = installer2.install
1432+
1433+
assert_path_not_exist short_extension_dir
1434+
assert_equal "a-2-#{widened_address}", spec2.full_name
1435+
assert_path_exist File.join(@gemhome, "gems", "a-2-#{widened_address}")
1436+
assert_path_exist File.join(@gemhome, "specifications", "3.4", "a-2-#{widened_address}.gemspec")
1437+
assert_equal 1, Dir[File.join(@gemhome, "gems", "a-2*")].size
1438+
assert_equal 1, Dir[File.join(@gemhome, "specifications", "3.4", "a-2*.gemspec")].size
1439+
assert_path_not_exist short_build_info_file
1440+
end
1441+
1442+
def test_reinstalling_with_a_widened_sha_removes_the_old_specification_from_the_record
1443+
source_spec, a_gem = util_gem("a", 2) do |spec|
1444+
spec.required_ruby_version = "~> #{Gem.ruby_abi}.0"
1445+
spec.platform = Gem::Platform.local
1446+
end
1447+
FileUtils.rm_rf source_spec.gem_dir
1448+
1449+
digest = Digest::SHA256.file(a_gem).hexdigest
1450+
address = digest[0, 8]
1451+
dir = File.dirname(a_gem)
1452+
filename = File.join(dir, "a-2-#{address}.gem")
1453+
FileUtils.cp a_gem, filename
1454+
short_spec = Gem::Installer.at(filename, force: true).install
1455+
1456+
assert_includes Gem::Specification.stubs.map(&:full_name), short_spec.full_name
1457+
1458+
widened_address = digest[0, 12]
1459+
widened_filename = File.join(dir, "a-2-#{widened_address}.gem")
1460+
FileUtils.cp a_gem, widened_filename
1461+
widened_spec = Gem::Installer.at(widened_filename, force: true).install
1462+
1463+
refute_includes Gem::Specification.stubs.map(&:full_name), short_spec.full_name
1464+
assert_includes Gem::Specification.stubs.map(&:full_name), widened_spec.full_name
1465+
end
1466+
1467+
def test_installing_widened_gem_does_not_remove_short_gem_when_checksums_differ
1468+
source_spec, a_gem = util_gem("a", 2) do |spec|
1469+
spec.required_ruby_version = "~> 3.4.0"
1470+
spec.platform = "x86_64-linux"
1471+
end
1472+
FileUtils.rm_rf source_spec.gem_dir
1473+
1474+
digest = Digest::SHA256.file(a_gem).hexdigest
1475+
address = digest[0, 8]
1476+
dir = File.dirname(a_gem)
1477+
filename = File.join(dir, "a-2-#{address}.gem")
1478+
FileUtils.cp a_gem, filename
1479+
installer = Gem::Installer.at filename, install_dir: @gemhome, force: true
1480+
short_spec = installer.install
1481+
File.binwrite(short_spec.cache_file, "different gem contents")
1482+
1483+
widened_address = digest[0, 12]
1484+
widened_filename = File.join(dir, "a-2-#{widened_address}.gem")
1485+
FileUtils.cp a_gem, widened_filename
1486+
installer2 = Gem::Installer.at widened_filename, install_dir: @gemhome, force: true
1487+
spec2 = installer2.install
1488+
1489+
assert_path_exist File.join(@gemhome, "gems", "a-2-#{address}")
1490+
assert_path_exist short_spec.spec_file
1491+
assert_equal "a-2-#{widened_address}", spec2.full_name
1492+
assert_path_exist File.join(@gemhome, "gems", "a-2-#{widened_address}")
1493+
assert_path_exist spec2.spec_file
1494+
assert_equal 2, Dir[File.join(@gemhome, "gems", "a-2*")].size
1495+
assert_equal 2, Dir[File.join(File.dirname(spec2.spec_file), "a-2*.gemspec")].size
1496+
assert_path_exist short_spec.cache_file
1497+
end
1498+
1499+
def test_failed_widened_install_does_not_remove_existing_short_gem
1500+
source_spec, a_gem = util_gem("a", 2) do |spec|
1501+
spec.required_ruby_version = "~> 3.4.0"
1502+
spec.platform = "x86_64-linux"
1503+
end
1504+
FileUtils.rm_rf source_spec.gem_dir
1505+
1506+
digest = Digest::SHA256.file(a_gem).hexdigest
1507+
address = digest[0, 8]
1508+
dir = File.dirname(a_gem)
1509+
filename = File.join(dir, "a-2-#{address}.gem")
1510+
FileUtils.cp a_gem, filename
1511+
short_spec = Gem::Installer.at(filename, install_dir: @gemhome, force: true).install
1512+
1513+
widened_address = digest[0, 12]
1514+
widened_filename = File.join(dir, "a-2-#{widened_address}.gem")
1515+
FileUtils.cp a_gem, widened_filename
1516+
widened_installer = Gem::Installer.at widened_filename, install_dir: @gemhome, force: true
1517+
1518+
Gem.pre_install { false }
1519+
1520+
assert_raise Gem::InstallError do
1521+
widened_installer.install
1522+
end
1523+
1524+
assert_path_exist short_spec.full_gem_path
1525+
assert_path_exist short_spec.spec_file
1526+
assert_path_exist short_spec.cache_file
1527+
end
1528+
13941529
def test_install_assigns_content_address_from_filename_with_full_sha
13951530
_, a_gem = util_gem("a", 2) do |spec|
13961531
spec.required_ruby_version = "~> 3.4.0"

0 commit comments

Comments
 (0)