diff --git a/lib/vagrant/bundler.rb b/lib/vagrant/bundler.rb index d5c4bec01..7abc01a97 100644 --- a/lib/vagrant/bundler.rb +++ b/lib/vagrant/bundler.rb @@ -23,13 +23,13 @@ module Vagrant attr_reader :plugin_file # @return [Pathname] path to solution file attr_reader :solution_file - # @return [Array] list of required dependencies + # @return [Array] list of required dependencies attr_reader :dependency_list # @param [Pathname] plugin_file Path to plugin file # @param [Pathname] solution_file Custom path to solution file def initialize(plugin_file:, solution_file: nil) - @logger = Log4r::Logger.new("vagrant::bundler::signature_file") + @logger = Log4r::Logger.new("vagrant::bundler::solution_file") @plugin_file = Pathname.new(plugin_file.to_s) if solution_file @solution_file = Pathname.new(solution_file.to_s) @@ -46,13 +46,16 @@ module Vagrant # Set the list of dependencies for this solution # # @param [Array] dependency_list List of dependencies for the solution + # @return [Array] def dependency_list=(dependency_list) Array(dependency_list).each do |d| if !d.is_a?(Gem::Dependency) raise TypeError, "Expected `Gem::Dependency` but received `#{d.class}`" end end - @dependency_list = dependency_list.map(&:freeze).freeze + @dependency_list = dependency_list.map do |d| + Gem::Resolver::DependencyRequest.new(d, nil).freeze + end.freeze end # @return [Boolean] contained solution is valid @@ -62,8 +65,9 @@ module Vagrant # @return [FalseClass] invalidate this solution file def invalidate! - @logger.debug("manually invalidating solution file") @valid = false + @logger.debug("manually invalidating solution file #{self}") + @valid end # Delete the solution file @@ -92,7 +96,7 @@ module Vagrant @logger.debug("writing solution file contents to disk") solution_file.write({ dependencies: dependency_list.map { |d| - [d.name, d.requirements_list] + [d.dependency.name, d.dependency.requirements_list] }, checksum: plugin_file_checksum, vagrant_version: Vagrant::VERSION @@ -121,9 +125,10 @@ module Vagrant version: solution[:vagrant_version] ) @logger.debug("loading solution dependency list") - @dependency_list = solution[:dependencies].map do |name, requirements| - Gem::Dependency.new(name, requirements) - end + @dependency_list = Array(solution[:dependencies]).map do |name, requirements| + gd = Gem::Dependency.new(name, requirements) + Gem::Resolver::DependencyRequest.new(gd, nil).freeze + end.freeze @logger.debug("solution dependency list: #{dependency_list}") @valid = true end @@ -158,6 +163,7 @@ module Vagrant Vagrant::Util::HashWithIndifferentAccess.new(hash) rescue => err @logger.warn("failed to load solution file, ignoring (error: #{err})") + nil end end end @@ -234,14 +240,18 @@ module Vagrant Gem::Specification.reset end - enable_prerelease!(specs: @initial_specifications) - solution_file = load_solution_file(opts) @logger.debug("solution file in use for init: #{solution_file}") solution = nil composed_set = generate_vagrant_set + # Force the composed set to allow prereleases + if Vagrant.allow_prerelease_dependencies? + @logger.debug("enabling prerelease dependency matching due to user request") + composed_set.prerelease = true + end + if solution_file&.valid? @logger.debug("loading cached solution set") solution = solution_file.dependency_list.map do |dep| @@ -466,30 +476,6 @@ module Vagrant protected - # This will enable prerelease if any of the root dependency constraints - # include prerelease versions - # - # @param [Array] spec_list List of specifications - # @param [Gem::RequestSet] rs Request set of dependencies - def enable_prerelease!(specs: nil, request_set: nil) - pre = specs.detect do |spec| - spec.version.prerelease? - end if specs - if pre - @logger.debug("Enabling prerelease plugin resolution due to dependency: #{pre.full_name}") - ENV["VAGRANT_ALLOW_PRERELEASE"] = "1" - return - end - dep = request_set.dependencies.detect do |d| - d.prerelease? - end if request_set - if dep - @logger.debug("Enabling prerelease plugin resolution due to dependency: #{dep}") - ENV["VAGRANT_ALLOW_PRERELEASE"] = "1" - return - end - end - def internal_install(plugins, update, **extra) update = {} if !update.is_a?(Hash) skips = [] @@ -576,15 +562,19 @@ module Vagrant # Create the request set for the new plugins request_set = Gem::RequestSet.new(*plugin_deps) - enable_prerelease!(request_set: request_set) - request_set.prerelease = Vagrant.prerelease? || - Vagrant.allow_prerelease_dependencies? installer_set = Gem::Resolver.compose_sets( installer_set, generate_builtin_set(system_plugins), generate_plugin_set(skips) ) + + if Vagrant.allow_prerelease_dependencies? + @logger.debug("enabling prerelease dependency matching based on user request") + request_set.prerelease = true + installer_set.prerelease = true + end + @logger.debug("Generating solution set for installation.") # Generate the required solution set for new plugins @@ -839,13 +829,22 @@ module Vagrant end def find_all(req) - @specs.select do |spec| - allow_prerelease = Vagrant.allow_prerelease_dependencies? || - (spec.name == "vagrant" && Vagrant.prerelease?) - req.match?(spec, allow_prerelease) + r = @specs.select do |spec| + # When matching requests against builtin specs, we _always_ enable + # prerelease matching since any prerelease that's found in this + # set has been added explicitly and should be available for all + # plugins to resolve against. This includes Vagrant itself since + # it is considered a prerelease when in development mode + req.match?(spec, true) end.map do |spec| Gem::Resolver::InstalledSpecification.new(self, spec) end + # If any of the results are a prerelease, we need to mark the request + # to allow prereleases so the solution can be properly fulfilled + if r.any? { |x| x.version.prerelease? } + req.dependency.prerelease = true + end + r end end @@ -879,7 +878,7 @@ module Vagrant # DependencyRequest +req+. def find_all(req) @specs.values.flatten.select do |spec| - req.match?(spec) + req.match?(spec, prerelease) end.map do |spec| source = Gem::Source::Vendor.new(@directories[spec]) Gem::Resolver::VendorSpecification.new(self, spec, source) diff --git a/test/unit/vagrant/bundler_test.rb b/test/unit/vagrant/bundler_test.rb index 8dcae5b14..00cedc021 100644 --- a/test/unit/vagrant/bundler_test.rb +++ b/test/unit/vagrant/bundler_test.rb @@ -67,13 +67,28 @@ describe Vagrant::Bundler::SolutionFile do describe "#dependency_list=" do it "should accept a list of Gem::Dependency instances" do list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } - expect(subject.dependency_list = list).to eq(list) + subject.dependency_list = list + expect(subject.dependency_list.map(&:dependency)).to eq(list) end it "should error if list includes instance not Gem::Dependency" do list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } << :invalid expect{ subject.dependency_list = list }.to raise_error(TypeError) end + + it "should convert list into resolver dependency request" do + list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } + subject.dependency_list = list + subject.dependency_list.each do |dep| + expect(dep).to be_a(Gem::Resolver::DependencyRequest) + end + end + + it "should freeze the new dependency list" do + list = ["dep1", "dep2"].map{ |x| Gem::Dependency.new(x) } + subject.dependency_list = list + expect(subject.dependency_list).to be_frozen + end end describe "#delete!" do @@ -119,7 +134,6 @@ describe Vagrant::Bundler::SolutionFile do end end - context "when plugin file does exist" do before { subject.plugin_file.write("x") } @@ -238,6 +252,191 @@ describe Vagrant::Bundler::SolutionFile do end end end + + describe "#load" do + let(:plugin_file_exists) { false } + let(:solution_file_exists) { false } + let(:plugin_file_path) { "PLUGIN_FILE_PATH" } + let(:solution_file_path) { "SOLUTION_FILE_PATH" } + let(:plugin_file) { double("plugin-file") } + let(:solution_file) { double("solution-file") } + + subject do + described_class.new(plugin_file: plugin_file_path, solution_file: solution_file_path) + end + + before do + allow(Pathname).to receive(:new).with(plugin_file_path).and_return(plugin_file) + allow(Pathname).to receive(:new).with(solution_file_path).and_return(solution_file) + allow(plugin_file).to receive(:exist?).and_return(plugin_file_exists) + allow(solution_file).to receive(:exist?).and_return(solution_file_exists) + end + + context "when plugin file and solution file do not exist" do + it "should not attempt to read the solution" do + expect_any_instance_of(described_class).not_to receive(:read_solution) + subject + end + end + + context "when plugin file exists and solution file does not" do + let(:plugin_file_exists) { true } + + it "should not attempt to read the solution" do + expect_any_instance_of(described_class).not_to receive(:read_solution) + subject + end + end + + context "when solution file exists and plugin file does not" do + let(:solution_file_exists) { true } + + it "should not attempt to read the solution" do + expect_any_instance_of(described_class).not_to receive(:read_solution) + subject + end + end + + context "when solution file and plugin file exist" do + let(:plugin_file_exists) { true } + let(:solution_file_exists) { true } + + let(:solution_file_contents) { "" } + + before do + allow(solution_file).to receive(:read).and_return(solution_file_contents) + allow_any_instance_of(described_class).to receive(:plugin_file_checksum).and_return("VALID") + end + + context "when solution file is empty" do + it "should return false" do + expect(subject.send(:load)).to be_falsey + end + end + + context "when solution file contains invalid checksum" do + let(:solution_file_contents) { {checksum: "INVALID", vagrant_version: Vagrant::VERSION}.to_json } + + it "should return false" do + expect(subject.send(:load)).to be_falsey + end + end + + context "when solution file contains different Vagrant version" do + let(:solution_file_contents) { {checksum: "VALID", vagrant_version: "0.1"}.to_json } + + it "should return false" do + expect(subject.send(:load)).to be_falsey + end + end + + context "when solution file contains valid Vagrant version and valid checksum" do + let(:solution_file_contents) { + {checksum: "VALID", vagrant_version: Vagrant::VERSION, dependencies: file_dependencies}.to_json + } + let(:file_dependencies) { dependency_list.map{|d| [d.name, d.requirements_list]} } + let(:dependency_list) { [] } + + it "should return true" do + expect(subject.send(:load)).to be_truthy + end + + it "should be valid" do + expect(subject).to be_valid + end + + context "when solution file contains dependency list" do + let(:dependency_list) { [ + Gem::Dependency.new("dep1", "> 0"), + Gem::Dependency.new("dep2", "< 3") + ] } + + it "should be valid" do + expect(subject).to be_valid + end + + it "should convert list into dependency requests" do + subject.dependency_list.each do |d| + expect(d).to be_a(Gem::Resolver::DependencyRequest) + end + end + + it "should include defined dependencies" do + expect(subject.dependency_list.first).to eq(dependency_list.first) + expect(subject.dependency_list.last).to eq(dependency_list.last) + end + + it "should freeze the dependency list" do + expect(subject.dependency_list).to be_frozen + end + end + end + end + end + + describe "#read_solution" do + let(:solution_file_contents) { "" } + let(:plugin_file_path) { "PLUGIN_FILE_PATH" } + let(:solution_file_path) { "SOLUTION_FILE_PATH" } + let(:plugin_file) { double("plugin-file") } + let(:solution_file) { double("solution-file") } + + subject do + described_class.new(plugin_file: plugin_file_path, solution_file: solution_file_path) + end + + before do + allow(Pathname).to receive(:new).with(plugin_file_path).and_return(plugin_file) + allow(Pathname).to receive(:new).with(solution_file_path).and_return(solution_file) + allow(plugin_file).to receive(:exist?).and_return(false) + allow(solution_file).to receive(:exist?).and_return(false) + allow(solution_file).to receive(:read).and_return(solution_file_contents) + end + + it "should return nil when file contents are empty" do + expect(subject.send(:read_solution)).to be_nil + end + + context "when file contents are hash" do + let(:solution_file_contents) { {checksum: "VALID"}.to_json } + + it "should return a hash" do + expect(subject.send(:read_solution)).to be_a(Hash) + end + + it "should return a hash with indifferent access" do + expect(subject.send(:read_solution)).to be_a(Vagrant::Util::HashWithIndifferentAccess) + end + end + + context "when file contents are array" do + let(:solution_file_contents) { ["test"].to_json } + + it "should return a hash" do + expect(subject.send(:read_solution)).to be_a(Hash) + end + + it "should return a hash with indifferent access" do + expect(subject.send(:read_solution)).to be_a(Vagrant::Util::HashWithIndifferentAccess) + end + end + + context "when file contents are null" do + let(:solution_file_contents) { "null" } + + it "should return nil" do + expect(subject.send(:read_solution)).to be_nil + end + end + + context "when file contents are invalid" do + let(:solution_file_contents) { "{2dfwef" } + + it "should return nil" do + expect(subject.send(:read_solution)).to be_nil + end + end + end end describe Vagrant::Bundler do @@ -618,117 +817,6 @@ describe Vagrant::Bundler do end end end - - describe "#enable_prerelease!" do - before do - @_ev = ENV.delete("VAGRANT_ALLOW_PRERELEASE") - end - - after do - ENV["VAGRANT_ALLOW_PRERELEASE"] = @_ev - end - - context "with specification list" do - let(:specifications) { [] } - - it "should not modify prerelease by default" do - subject.send(:enable_prerelease!, specs: specifications) - expect(ENV["VAGRANT_ALLOW_PRERELEASE"]).to be_falsey - end - - it "should not have enabled allow prerelease dependencies" do - subject.send(:enable_prerelease!, specs: specifications) - expect(Vagrant.allow_prerelease_dependencies?).to be_falsey - end - - context "when specifications do not contain prerelease versions" do - let(:specifications) { [ - double("spec1", full_name: "spec1", version: double("version1", prerelease?: false)), - double("spec2", full_name: "spec2", version: double("version2", prerelease?: false)), - double("spec3", full_name: "spec3", version: double("version3", prerelease?: false)) - ] } - - it "should not modify prerelease" do - subject.send(:enable_prerelease!, specs: specifications) - expect(ENV["VAGRANT_ALLOW_PRERELEASE"]).to be_falsey - end - - it "should not have enabled allow prerelease dependencies" do - subject.send(:enable_prerelease!, specs: specifications) - expect(Vagrant.allow_prerelease_dependencies?).to be_falsey - end - end - - context "when specifications contain prerelease versions" do - let(:specifications) { [ - double("spec1", full_name: "spec1", version: double("version1", prerelease?: false)), - double("spec2", full_name: "spec2", version: double("version2", prerelease?: true)), - double("spec3", full_name: "spec3", version: double("version3", prerelease?: false)) - ] } - - it "should enable prerelease" do - subject.send(:enable_prerelease!, specs: specifications) - expect(ENV["VAGRANT_ALLOW_PRERELEASE"]).to be_truthy - end - - it "should have enabled allow prerelease dependencies" do - subject.send(:enable_prerelease!, specs: specifications) - expect(Vagrant.allow_prerelease_dependencies?).to be_truthy - end - end - end - - context "with request set" do - let(:request_set) { double("request_set", dependencies: dependencies) } - let(:dependencies) { [] } - - it "should not modify prerelease by default" do - subject.send(:enable_prerelease!, request_set: request_set) - expect(ENV["VAGRANT_ALLOW_PRERELEASE"]).to be_falsey - end - - it "should not have enabled allow prerelease dependencies" do - subject.send(:enable_prerelease!, request_set: request_set) - expect(Vagrant.allow_prerelease_dependencies?).to be_falsey - end - - context "when specifications do not contain prerelease versions" do - let(:dependencies) { [ - double("dep1", prerelease?: false, to_s: nil), - double("dep2", prerelease?: false, to_s: nil), - double("dep3", prerelease?: false, to_s: nil) - ] } - - it "should not modify prerelease" do - subject.send(:enable_prerelease!, request_set: request_set) - expect(ENV["VAGRANT_ALLOW_PRERELEASE"]).to be_falsey - end - - it "should not have enabled allow prerelease dependencies" do - subject.send(:enable_prerelease!, request_set: request_set) - expect(Vagrant.allow_prerelease_dependencies?).to be_falsey - end - end - - context "when specifications contain prerelease versions" do - let(:dependencies) { [ - double("dep1", prerelease?: false, to_s: nil), - double("dep2", prerelease?: true, to_s: nil), - double("dep3", prerelease?: false, to_s: nil) - ] } - - it "should enable prerelease" do - subject.send(:enable_prerelease!, request_set: request_set) - expect(ENV["VAGRANT_ALLOW_PRERELEASE"]).to be_truthy - end - - it "should have enabled allow prerelease dependencies" do - subject.send(:enable_prerelease!, request_set: request_set) - expect(Vagrant.allow_prerelease_dependencies?).to be_truthy - end - end - end - end end describe Vagrant::Bundler::PluginSet do