Skip to content

Commit f854b32

Browse files
girachawdajenshenny
authored andcommitted
Support content addressable gems in gem update, outdated, dependency, and fetch
1 parent c6a345d commit f854b32

15 files changed

Lines changed: 411 additions & 34 deletions

lib/rubygems/commands/dependency_command.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ def fetch_remote_specs(name, requirement, prerelease) # :nodoc:
6666
end
6767
end
6868

69-
ss.map {|tuple, source| source.fetch_spec(tuple) }
69+
fetcher.decode_content_addressable_tuples(ss).map do |tuple, source|
70+
spec = source.fetch_spec(tuple)
71+
spec.content_address = tuple.content_address if tuple.content_address
72+
spec
73+
end
7074
end
7175

7276
def fetch_specs(name_pattern, requirement, prerelease) # :nodoc:
@@ -85,7 +89,7 @@ def fetch_specs(name_pattern, requirement, prerelease) # :nodoc:
8589

8690
ensure_specs specs
8791

88-
specs.uniq.sort
92+
specs.uniq(&:full_name).sort
8993
end
9094

9195
def display_pipe(specs) # :nodoc:
@@ -148,9 +152,17 @@ def ensure_specs(specs) # :nodoc:
148152
terminate_interaction 1
149153
end
150154

155+
def content_address_annotation(spec) # :nodoc:
156+
return "" unless Gem::ContentAddress.content_addressed?(spec)
157+
158+
parts = ["Platform: #{spec.platform}"]
159+
parts << "Ruby ABI: #{spec.ruby_abi}" if spec.ruby_abi
160+
" (#{parts.join(" ")})"
161+
end
162+
151163
def print_dependencies(spec, level = 0) # :nodoc:
152164
response = String.new
153-
response << " " * level + "Gem #{spec.full_name}\n"
165+
response << " " * level + "Gem #{spec.full_name}#{content_address_annotation(spec)}\n"
154166
unless spec.dependencies.empty?
155167
spec.dependencies.sort_by(&:name).each do |dep|
156168
response << " " * level + " #{dep}\n"

lib/rubygems/commands/outdated_command.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def partition_by_cooldown(spec_tuples)
8888
embargoed = []
8989

9090
with_times = spec_tuples.map do |tup, source|
91-
[tup, source, source.created_at(tup.name, tup.version, tup.platform)]
91+
[tup, source, source.created_at_for_tuple(tup)]
9292
end
9393

9494
if !with_times.empty? && with_times.none? {|_, _, created_at| created_at }

lib/rubygems/commands/update_command.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def filter_cooldown_tuples(spec_tuples) # :nodoc:
194194
return spec_tuples unless @cooldown&.active?
195195

196196
with_times = spec_tuples.map do |tup, source|
197-
[tup, source, source.created_at(tup.name, tup.version, tup.platform)]
197+
[tup, source, source.created_at_for_tuple(tup)]
198198
end
199199

200200
if !with_times.empty? && with_times.none? {|_, _, created_at| created_at }

lib/rubygems/compact_index_client.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ class Gem::CompactIndexClient
2020
# info returns an Array of INFO Arrays. Each INFO Array has the following indices:
2121
INFO_NAME = 0
2222
INFO_VERSION = 1
23-
INFO_PLATFORM = 2
23+
INFO_SUFFIX = 2
2424
INFO_DEPS = 3
2525
INFO_REQS = 4
2626

27+
INFO_PLATFORM = INFO_SUFFIX
28+
deprecate_constant :INFO_PLATFORM
29+
2730
def self.debug
2831
return unless ENV["DEBUG_COMPACT_INDEX"]
2932
DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") }

lib/rubygems/query_utils.rb

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -156,22 +156,13 @@ def show_remote_gems(name)
156156
if args.empty?
157157
matching_tuples
158158
else
159-
decode_content_addressable_tuples(matching_tuples, latest: specs_type == :latest)
159+
fetcher.decode_content_addressable_tuples(matching_tuples, latest: specs_type == :latest)
160160
end
161161
end
162162

163163
output_query_results(spec_tuples)
164164
end
165165

166-
def decode_content_addressable_tuples(spec_tuples, latest: false)
167-
spec_tuples.group_by {|_, source| source }.flat_map do |source, source_tuples|
168-
next source_tuples unless source.respond_to?(:decode_content_addressable_tuples)
169-
170-
tuples = source_tuples.map(&:first)
171-
source.decode_content_addressable_tuples(tuples, latest: latest).map {|tuple| [tuple, source] }
172-
end
173-
end
174-
175166
def specs_type
176167
if options[:all] || options[:version].specific?
177168
if options[:prerelease]

lib/rubygems/source.rb

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,12 +240,12 @@ def decode_content_addressable_tuples(tuples, latest: false)
240240
end
241241

242242
##
243-
# The publish time of gem +name+ at +version+ for +platform+, when this
243+
# The publish time of gem +name+ at +version+ for +suffix+, when this
244244
# source provides it through the compact index created_at metadata.
245245
# Returns nil when the source, the gem or the version has no known
246246
# publish time.
247247

248-
def created_at(name, version, platform = Gem::Platform::RUBY)
248+
def created_at(name, version, suffix = Gem::Platform::RUBY)
249249
return unless %w[http https].include?(uri.scheme)
250250

251251
@created_at_info ||= {}
@@ -255,12 +255,12 @@ def created_at(name, version, platform = Gem::Platform::RUBY)
255255
[]
256256
end
257257

258-
platform = (platform || Gem::Platform::RUBY).to_s
258+
suffix = (suffix || Gem::Platform::RUBY).to_s
259259
version = version.to_s
260260

261261
row = info.find do |row_info|
262262
row_info[Gem::CompactIndexClient::INFO_VERSION] == version &&
263-
(row_info[Gem::CompactIndexClient::INFO_PLATFORM] || Gem::Platform::RUBY) == platform
263+
(row_info[Gem::CompactIndexClient::INFO_SUFFIX] || Gem::Platform::RUBY) == suffix
264264
end
265265
return unless row
266266

@@ -269,6 +269,14 @@ def created_at(name, version, platform = Gem::Platform::RUBY)
269269
Gem::Cooldown.parse_created_at(value)
270270
end
271271

272+
##
273+
# The publish time for +tuple+. Content-addressable tuples are looked up by
274+
# content address; all other tuples are looked up by platform.
275+
276+
def created_at_for_tuple(tuple)
277+
created_at(tuple.name, tuple.version, tuple.content_address || tuple.platform)
278+
end
279+
272280
##
273281
# Downloads +spec+ and writes it to +dir+. See also
274282
# Gem::RemoteFetcher#download.
@@ -403,7 +411,7 @@ def content_addressable_metadata(name, rows)
403411

404412
available_rows = compact_index_info_rows(name).filter_map do |info_row|
405413
version = info_row[Gem::CompactIndexClient::INFO_VERSION]
406-
suffix = info_row[Gem::CompactIndexClient::INFO_PLATFORM]
414+
suffix = info_row[Gem::CompactIndexClient::INFO_SUFFIX]
407415

408416
requirements = compact_index_requirements(info_row)
409417
platform = required_platform_from(requirements[:platform])

lib/rubygems/spec_fetcher.rb

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ def search_for_dependency(dependency, matching_platform = true, type: nil)
9191

9292
rejected_specs = {}
9393

94-
list, errors = available_specs(type || dependency.identity)
94+
specs_type = type || dependency.identity
95+
list, errors = available_specs(specs_type)
9596

9697
list.each do |source, specs|
9798
if dependency.name.is_a?(String) && specs.respond_to?(:bsearch)
@@ -100,17 +101,20 @@ def search_for_dependency(dependency, matching_platform = true, type: nil)
100101
specs = specs[start_index...end_index] if start_index && end_index
101102
end
102103

104+
specs = specs.select {|tup| dependency.match?(tup) }
105+
specs = decode_source_content_addressable_tuples(source, specs, latest: specs_type == :latest)
106+
103107
found[source] = specs.select do |tup|
104-
if dependency.match?(tup)
105-
if matching_platform && !Gem::Platform.match_gem?(tup.platform, tup.name)
106-
pm = (
107-
rejected_specs[dependency] ||= \
108-
Gem::PlatformMismatch.new(tup.name, tup.version))
109-
pm.add_platform tup.platform
110-
false
111-
else
112-
true
113-
end
108+
if matching_platform && !Gem::Platform.match_gem?(tup.platform, tup.name)
109+
pm = (
110+
rejected_specs[dependency] ||= \
111+
Gem::PlatformMismatch.new(tup.name, tup.version))
112+
pm.add_platform tup.platform
113+
false
114+
elsif matching_platform && !ruby_abi_match?(tup)
115+
false
116+
else
117+
true
114118
end
115119
end
116120
end
@@ -159,6 +163,7 @@ def spec_for_dependency(dependency, matching_platform = true)
159163
specs = []
160164
tuples.each do |tup, source|
161165
spec = source.fetch_spec(tup)
166+
spec.content_address = tup.content_address if tup.content_address
162167
rescue Gem::RemoteFetcher::FetchError => e
163168
errors << Gem::SourceFetchProblem.new(source, e)
164169
else
@@ -168,6 +173,17 @@ def spec_for_dependency(dependency, matching_platform = true)
168173
[specs, errors]
169174
end
170175

176+
##
177+
# Decodes the content-addressable tuples in +spec_tuples+ ([tuple, source]
178+
# pairs) to carry their real platform and Ruby ABI via each source.
179+
180+
def decode_content_addressable_tuples(spec_tuples, latest: false)
181+
spec_tuples.group_by {|_, source| source }.flat_map do |source, source_tuples|
182+
tuples = source_tuples.map(&:first)
183+
decode_source_content_addressable_tuples(source, tuples, latest: latest).map {|tuple| [tuple, source] }
184+
end
185+
end
186+
171187
##
172188
# Suggests gems based on the supplied +gem_name+. Returns an array of
173189
# alternative gem names.
@@ -290,4 +306,20 @@ def tuples_for(source, type, gracefully_ignore = false) # :nodoc:
290306
raise unless gracefully_ignore
291307
[]
292308
end
309+
310+
private
311+
312+
def decode_source_content_addressable_tuples(source, tuples, latest: false) # :nodoc:
313+
return tuples unless source.respond_to?(:decode_content_addressable_tuples)
314+
315+
source.decode_content_addressable_tuples(tuples, latest: latest)
316+
end
317+
318+
def ruby_abi_match?(tuple) # :nodoc:
319+
!tuple.ruby_abi || tuple.ruby_abi == current_ruby_abi
320+
end
321+
322+
def current_ruby_abi # :nodoc:
323+
@current_ruby_abi ||= Gem.ruby_version.segments.first(2).join(".")
324+
end
293325
end

test/rubygems/helper.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,31 @@ def util_gem(name, version, deps = nil, ruby_abi: nil, &block)
10371037
[spec, cache_file]
10381038
end
10391039

1040+
##
1041+
# Builds a platform gem and serves it through compact index as a
1042+
# content-addressable gem. Returns the specification, gem path, and content
1043+
# address.
1044+
1045+
def util_setup_content_addressable_compact_index_gem(name, version, platform: "x86_64-linux", required_ruby_version: ">= 3.0", &block)
1046+
spec, gem_path = util_gem(name, version) do |s|
1047+
s.platform = platform
1048+
s.required_ruby_version = required_ruby_version
1049+
yield(s) if block
1050+
end
1051+
1052+
content_address = Digest::SHA256.file(gem_path).hexdigest[0, 8]
1053+
ca_gem_path = File.join(File.dirname(gem_path), "#{spec.name}-#{spec.version}-#{content_address}.gem")
1054+
FileUtils.cp gem_path, ca_gem_path
1055+
spec.content_address = content_address
1056+
1057+
util_setup_compact_index spec
1058+
@fetcher.data["#{@gem_repo}quick/Marshal.#{Gem.marshal_version}/#{spec.full_name}.gemspec.rz"] = util_zip(Marshal.dump(spec))
1059+
add_to_fetcher spec, ca_gem_path
1060+
Gem::SpecFetcher.fetcher = nil
1061+
1062+
[spec, ca_gem_path, content_address]
1063+
end
1064+
10401065
##
10411066
# Gzips +data+.
10421067

test/rubygems/test_gem_commands_dependency_command.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,53 @@ def test_execute_remote
188188
assert_equal "", @stub_ui.error
189189
end
190190

191+
def test_execute_remote_content_addressable_compact_index_gem
192+
spec_fetcher {}
193+
util_set_arch "x86_64-linux"
194+
195+
_spec, _gem_path, content_address = util_setup_content_addressable_compact_index_gem(
196+
"ca_dependency",
197+
"1.0.0",
198+
platform: "x86_64-linux",
199+
required_ruby_version: "~> 3.3.0"
200+
) do |s|
201+
s.add_runtime_dependency "dep_tophat", "~> 1.0"
202+
end
203+
204+
@cmd.options[:args] = %w[ca_dependency]
205+
@cmd.options[:domain] = :remote
206+
207+
use_ui @stub_ui do
208+
@cmd.execute
209+
end
210+
211+
assert_equal "Gem ca_dependency-1.0.0-#{content_address} (Platform: x86_64-linux Ruby ABI: 3.3)\n dep_tophat (~> 1.0)\n\n", @stub_ui.output
212+
assert_equal "", @stub_ui.error
213+
end
214+
215+
def test_execute_remote_platform_compact_index_gem
216+
spec_fetcher {}
217+
util_set_arch "x86_64-linux"
218+
219+
spec = util_spec "platform_dependency", "1.0.0" do |s|
220+
s.platform = "x86_64-linux"
221+
s.add_runtime_dependency "dep_tophat", "~> 1.0"
222+
end
223+
util_setup_compact_index spec
224+
write_marshalled_gemspecs spec
225+
Gem::SpecFetcher.fetcher = nil
226+
227+
@cmd.options[:args] = %w[platform_dependency]
228+
@cmd.options[:domain] = :remote
229+
230+
use_ui @stub_ui do
231+
@cmd.execute
232+
end
233+
234+
assert_equal "Gem platform_dependency-1.0.0-x86_64-linux\n dep_tophat (~> 1.0)\n\n", @stub_ui.output
235+
assert_equal "", @stub_ui.error
236+
end
237+
191238
def test_execute_remote_version
192239
@fetcher = Gem::FakeFetcher.new
193240
Gem::RemoteFetcher.fetcher = @fetcher

test/rubygems/test_gem_commands_fetch_command.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,44 @@ def test_execute_prerelease
6868
"#{a2.full_name} not fetched")
6969
end
7070

71+
def test_execute_content_addressable_compact_index_gem
72+
spec_fetcher {}
73+
util_set_arch "x86_64-linux"
74+
75+
spec, _gem_path, content_address = util_setup_content_addressable_compact_index_gem(
76+
"ca_fetch",
77+
"1.0.0",
78+
platform: "x86_64-linux"
79+
)
80+
81+
@cmd.options[:args] = %w[ca_fetch]
82+
83+
execute_with_exit_code
84+
85+
assert_path_exist File.join(@tempdir, "ca_fetch-1.0.0-#{content_address}.gem")
86+
assert_path_not_exist File.join(@tempdir, "ca_fetch-1.0.0-x86_64-linux.gem")
87+
assert_equal "ca_fetch-1.0.0-#{content_address}", spec.full_name
88+
end
89+
90+
def test_execute_platform_compact_index_gem
91+
spec_fetcher {}
92+
util_set_arch "x86_64-linux"
93+
94+
spec, gem_path = util_gem "platform_fetch", "1.0.0" do |s|
95+
s.platform = "x86_64-linux"
96+
end
97+
util_setup_compact_index spec
98+
write_marshalled_gemspecs spec
99+
add_to_fetcher spec, gem_path
100+
Gem::SpecFetcher.fetcher = nil
101+
102+
@cmd.options[:args] = %w[platform_fetch]
103+
104+
execute_with_exit_code
105+
106+
assert_path_exist File.join(@tempdir, "platform_fetch-1.0.0-x86_64-linux.gem")
107+
end
108+
71109
def test_execute_platform
72110
a2_spec, a2 = util_gem("a", "2")
73111

0 commit comments

Comments
 (0)