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:
Chris Roberts 2020-11-17 09:18:20 -08:00
parent d850c88840
commit c4eda3f08f
2 changed files with 242 additions and 155 deletions

View File

@ -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)

View File

@ -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