Skip to content

Commit 8a33b2e

Browse files
committed
Remove the short-name content-addressed gem after a wide-named identical gem has been installed
1 parent 5dc08ea commit 8a33b2e

3 files changed

Lines changed: 148 additions & 0 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 spec.content_address && spec.content_address.length > Gem::ContentAddress::DEFAULT_LENGTH
72+
remove_stale_matching_gems
73+
end
74+
7175
spec
7276
end
7377

lib/rubygems/installer.rb

Lines changed: 25 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.
@@ -314,6 +315,10 @@ def install
314315

315316
run_post_install_hooks
316317

318+
if spec.content_address && spec.content_address.length > Gem::ContentAddress::DEFAULT_LENGTH
319+
remove_stale_matching_gems
320+
end
321+
317322
spec
318323
rescue Errno::EACCES => e
319324
# Permission denied - /path/to/foo
@@ -973,6 +978,26 @@ def ensure_writable_dir(dir) # :nodoc:
973978

974979
private
975980

981+
def remove_stale_matching_gems
982+
require "digest"
983+
incoming_sha = Digest::SHA256.file(gem).hexdigest
984+
installed_specs.each do |installed_spec|
985+
next unless installed_spec.name == spec.name
986+
next unless installed_spec.version == spec.version
987+
next unless installed_spec.platform == spec.platform
988+
next unless installed_spec.ruby_abi == spec.ruby_abi
989+
next unless installed_spec.content_address&.length == Gem::ContentAddress::DEFAULT_LENGTH
990+
next unless spec.content_address.start_with?(installed_spec.content_address)
991+
next unless File.exist?(installed_spec.cache_file)
992+
installed_sha = Digest::SHA256.file(installed_spec.cache_file).hexdigest
993+
next unless installed_sha == incoming_sha
994+
995+
uninstaller = Gem::Uninstaller.new(nil, install_dir: gem_home)
996+
uninstaller.remove(installed_spec)
997+
Gem::Specification.remove_spec(installed_spec) unless @install_dir
998+
end
999+
end
1000+
9761001
def assign_content_address
9771002
address = @package.content_address
9781003
expected = options[:content_address]

test/rubygems/test_gem_installer.rb

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

1393+
def test_reinstalling_a_content_addressed_gem_with_a_widened_sha_removes_the_old_gem
1394+
source_spec, a_gem = util_gem("a", 2) do |spec|
1395+
spec.required_ruby_version = "~> 3.4.0"
1396+
spec.platform = "x86_64-linux"
1397+
end
1398+
FileUtils.rm_rf source_spec.gem_dir
1399+
1400+
digest = Digest::SHA256.file(a_gem).hexdigest
1401+
address = digest[0, 8]
1402+
dir = File.dirname(a_gem)
1403+
filename = File.join(dir, "a-2-#{address}.gem")
1404+
FileUtils.cp a_gem, filename
1405+
installer = Gem::Installer.at filename, install_dir: @gemhome, force: true
1406+
short_spec = installer.install
1407+
short_extension_dir = short_spec.extension_dir
1408+
FileUtils.mkdir_p short_extension_dir
1409+
assert_path_exist short_extension_dir
1410+
1411+
widened_address = digest[0, 12]
1412+
widened_filename = File.join(dir, "a-2-#{widened_address}.gem")
1413+
FileUtils.cp a_gem, widened_filename
1414+
installer2 = Gem::Installer.at widened_filename, install_dir: @gemhome, force: true
1415+
spec2 = installer2.install
1416+
1417+
assert_path_not_exist short_extension_dir
1418+
assert_equal "a-2-#{widened_address}", spec2.full_name
1419+
assert_path_exist File.join(@gemhome, "gems", "a-2-#{widened_address}")
1420+
assert_path_exist File.join(@gemhome, "specifications", "a-2-#{widened_address}.gemspec")
1421+
assert_equal 1, Dir[File.join(@gemhome, "gems", "a-2*")].size
1422+
assert_equal 1, Dir[File.join(@gemhome, "specifications", "a-2*.gemspec")].size
1423+
end
1424+
1425+
def test_reinstalling_with_a_widened_sha_removes_the_old_specification_from_the_record
1426+
source_spec, a_gem = util_gem("a", 2) do |spec|
1427+
spec.required_ruby_version = "~> 3.4.0"
1428+
spec.platform = "x86_64-linux"
1429+
end
1430+
FileUtils.rm_rf source_spec.gem_dir
1431+
1432+
digest = Digest::SHA256.file(a_gem).hexdigest
1433+
address = digest[0, 8]
1434+
dir = File.dirname(a_gem)
1435+
filename = File.join(dir, "a-2-#{address}.gem")
1436+
FileUtils.cp a_gem, filename
1437+
short_spec = Gem::Installer.at(filename, force: true).install
1438+
1439+
assert_includes Gem::Specification.stubs.map(&:full_name), short_spec.full_name
1440+
1441+
widened_address = digest[0, 12]
1442+
widened_filename = File.join(dir, "a-2-#{widened_address}.gem")
1443+
FileUtils.cp a_gem, widened_filename
1444+
widened_spec = Gem::Installer.at(widened_filename, force: true).install
1445+
1446+
refute_includes Gem::Specification.stubs.map(&:full_name), short_spec.full_name
1447+
assert_includes Gem::Specification.stubs.map(&:full_name), widened_spec.full_name
1448+
end
1449+
1450+
def test_installing_widened_gem_does_not_remove_short_gem_when_checksums_differ
1451+
source_spec, a_gem = util_gem("a", 2) do |spec|
1452+
spec.required_ruby_version = "~> 3.4.0"
1453+
spec.platform = "x86_64-linux"
1454+
end
1455+
FileUtils.rm_rf source_spec.gem_dir
1456+
1457+
digest = Digest::SHA256.file(a_gem).hexdigest
1458+
address = digest[0, 8]
1459+
dir = File.dirname(a_gem)
1460+
filename = File.join(dir, "a-2-#{address}.gem")
1461+
FileUtils.cp a_gem, filename
1462+
installer = Gem::Installer.at filename, install_dir: @gemhome, force: true
1463+
short_spec = installer.install
1464+
File.binwrite(short_spec.cache_file, "different gem contents")
1465+
1466+
widened_address = digest[0, 12]
1467+
widened_filename = File.join(dir, "a-2-#{widened_address}.gem")
1468+
FileUtils.cp a_gem, widened_filename
1469+
installer2 = Gem::Installer.at widened_filename, install_dir: @gemhome, force: true
1470+
spec2 = installer2.install
1471+
1472+
assert_path_exist File.join(@gemhome, "gems", "a-2-#{address}")
1473+
assert_path_exist File.join(@gemhome, "specifications", "a-2-#{address}.gemspec")
1474+
assert_equal "a-2-#{widened_address}", spec2.full_name
1475+
assert_path_exist File.join(@gemhome, "gems", "a-2-#{widened_address}")
1476+
assert_path_exist File.join(@gemhome, "specifications", "a-2-#{widened_address}.gemspec")
1477+
assert_equal 2, Dir[File.join(@gemhome, "gems", "a-2*")].size
1478+
assert_equal 2, Dir[File.join(@gemhome, "specifications", "a-2*.gemspec")].size
1479+
assert_path_exist short_spec.cache_file
1480+
end
1481+
1482+
def test_failed_widened_install_does_not_remove_existing_short_gem
1483+
source_spec, a_gem = util_gem("a", 2) do |spec|
1484+
spec.required_ruby_version = "~> 3.4.0"
1485+
spec.platform = "x86_64-linux"
1486+
end
1487+
FileUtils.rm_rf source_spec.gem_dir
1488+
1489+
digest = Digest::SHA256.file(a_gem).hexdigest
1490+
address = digest[0, 8]
1491+
dir = File.dirname(a_gem)
1492+
filename = File.join(dir, "a-2-#{address}.gem")
1493+
FileUtils.cp a_gem, filename
1494+
short_spec = Gem::Installer.at(filename, install_dir: @gemhome, force: true).install
1495+
1496+
widened_address = digest[0, 12]
1497+
widened_filename = File.join(dir, "a-2-#{widened_address}.gem")
1498+
FileUtils.cp a_gem, widened_filename
1499+
widened_installer = Gem::Installer.at widened_filename, install_dir: @gemhome, force: true
1500+
1501+
Gem.pre_install { false }
1502+
1503+
assert_raise Gem::InstallError do
1504+
widened_installer.install
1505+
end
1506+
1507+
assert_path_exist short_spec.full_gem_path
1508+
assert_path_exist short_spec.spec_file
1509+
assert_path_exist short_spec.cache_file
1510+
end
1511+
13931512
def test_install_assigns_content_address_from_filename_with_full_sha
13941513
_, a_gem = util_gem("a", 2) do |spec|
13951514
spec.required_ruby_version = "~> 3.4.0"

0 commit comments

Comments
 (0)