Skip to content

Commit 8d050c3

Browse files
committed
Support content addressable gems in lockfile and local cache
Append platform to content-addressable gems entries in lockfile Support content addressable gems in lockfile and local cache
1 parent f0f4b7d commit 8d050c3

10 files changed

Lines changed: 153 additions & 13 deletions

lib/bundler/lazy_specification.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def source_changed?
6666
end
6767

6868
def full_name
69-
@full_name ||= if @content_address
69+
@full_name ||= if Gem::ContentAddress.match?(@content_address) && platform != Gem::Platform::RUBY
7070
"#{@name}-#{@version}-#{@content_address}"
7171
elsif platform == Gem::Platform::RUBY
7272
"#{@name}-#{@version}"
@@ -80,7 +80,7 @@ def lock_name
8080
end
8181

8282
def name_tuple
83-
Gem::NameTuple.new(@name, @version, @platform)
83+
Gem::NameTuple.new(@name, @version, @platform, content_address: @content_address)
8484
end
8585

8686
def ==(other)
@@ -117,7 +117,11 @@ def satisfies?(dependency)
117117

118118
def to_lock
119119
out = String.new
120-
out << " #{lock_name}\n"
120+
out << " #{lock_name}"
121+
# Append the platform additionally for content-addressable gems that contain a SHA
122+
# where the platform would otherwise be
123+
out << " #{platform}" if Gem::ContentAddress.match?(content_address) && platform != Gem::Platform::RUBY
124+
out << "\n"
121125

122126
dependencies.sort_by(&:to_s).uniq.each do |dep|
123127
next if dep.type == :development

lib/bundler/lockfile_parser.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,13 @@ def parse_checksum(line)
264264
checksums = $6
265265
name = $2
266266
version = $3
267-
platform = $4
267+
content_address = $4 if Gem::ContentAddress.match?($4)
268+
platform = $4 unless content_address
268269

269270
version = Gem::Version.new(version)
270271
platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
271-
full_name = Gem::NameTuple.new(name, version, platform).full_name
272+
name_tuple = Gem::NameTuple.new(name, version, platform, content_address: content_address)
273+
full_name = name_tuple.full_name
272274
spec = @specs[full_name]
273275

274276
if name == "bundler"
@@ -295,11 +297,17 @@ def parse_spec(line)
295297

296298
if spaces.size == 4
297299
# only load platform for non-dependency (spec) line
298-
platform = $4
300+
if Gem::ContentAddress.match?($4) && $6 && $6 != Gem::Platform::RUBY.to_s
301+
content_address = $4
302+
platform = $6
303+
else
304+
platform = $4
305+
content_address = $6 if Gem::ContentAddress.match?($6)
306+
end
299307

300308
version = Gem::Version.new(version)
301309
platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
302-
@current_spec = LazySpecification.new(name, version, platform, @current_source, strict: @strict)
310+
@current_spec = LazySpecification.new(name, version, platform, @current_source, content_address: content_address, strict: @strict)
303311
@current_source.add_dependency_names(name)
304312

305313
@specs[@current_spec.full_name] = @current_spec

lib/bundler/remote_specification.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@ class RemoteSpecification
1010
include MatchPlatform
1111
include Comparable
1212

13-
attr_reader :name, :version, :platform
13+
attr_reader :name, :version, :platform, :content_address
1414
attr_writer :dependencies
1515
attr_accessor :source, :remote, :locked_platform, :created_at
1616

17-
def initialize(name, version, platform, spec_fetcher)
17+
def initialize(name, version, platform, spec_fetcher, content_address: nil)
1818
@name = name
1919
@version = Gem::Version.create version
2020
@original_platform = platform || Gem::Platform::RUBY
2121
@platform = Gem::Platform.new(platform)
2222
@spec_fetcher = spec_fetcher
2323
@dependencies = nil
2424
@locked_platform = nil
25+
@content_address = content_address
2526
end
2627

2728
def insecurely_materialized?
@@ -35,7 +36,9 @@ def fetch_platform
3536
end
3637

3738
def full_name
38-
@full_name ||= if @platform == Gem::Platform::RUBY
39+
@full_name ||= if Gem::ContentAddress.match?(@content_address) && @platform != Gem::Platform::RUBY
40+
"#{@name}-#{@version}-#{@content_address}"
41+
elsif @platform == Gem::Platform::RUBY
3942
"#{@name}-#{@version}"
4043
else
4144
"#{@name}-#{@version}-#{@platform}"

lib/bundler/rubygems_ext.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
Gem::NameTuple.attr_reader :content_address
2424
end
2525

26+
unless Gem::Installer.private_method_defined?(:assign_content_address)
27+
Gem::Installer.send(:define_method, :assign_content_address) {}
28+
end
29+
2630
module Gem
2731
# Can be removed once RubyGems 4.0.0 support is dropped
2832
unless defined?(Gem::ContentAddress)
@@ -454,6 +458,15 @@ def initialize(name, version, platform = Gem::Platform::RUBY, content_address =
454458
end
455459
end
456460

461+
unless instance_method(:initialize).parameters.any? {|kind, name| kind == :key && name == :content_address }
462+
alias_method :initialize_without_content_address, :initialize
463+
464+
def initialize(name, version, platform = Gem::Platform::RUBY, content_address: nil)
465+
initialize_without_content_address(name, version, platform)
466+
@content_address = content_address
467+
end
468+
end
469+
457470
def lock_name
458471
return "#{name} (#{version}-#{content_address})" if Gem::ContentAddress.match?(content_address)
459472

lib/bundler/rubygems_gem_installer.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ def check_executable_overwrite(filename)
1414
end
1515

1616
def install
17+
assign_content_address
18+
1719
pre_install_checks
1820

1921
run_pre_install_hooks

lib/bundler/rubygems_integration.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,12 @@ def ext_lock
144144

145145
def spec_from_gem(path)
146146
require "rubygems/package"
147-
Gem::Package.new(path).spec
147+
package = Gem::Package.new(path)
148+
spec = package.spec
149+
if package.respond_to?(:content_address)
150+
spec.content_address = package.content_address
151+
end
152+
spec
148153
end
149154

150155
def build_gem(gem_dir, spec)

lib/bundler/stub_specification.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ module Bundler
44
class StubSpecification < RemoteSpecification
55
def self.from_stub(stub)
66
return stub if stub.is_a?(Bundler::StubSpecification)
7-
spec = new(stub.name, stub.version, stub.platform, nil)
7+
content_address = stub.content_address
8+
spec = new(stub.name, stub.version, stub.platform, nil, content_address: content_address)
89
spec.stub = stub
910
spec
1011
end

spec/bundler/lockfile_parser_spec.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,65 @@
145145

146146
include_examples "parsing"
147147

148+
context "when a spec has a content address" do
149+
let(:lockfile_contents) do
150+
<<~L
151+
GEM
152+
remote: https://rubygems.org/
153+
specs:
154+
mygem (1.0-abcdef1234) x86_64-linux
155+
156+
PLATFORMS
157+
x86_64-linux
158+
159+
DEPENDENCIES
160+
mygem
161+
162+
CHECKSUMS
163+
mygem (1.0-abcdef1234) sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8
164+
165+
BUNDLED WITH
166+
1.12.0.rc.2
167+
L
168+
end
169+
170+
it "parses the platform and content address" do
171+
spec = subject.specs.find {|s| s.name == "mygem" }
172+
173+
expect(spec.platform).to eq(Gem::Platform.new("x86_64-linux"))
174+
expect(spec.content_address).to eq("abcdef1234")
175+
176+
checksums = subject.sources.first.checksum_store.to_lock(spec)
177+
expect(checksums).to eq("#{spec.lock_name} sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8")
178+
end
179+
end
180+
181+
context "when a Ruby-platform suffix resembles a content address but no platform is present" do
182+
let(:lockfile_contents) do
183+
<<~L
184+
GEM
185+
remote: https://rubygems.org/
186+
specs:
187+
mygem (1.0-abcdef1234)
188+
189+
PLATFORMS
190+
ruby
191+
192+
DEPENDENCIES
193+
mygem
194+
195+
BUNDLED WITH
196+
1.12.0.rc.2
197+
L
198+
end
199+
200+
it "does not parse the suffix as a content address" do
201+
spec = subject.specs.find {|s| s.name == "mygem" }
202+
203+
expect(spec.content_address).to be_nil
204+
end
205+
end
206+
148207
context "when an extra section is at the end" do
149208
let(:lockfile_contents) { super() + "\n\nFOO BAR\n baz\n baa\n qux\n" }
150209
include_examples "parsing"

spec/install/gemfile/content_addressable_spec.rb

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,43 @@
3333

3434
cached_files = Dir.glob(default_bundle_path("cache", "mygem-1.0-*.gem").to_s)
3535
expect(cached_files.size).to eq(1), "expected exactly one cached gem file, found: #{cached_files}"
36-
expect(cached_files.first).to match(/mygem-1\.0-[0-9a-f]{8}\.gem$/)
36+
expect(cached_files.first).to match(/mygem-1\.0-[0-9a-f]{8,64}\.gem$/)
3737
expect(default_bundle_path("cache", "mygem-1.0-x86_64-linux.gem")).not_to exist
38+
expect(lockfile).to match(/^ mygem \(1\.0-[0-9a-f]{8,64}\) x86_64-linux$/)
39+
end
40+
end
41+
42+
it "resolves a content-addressed binary from the local cache after a lockfile round-trip" do
43+
simulate_platform "x86_64-linux" do
44+
build_repo2 do
45+
build_gem "mygem", "1.0" do |s|
46+
s.platform = Gem::Platform.new("x86_64-linux")
47+
s.write "lib/mygem.rb", "MYGEM = '1.0 not_content_addressed'"
48+
end
49+
end
50+
51+
build_gem "mygem", "1.0", ruby_abi: current_abi, path: gem_repo2("gems") do |s|
52+
s.platform = Gem::Platform.new("x86_64-linux")
53+
s.required_ruby_version = "~> #{current_abi}.0"
54+
s.write "lib/mygem.rb", "MYGEM = '1.0 content_addressed'"
55+
end
56+
57+
install_gemfile <<~G, artifice: "compact_index_v2", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
58+
source "https://gem.repo2"
59+
60+
gem "mygem"
61+
G
62+
63+
cached_file = Dir[default_bundle_path("cache", "mygem-1.0-*.gem").to_s].first
64+
FileUtils.mkdir_p(bundled_app("vendor/cache"))
65+
FileUtils.cp(cached_file, bundled_app("vendor/cache"))
66+
67+
gem_dir = Dir[default_bundle_path("gems", "mygem-1.0-*").to_s].first
68+
pristine_system_gems
69+
bundle "install --local"
70+
71+
expect(the_bundle).to include_gems "mygem 1.0 content_addressed"
72+
expect(Dir[default_bundle_path("gems", "mygem-1.0-*").to_s].first).to eq(gem_dir)
3873
end
3974
end
4075

spec/other/ext_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,13 @@
5353
end
5454
end
5555
end
56+
57+
RSpec.describe Bundler::LazySpecification do
58+
describe "#to_lock" do
59+
it "appends the content address after the platform lock name when set" do
60+
spec = Bundler::LazySpecification.new("mygem", v("1.0"), "x86_64-linux", nil, content_address: "abcdef1234")
61+
62+
expect(spec.to_lock).to eq(" mygem (1.0-abcdef1234) x86_64-linux\n")
63+
end
64+
end
65+
end

0 commit comments

Comments
 (0)