Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Manifest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ bundler/lib/bundler/plugin/installer/git.rb
bundler/lib/bundler/plugin/installer/path.rb
bundler/lib/bundler/plugin/installer/rubygems.rb
bundler/lib/bundler/plugin/source_list.rb
bundler/lib/bundler/plugin/unloaded_source.rb
bundler/lib/bundler/process_lock.rb
bundler/lib/bundler/remote_specification.rb
bundler/lib/bundler/resolver.rb
Expand Down
18 changes: 12 additions & 6 deletions bundler/lib/bundler/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

module Bundler
module Plugin
autoload :DSL, File.expand_path("plugin/dsl", __dir__)
autoload :Events, File.expand_path("plugin/events", __dir__)
autoload :Index, File.expand_path("plugin/index", __dir__)
autoload :Installer, File.expand_path("plugin/installer", __dir__)
autoload :SourceList, File.expand_path("plugin/source_list", __dir__)
autoload :DSL, File.expand_path("plugin/dsl", __dir__)
autoload :Events, File.expand_path("plugin/events", __dir__)
autoload :Index, File.expand_path("plugin/index", __dir__)
autoload :Installer, File.expand_path("plugin/installer", __dir__)
autoload :SourceList, File.expand_path("plugin/source_list", __dir__)
autoload :UnloadedSource, File.expand_path("plugin/unloaded_source", __dir__)

class MalformattedPlugin < PluginError; end
class UndefinedCommandError < PluginError; end
Expand Down Expand Up @@ -199,9 +200,14 @@ def source(name)
# @return [API::Source] the instance of the class that handles the source
# type passed in locked_opts
def from_lock(locked_opts)
opts = locked_opts.merge("uri" => locked_opts["remote"])
# use an inert placeholder when the plugin handling this source is not
# installed, so that the lockfile can still be parsed
return UnloadedSource.new(opts) unless source?(locked_opts["type"])

src = source(locked_opts["type"])

src.new(locked_opts.merge("uri" => locked_opts["remote"]))
src.new(opts)
end

# To be called via the API to register a hooks and corresponding block that
Expand Down
25 changes: 25 additions & 0 deletions bundler/lib/bundler/plugin/unloaded_source.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Bundler
module Plugin
# Stands in for a source handled by a plugin that is not loaded yet, so
# that the lockfile can still be parsed during the plugin install pass,
# and by external tools reading a lockfile without the plugin installed.
class UnloadedSource
include API::Source

# Unlike real plugin sources, where the handling class encodes the
# source type, all unloaded sources share this class, so the type must
# be compared explicitly.
def ==(other)
super && options["type"] == other.options["type"]
end

alias_method :eql?, :==

def hash
[super, options["type"]].hash
end
end
end
end
19 changes: 19 additions & 0 deletions bundler/spec/bundler/lockfile_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,25 @@
end
end

context "when a plugin source's plugin is not installed" do
let(:lockfile_contents) { <<~L + super().sub("DEPENDENCIES\n", "DEPENDENCIES\n private_gem!\n") }
PLUGIN SOURCE
remote: https://example.com/private
type: not_installed_plugin_type
specs:
private_gem (1.2.3)

L

it "parses dependencies and specs using a placeholder source" do
expect(subject.valid?).to be(true)
expect(subject.dependencies.keys).to include("private_gem", "peiji-san", "rake")
private_spec = subject.specs.find {|s| s.name == "private_gem" }
expect(private_spec.version).to eq(v("1.2.3"))
expect(private_spec.source).to be_a(Bundler::Plugin::UnloadedSource)
end
end

context "when CHECKSUMS has duplicate checksums in the lockfile that don't match" do
let(:bad_checksum) { "sha256=c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11" }
let(:lockfile_contents) { super().split(/(?<=CHECKSUMS\n)/m).insert(1, " rake (10.3.2) #{bad_checksum}\n").join }
Expand Down
36 changes: 36 additions & 0 deletions bundler/spec/bundler/plugin/unloaded_source_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

RSpec.describe Bundler::Plugin::UnloadedSource do
let(:uri) { "uri://to/test" }

def source(type)
described_class.new("uri" => uri, "type" => type)
end

describe "equality" do
it "treats sources with the same uri and type as equal" do
a = source("type_a")
b = source("type_a")

expect(a).to eq(b)
expect(a).to eql(b)
expect(a.hash).to eq(b.hash)
end

it "treats sources with the same uri but different types as not equal" do
a = source("type_a")
b = source("type_b")

expect(a).not_to eq(b)
expect(a).not_to eql(b)
expect(a.hash).not_to eq(b.hash)
end

it "is not equal to a real plugin source with the same uri and type" do
klass = Class.new
klass.send :include, Bundler::Plugin::API::Source

expect(source("type_a")).not_to eq(klass.new("uri" => uri, "type" => "type_a"))
end
end
end
9 changes: 9 additions & 0 deletions bundler/spec/bundler/plugin_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,15 @@
with(hash_including("type" => "l_source", "uri" => "xyz", "other" => "random")) { s_instance }
expect(subject.from_lock(opts)).to be(s_instance)
end

it "returns an UnloadedSource when the plugin handling the source is not installed" do
opts = { "type" => "missing_source", "remote" => "https://example.com/private" }
allow(index).to receive(:source_plugin).with("missing_source") { nil }

source = subject.from_lock(opts)
expect(source).to be_a(Plugin::UnloadedSource)
expect(source.uri).to eq("https://example.com/private")
end
end

describe "#root" do
Expand Down
Loading