diff --git a/Manifest.txt b/Manifest.txt index ddf4f6044b47..e0b66eb9619e 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -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 diff --git a/bundler/lib/bundler/plugin.rb b/bundler/lib/bundler/plugin.rb index faca6bea5306..637b61251756 100644 --- a/bundler/lib/bundler/plugin.rb +++ b/bundler/lib/bundler/plugin.rb @@ -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 @@ -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 diff --git a/bundler/lib/bundler/plugin/unloaded_source.rb b/bundler/lib/bundler/plugin/unloaded_source.rb new file mode 100644 index 000000000000..948b6c7e1e13 --- /dev/null +++ b/bundler/lib/bundler/plugin/unloaded_source.rb @@ -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 diff --git a/bundler/spec/bundler/lockfile_parser_spec.rb b/bundler/spec/bundler/lockfile_parser_spec.rb index 4b493a37576b..7bf13ee64da3 100644 --- a/bundler/spec/bundler/lockfile_parser_spec.rb +++ b/bundler/spec/bundler/lockfile_parser_spec.rb @@ -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 } diff --git a/bundler/spec/bundler/plugin/unloaded_source_spec.rb b/bundler/spec/bundler/plugin/unloaded_source_spec.rb new file mode 100644 index 000000000000..abc048866f5c --- /dev/null +++ b/bundler/spec/bundler/plugin/unloaded_source_spec.rb @@ -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 diff --git a/bundler/spec/bundler/plugin_spec.rb b/bundler/spec/bundler/plugin_spec.rb index b379594c6f9a..27f93f8fe298 100644 --- a/bundler/spec/bundler/plugin_spec.rb +++ b/bundler/spec/bundler/plugin_spec.rb @@ -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