Skip to content

Commit df4a1dc

Browse files
segiddinsclaude
andcommitted
Look up gems for gem fetch through the compact index
`gem fetch` looks gems up through Gem::SpecFetcher, which asks a source for its entire index -- the compact index `versions` file when the source serves one, 23MB of it for rubygems.org on a cold cache, and the Marshal index otherwise -- and then fetches a Marshal gemspec for every version that matches. `gem install` resolves through Gem::Resolver::BestSet, which reads only the `info` file of the gem it is looking for. Look gems up through a BestSet instead, so `gem fetch a` reads `/info/a` rather than the whole index, and fetches only the gemspec of the version it downloads. Platform preference and the reporting of specs rejected for another platform are unchanged. Gem::Source now requires rubygems/remote_fetcher, which it has always used without loading: until now every caller of #dependency_resolver_set had loaded Gem::SpecFetcher, which requires it, first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 86cbb81 commit df4a1dc

3 files changed

Lines changed: 98 additions & 8 deletions

File tree

lib/rubygems/commands/fetch_command.rb

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require_relative "../command"
44
require_relative "../local_remote_options"
5+
require_relative "../resolver"
56
require_relative "../version_option"
67

78
class Gem::Commands::FetchCommand < Gem::Command
@@ -79,31 +80,59 @@ def fetch_gems
7980
platform = Gem.platforms.last
8081
gem_names = get_all_gem_names_and_versions
8182

83+
# A BestSet reads a source's compact index when it serves one, the same way
84+
# gems are looked up when installing them.
85+
remote_set = Gem::Resolver::BestSet.new
86+
8287
gem_names.each do |gem_name, gem_version|
8388
gem_version ||= version
8489
dep = Gem::Dependency.new gem_name, gem_version
8590
dep.prerelease = options[:prerelease]
8691
suppress_suggestions = !options[:suggest_alternate]
8792

88-
specs_and_sources, errors =
89-
Gem::SpecFetcher.fetcher.spec_for_dependency dep
93+
remote_specs, errors = find_remote_specs dep, remote_set
9094

9195
if platform
92-
filtered = specs_and_sources.select {|s,| s.platform == platform }
93-
specs_and_sources = filtered unless filtered.empty?
96+
filtered = remote_specs.select {|s| s.platform == platform }
97+
remote_specs = filtered unless filtered.empty?
9498
end
9599

96-
spec, source = specs_and_sources.max_by {|s,| s }
100+
remote_spec = remote_specs.max_by {|s| [s.version, Gem::Platform.sort_priority(s.platform)] }
97101

98-
if spec.nil?
102+
if remote_spec.nil?
99103
show_lookup_failure gem_name, gem_version, errors, suppress_suggestions, options[:domain]
100104
exit_code |= 2
101105
next
102106
end
103-
source.download spec
107+
108+
spec = remote_spec.spec
109+
remote_spec.source.download spec
104110
say "Downloaded #{spec.full_name}"
105111
end
106112

107113
exit_code
108114
end
115+
116+
# Find specs in +set+ that match +dep+ and can be used on this platform,
117+
# along with the reasons any other spec was rejected.
118+
119+
def find_remote_specs(dep, set)
120+
set.prerelease = dep.prerelease?
121+
122+
request = Gem::Resolver::DependencyRequest.new dep, nil
123+
124+
matching, mismatched = set.find_all(request).partition do |spec|
125+
Gem::Platform.match_spec? spec
126+
end
127+
128+
[matching, set.errors + platform_mismatches(mismatched)]
129+
end
130+
131+
def platform_mismatches(specs)
132+
specs.group_by {|spec| [spec.name, spec.version] }.map do |(name, version), group|
133+
mismatch = Gem::PlatformMismatch.new name, version
134+
group.each {|spec| mismatch.add_platform spec.platform.to_s }
135+
mismatch
136+
end
137+
end
109138
end

lib/rubygems/source.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require_relative "remote_fetcher"
34
require_relative "text"
45
##
56
# A Source knows how to list and fetch gems from a RubyGems marshal index.

test/rubygems/test_gem_commands_fetch_command.rb

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def test_execute_platform
8181

8282
@cmd.options[:args] = %w[a]
8383

84-
@fetcher.data["#{@gem_repo}latest_specs.#{Gem.marshal_version}.gz"] = util_gzip(Marshal.dump([
84+
@fetcher.data["#{@gem_repo}specs.#{Gem.marshal_version}.gz"] = util_gzip(Marshal.dump([
8585
Gem::NameTuple.new(a2_spec.name, a2_spec.version, a2_spec.platform),
8686
Gem::NameTuple.new(a2_universal_darwin_spec.name, a2_universal_darwin_spec.version, a2_universal_darwin_spec.platform),
8787
]))
@@ -100,6 +100,66 @@ def test_execute_platform
100100
"#{a2_universal_darwin_spec.full_name} not fetched")
101101
end
102102

103+
def test_execute_compact_index
104+
specs = spec_fetcher do |fetcher|
105+
fetcher.gem "a", 1
106+
fetcher.gem "a", 2
107+
end
108+
109+
util_setup_compact_index(*specs.values)
110+
111+
@cmd.options[:args] = %w[a]
112+
113+
execute_with_exit_code
114+
115+
a2 = specs["a-2"]
116+
117+
assert_path_exist(File.join(@tempdir, a2.file_name),
118+
"#{a2.full_name} not fetched")
119+
120+
assert_includes @fetcher.paths, "#{@gem_repo}info/a"
121+
refute_includes @fetcher.paths, "#{@gem_repo}specs.#{Gem.marshal_version}.gz"
122+
end
123+
124+
def test_execute_compact_index_platform
125+
specs = spec_fetcher do |fetcher|
126+
fetcher.gem "a", 2
127+
fetcher.gem("a", 2) {|s| s.platform = "universal-darwin" }
128+
end
129+
130+
util_setup_compact_index(*specs.values)
131+
132+
@cmd.options[:args] = %w[a]
133+
134+
util_set_arch "arm64-darwin20" do
135+
execute_with_exit_code
136+
end
137+
138+
a2_universal_darwin = specs["a-2-universal-darwin"]
139+
140+
assert_path_exist(File.join(@tempdir, a2_universal_darwin.file_name),
141+
"#{a2_universal_darwin.full_name} not fetched")
142+
end
143+
144+
def test_execute_compact_index_platform_mismatch
145+
specs = spec_fetcher do |fetcher|
146+
fetcher.spec("a", 2) {|s| s.platform = "java" }
147+
end
148+
149+
util_setup_compact_index(*specs.values)
150+
151+
@cmd.options[:args] = %w[a]
152+
153+
execute_with_term_error
154+
155+
expected = <<-EXPECTED
156+
ERROR: Could not find a valid gem 'a' (>= 0), here is why:
157+
Found a (2), but was for platform java
158+
EXPECTED
159+
160+
assert_equal expected, @ui.error
161+
end
162+
103163
def test_execute_specific_prerelease
104164
specs = spec_fetcher do |fetcher|
105165
fetcher.gem "a", 2

0 commit comments

Comments
 (0)