Update solution file to use DependencyRequests and allow prerelease
Maintain the solution file persisting dependency information on
disk but update the runtime representation to
Gem::Resolver::DependencyRequest instances which are expected
by the sets when locating matches.
Properly abide by prerelease setting in customized sets and
force prerelease matching when in the builtin set. If a request
is matched on a prerelease, and the request itself is not set
to allow prereleases, update it to ensure successful resolution.
This commit is contained in:
parent
d850c88840
commit
c4eda3f08f
@ -23,13 +23,13 @@ module Vagrant
|
||||
attr_reader :plugin_file
|
||||
# @return [Pathname] path to solution file
|
||||
attr_reader :solution_file
|
||||
# @return [Array<Gem::Dependency>] list of required dependencies
|
||||
# @return [Array<Gem::Resolver::DependencyRequest>] 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<Gem::Dependency>] dependency_list List of dependencies for the solution
|
||||
# @return [Array<Gem::Resolver::DependencyRequest>]
|
||||
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<Gem::Specification>] 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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user