Skip to content

Commit 7acf040

Browse files
hsbtclaude
andcommitted
Don't require source plugins to be installed to parse a lockfile
Third-party tools like bundler-audit read lockfiles through Bundler::LockfileParser without evaluating the Gemfile, and a PLUGIN SOURCE block whose plugin isn't installed locally aborted the whole parse with UnknownSourceError, taking even DEPENDENCIES down with it. Fall back to the inert UnloadedSource placeholder instead, which also lets bundle install converge away a plugin source that was removed from the Gemfile. #9614 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 11741d3 commit 7acf040

3 files changed

Lines changed: 34 additions & 1 deletion

File tree

bundler/lib/bundler/plugin.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,14 @@ def source(name)
200200
# @return [API::Source] the instance of the class that handles the source
201201
# type passed in locked_opts
202202
def from_lock(locked_opts)
203+
opts = locked_opts.merge("uri" => locked_opts["remote"])
204+
# use an inert placeholder when the plugin handling this source is not
205+
# installed, so that the lockfile can still be parsed
206+
return UnloadedSource.new(opts) unless source?(locked_opts["type"])
207+
203208
src = source(locked_opts["type"])
204209

205-
src.new(locked_opts.merge("uri" => locked_opts["remote"]))
210+
src.new(opts)
206211
end
207212

208213
# To be called via the API to register a hooks and corresponding block that

bundler/spec/bundler/lockfile_parser_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,25 @@
252252
end
253253
end
254254

255+
context "when a plugin source's plugin is not installed" do
256+
let(:lockfile_contents) { <<~L + super().sub("DEPENDENCIES\n", "DEPENDENCIES\n private_gem!\n") }
257+
PLUGIN SOURCE
258+
remote: https://example.com/private
259+
type: not_installed_plugin_type
260+
specs:
261+
private_gem (1.2.3)
262+
263+
L
264+
265+
it "parses dependencies and specs using a placeholder source" do
266+
expect(subject.valid?).to be(true)
267+
expect(subject.dependencies.keys).to include("private_gem", "peiji-san", "rake")
268+
private_spec = subject.specs.find {|s| s.name == "private_gem" }
269+
expect(private_spec.version).to eq(v("1.2.3"))
270+
expect(private_spec.source).to be_a(Bundler::Plugin::UnloadedSource)
271+
end
272+
end
273+
255274
context "when CHECKSUMS has duplicate checksums in the lockfile that don't match" do
256275
let(:bad_checksum) { "sha256=c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11" }
257276
let(:lockfile_contents) { super().split(/(?<=CHECKSUMS\n)/m).insert(1, " rake (10.3.2) #{bad_checksum}\n").join }

bundler/spec/bundler/plugin_spec.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,15 @@
238238
with(hash_including("type" => "l_source", "uri" => "xyz", "other" => "random")) { s_instance }
239239
expect(subject.from_lock(opts)).to be(s_instance)
240240
end
241+
242+
it "returns an UnloadedSource when the plugin handling the source is not installed" do
243+
opts = { "type" => "missing_source", "remote" => "https://example.com/private" }
244+
allow(index).to receive(:source_plugin).with("missing_source") { nil }
245+
246+
source = subject.from_lock(opts)
247+
expect(source).to be_a(Plugin::UnloadedSource)
248+
expect(source.uri).to eq("https://example.com/private")
249+
end
241250
end
242251

243252
describe "#root" do

0 commit comments

Comments
 (0)