Migrate our resolver engine to PubGrub
https://github.com/rubygems/rubygems/pull/5960 Co-authored-by: David Rodríguez <deivid.rodriguez@riseup.net>
This commit is contained in:
parent
14a1394bcd
commit
0a9d51ee9d
@ -75,7 +75,6 @@ module Bundler
|
||||
autoload :StubSpecification, File.expand_path("bundler/stub_specification", __dir__)
|
||||
autoload :UI, File.expand_path("bundler/ui", __dir__)
|
||||
autoload :URICredentialsFilter, File.expand_path("bundler/uri_credentials_filter", __dir__)
|
||||
autoload :VersionRanges, File.expand_path("bundler/version_ranges", __dir__)
|
||||
|
||||
class << self
|
||||
def configure
|
||||
|
@ -17,7 +17,7 @@ module Bundler
|
||||
begin
|
||||
definition.resolve_only_locally!
|
||||
not_installed = definition.missing_specs
|
||||
rescue GemNotFound, VersionConflict
|
||||
rescue GemNotFound, SolveFailure
|
||||
Bundler.ui.error "Bundler can't satisfy your Gemfile's dependencies."
|
||||
Bundler.ui.warn "Install missing gems with `bundle install`."
|
||||
exit 1
|
||||
|
@ -15,8 +15,8 @@ module Bundler
|
||||
end
|
||||
|
||||
print = options[:print]
|
||||
ui = Bundler.ui
|
||||
Bundler.ui = UI::Silent.new if print
|
||||
previous_ui_level = Bundler.ui.level
|
||||
Bundler.ui.level = "silent" if print
|
||||
|
||||
Bundler::Fetcher.disable_endpoint = options["full-index"]
|
||||
|
||||
@ -61,7 +61,7 @@ module Bundler
|
||||
definition.lock(file)
|
||||
end
|
||||
|
||||
Bundler.ui = ui
|
||||
Bundler.ui.level = previous_ui_level
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -150,7 +150,7 @@ module Bundler
|
||||
end
|
||||
|
||||
def gem_version_promoter
|
||||
@gem_version_promoter ||= GemVersionPromoter.new(@originally_locked_specs, @unlock[:gems])
|
||||
@gem_version_promoter ||= GemVersionPromoter.new
|
||||
end
|
||||
|
||||
def resolve_only_locally!
|
||||
@ -276,7 +276,7 @@ module Bundler
|
||||
end
|
||||
else
|
||||
Bundler.ui.debug("Found changes from the lockfile, re-resolving dependencies because #{change_reason}")
|
||||
resolver.start(expanded_dependencies)
|
||||
start_resolution
|
||||
end
|
||||
end
|
||||
|
||||
@ -299,7 +299,7 @@ module Bundler
|
||||
|
||||
if @locked_bundler_version
|
||||
locked_major = @locked_bundler_version.segments.first
|
||||
current_major = Gem::Version.create(Bundler::VERSION).segments.first
|
||||
current_major = Bundler.gem_version.segments.first
|
||||
|
||||
updating_major = locked_major < current_major
|
||||
end
|
||||
@ -474,7 +474,7 @@ module Bundler
|
||||
@resolver ||= begin
|
||||
last_resolve = converge_locked_specs
|
||||
remove_ruby_from_platforms_if_necessary!(current_dependencies)
|
||||
Resolver.new(source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve(last_resolve), platforms)
|
||||
Resolver.new(source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve(last_resolve))
|
||||
end
|
||||
end
|
||||
|
||||
@ -482,6 +482,23 @@ module Bundler
|
||||
@expanded_dependencies ||= dependencies + metadata_dependencies
|
||||
end
|
||||
|
||||
def resolution_packages
|
||||
@resolution_packages ||= begin
|
||||
packages = Hash.new do |h, k|
|
||||
h[k] = Resolver::Package.new(k, @platforms, @originally_locked_specs, @unlock[:gems])
|
||||
end
|
||||
|
||||
expanded_dependencies.each do |dep|
|
||||
name = dep.name
|
||||
platforms = dep.gem_platforms(@platforms)
|
||||
|
||||
packages[name] = Resolver::Package.new(name, platforms, @originally_locked_specs, @unlock[:gems], :dependency => dep)
|
||||
end
|
||||
|
||||
packages
|
||||
end
|
||||
end
|
||||
|
||||
def filter_specs(specs, deps)
|
||||
SpecSet.new(specs).for(deps, false, platforms)
|
||||
end
|
||||
@ -512,16 +529,22 @@ module Bundler
|
||||
break if incomplete_specs.empty?
|
||||
|
||||
Bundler.ui.debug("The lockfile does not have all gems needed for the current platform though, Bundler will still re-resolve dependencies")
|
||||
@resolve = resolver.start(expanded_dependencies, :exclude_specs => incomplete_specs)
|
||||
@resolve = start_resolution(:exclude_specs => incomplete_specs)
|
||||
specs = resolve.materialize(dependencies)
|
||||
end
|
||||
|
||||
bundler = sources.metadata_source.specs.search(Gem::Dependency.new("bundler", VERSION)).last
|
||||
bundler = sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last
|
||||
specs["bundler"] = bundler
|
||||
|
||||
specs
|
||||
end
|
||||
|
||||
def start_resolution(exclude_specs: [])
|
||||
result = resolver.start(expanded_dependencies, resolution_packages, :exclude_specs => exclude_specs)
|
||||
|
||||
SpecSet.new(SpecSet.new(result).for(dependencies, false, @platforms))
|
||||
end
|
||||
|
||||
def precompute_source_requirements_for_indirect_dependencies?
|
||||
@remote && sources.non_global_rubygems_sources.all?(&:dependency_api_available?) && !sources.aggregate_global_source?
|
||||
end
|
||||
|
@ -50,6 +50,7 @@ module Bundler
|
||||
# Returns the platforms this dependency is valid for, in the same order as
|
||||
# passed in the `valid_platforms` parameter
|
||||
def gem_platforms(valid_platforms)
|
||||
return [Gem::Platform::RUBY] if @force_ruby_platform
|
||||
return valid_platforms if @platforms.empty?
|
||||
|
||||
valid_platforms.select {|p| expanded_platforms.include?(GemHelpers.generic(p)) }
|
||||
|
@ -21,16 +21,7 @@ module Bundler
|
||||
class InstallError < BundlerError; status_code(5); end
|
||||
|
||||
# Internal error, should be rescued
|
||||
class VersionConflict < BundlerError
|
||||
attr_reader :conflicts
|
||||
|
||||
def initialize(conflicts, msg = nil)
|
||||
super(msg)
|
||||
@conflicts = conflicts
|
||||
end
|
||||
|
||||
status_code(6)
|
||||
end
|
||||
class SolveFailure < BundlerError; status_code(6); end
|
||||
|
||||
class GemNotFound < BundlerError; status_code(7); end
|
||||
class InstallHookError < BundlerError; status_code(8); end
|
||||
|
@ -7,9 +7,7 @@ module Bundler
|
||||
# available dependency versions as found in its index, before returning it to
|
||||
# to the resolution engine to select the best version.
|
||||
class GemVersionPromoter
|
||||
DEBUG = ENV["BUNDLER_DEBUG_RESOLVER"] || ENV["DEBUG_RESOLVER"]
|
||||
|
||||
attr_reader :level, :locked_specs, :unlock_gems
|
||||
attr_reader :level
|
||||
|
||||
# By default, strict is false, meaning every available version of a gem
|
||||
# is returned from sort_versions. The order gives preference to the
|
||||
@ -24,24 +22,12 @@ module Bundler
|
||||
# existing in the referenced source.
|
||||
attr_accessor :strict
|
||||
|
||||
attr_accessor :prerelease_specified
|
||||
|
||||
# Given a list of locked_specs and a list of gems to unlock creates a
|
||||
# GemVersionPromoter instance.
|
||||
# Creates a GemVersionPromoter instance.
|
||||
#
|
||||
# @param locked_specs [SpecSet] All current locked specs. Unlike Definition
|
||||
# where this list is empty if all gems are being updated, this should
|
||||
# always be populated for all gems so this class can properly function.
|
||||
# @param unlock_gems [String] List of gem names being unlocked. If empty,
|
||||
# all gems will be considered unlocked.
|
||||
# @return [GemVersionPromoter]
|
||||
def initialize(locked_specs = SpecSet.new([]), unlock_gems = [])
|
||||
def initialize
|
||||
@level = :major
|
||||
@strict = false
|
||||
@locked_specs = locked_specs
|
||||
@unlock_gems = unlock_gems
|
||||
@sort_versions = {}
|
||||
@prerelease_specified = {}
|
||||
end
|
||||
|
||||
# @param value [Symbol] One of three Symbols: :major, :minor or :patch.
|
||||
@ -55,34 +41,19 @@ module Bundler
|
||||
@level = v
|
||||
end
|
||||
|
||||
# Given a Dependency and an Array of Specifications of available versions for a
|
||||
# gem, this method will return the Array of Specifications sorted (and possibly
|
||||
# truncated if strict is true) in an order to give preference to the current
|
||||
# level (:major, :minor or :patch) when resolution is deciding what versions
|
||||
# best resolve all dependencies in the bundle.
|
||||
# @param dep [Dependency] The Dependency of the gem.
|
||||
# @param spec_groups [Specification] An array of Specifications for the same gem
|
||||
# named in the @dep param.
|
||||
# Given a Resolver::Package and an Array of Specifications of available
|
||||
# versions for a gem, this method will return the Array of Specifications
|
||||
# sorted (and possibly truncated if strict is true) in an order to give
|
||||
# preference to the current level (:major, :minor or :patch) when resolution
|
||||
# is deciding what versions best resolve all dependencies in the bundle.
|
||||
# @param package [Resolver::Package] The package being resolved.
|
||||
# @param specs [Specification] An array of Specifications for the package.
|
||||
# @return [Specification] A new instance of the Specification Array sorted and
|
||||
# possibly filtered.
|
||||
def sort_versions(dep, spec_groups)
|
||||
@sort_versions[dep] ||= begin
|
||||
gem_name = dep.name
|
||||
def sort_versions(package, specs)
|
||||
specs = filter_dep_specs(specs, package) if strict
|
||||
|
||||
# An Array per version returned, different entries for different platforms.
|
||||
# We only need the version here so it's ok to hard code this to the first instance.
|
||||
locked_spec = locked_specs[gem_name].first
|
||||
|
||||
if strict
|
||||
filter_dep_specs(spec_groups, locked_spec)
|
||||
else
|
||||
sort_dep_specs(spec_groups, locked_spec)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def reset
|
||||
@sort_versions = {}
|
||||
sort_dep_specs(specs, package)
|
||||
end
|
||||
|
||||
# @return [bool] Convenience method for testing value of level variable.
|
||||
@ -97,11 +68,13 @@ module Bundler
|
||||
|
||||
private
|
||||
|
||||
def filter_dep_specs(spec_groups, locked_spec)
|
||||
res = spec_groups.select do |spec_group|
|
||||
if locked_spec && !major?
|
||||
gsv = spec_group.version
|
||||
lsv = locked_spec.version
|
||||
def filter_dep_specs(specs, package)
|
||||
locked_version = package.locked_version
|
||||
|
||||
specs.select do |spec|
|
||||
if locked_version && !major?
|
||||
gsv = spec.version
|
||||
lsv = locked_version
|
||||
|
||||
must_match = minor? ? [0] : [0, 1]
|
||||
|
||||
@ -111,63 +84,53 @@ module Bundler
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
sort_dep_specs(res, locked_spec)
|
||||
end
|
||||
|
||||
def sort_dep_specs(spec_groups, locked_spec)
|
||||
@locked_version = locked_spec&.version
|
||||
@gem_name = locked_spec&.name
|
||||
def sort_dep_specs(specs, package)
|
||||
locked_version = package.locked_version
|
||||
|
||||
result = spec_groups.sort do |a, b|
|
||||
@a_ver = a.version
|
||||
@b_ver = b.version
|
||||
|
||||
unless @gem_name && @prerelease_specified[@gem_name]
|
||||
a_pre = @a_ver.prerelease?
|
||||
b_pre = @b_ver.prerelease?
|
||||
result = specs.sort do |a, b|
|
||||
unless locked_version && package.prerelease_specified?
|
||||
a_pre = a.prerelease?
|
||||
b_pre = b.prerelease?
|
||||
|
||||
next -1 if a_pre && !b_pre
|
||||
next 1 if b_pre && !a_pre
|
||||
end
|
||||
|
||||
if major?
|
||||
@a_ver <=> @b_ver
|
||||
elsif either_version_older_than_locked
|
||||
@a_ver <=> @b_ver
|
||||
elsif segments_do_not_match(:major)
|
||||
@b_ver <=> @a_ver
|
||||
elsif !minor? && segments_do_not_match(:minor)
|
||||
@b_ver <=> @a_ver
|
||||
a <=> b
|
||||
elsif either_version_older_than_locked(a, b, locked_version)
|
||||
a <=> b
|
||||
elsif segments_do_not_match(a, b, :major)
|
||||
b <=> a
|
||||
elsif !minor? && segments_do_not_match(a, b, :minor)
|
||||
b <=> a
|
||||
else
|
||||
@a_ver <=> @b_ver
|
||||
a <=> b
|
||||
end
|
||||
end
|
||||
post_sort(result)
|
||||
post_sort(result, package.unlock?, locked_version)
|
||||
end
|
||||
|
||||
def either_version_older_than_locked
|
||||
@locked_version && (@a_ver < @locked_version || @b_ver < @locked_version)
|
||||
def either_version_older_than_locked(a, b, locked_version)
|
||||
locked_version && (a.version < locked_version || b.version < locked_version)
|
||||
end
|
||||
|
||||
def segments_do_not_match(level)
|
||||
def segments_do_not_match(a, b, level)
|
||||
index = [:major, :minor].index(level)
|
||||
@a_ver.segments[index] != @b_ver.segments[index]
|
||||
end
|
||||
|
||||
def unlocking_gem?
|
||||
unlock_gems.empty? || (@gem_name && unlock_gems.include?(@gem_name))
|
||||
a.segments[index] != b.segments[index]
|
||||
end
|
||||
|
||||
# Specific version moves can't always reliably be done during sorting
|
||||
# as not all elements are compared against each other.
|
||||
def post_sort(result)
|
||||
def post_sort(result, unlock, locked_version)
|
||||
# default :major behavior in Bundler does not do this
|
||||
return result if major?
|
||||
if unlocking_gem? || @locked_version.nil?
|
||||
if unlock || locked_version.nil?
|
||||
result
|
||||
else
|
||||
move_version_to_end(result, @locked_version)
|
||||
move_version_to_end(result, locked_version)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -70,7 +70,7 @@ module Bundler
|
||||
case query
|
||||
when Gem::Specification, RemoteSpecification, LazySpecification, EndpointSpecification then search_by_spec(query)
|
||||
when String then specs_by_name(query)
|
||||
when Gem::Dependency then search_by_dependency(query)
|
||||
when Array then specs_by_name_and_version(*query)
|
||||
else
|
||||
raise "You can't search for a #{query.inspect}."
|
||||
end
|
||||
@ -157,20 +157,12 @@ module Bundler
|
||||
|
||||
private
|
||||
|
||||
def specs_by_name(name)
|
||||
@specs[name].values
|
||||
def specs_by_name_and_version(name, version)
|
||||
specs_by_name(name).select {|spec| spec.version == version }
|
||||
end
|
||||
|
||||
def search_by_dependency(dependency)
|
||||
@cache[dependency] ||= begin
|
||||
specs = specs_by_name(dependency.name)
|
||||
found = specs.select do |spec|
|
||||
next true if spec.source.is_a?(Source::Gemspec)
|
||||
dependency.matches_spec?(spec)
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
def specs_by_name(name)
|
||||
@specs[name].values
|
||||
end
|
||||
|
||||
EMPTY_SEARCH = [].freeze
|
||||
|
@ -34,7 +34,8 @@ def gemfile(install = false, options = {}, &gemfile)
|
||||
|
||||
opts = options.dup
|
||||
ui = opts.delete(:ui) { Bundler::UI::Shell.new }
|
||||
ui.level = "silent" if opts.delete(:quiet)
|
||||
ui.level = "silent" if opts.delete(:quiet) || !install
|
||||
Bundler.ui = ui
|
||||
raise ArgumentError, "Unknown options: #{opts.keys.join(", ")}" unless opts.empty?
|
||||
|
||||
begin
|
||||
@ -52,7 +53,6 @@ def gemfile(install = false, options = {}, &gemfile)
|
||||
def definition.lock(*); end
|
||||
definition.validate_runtime!
|
||||
|
||||
Bundler.ui = install ? ui : Bundler::UI::Silent.new
|
||||
if install || definition.missing_specs?
|
||||
Bundler.settings.temporary(:inline => true, :no_install => false) do
|
||||
installer = Bundler::Installer.install(Bundler.root, definition, :system => true)
|
||||
|
@ -79,7 +79,7 @@ module Bundler
|
||||
candidates = if source.is_a?(Source::Path) || !ruby_platform_materializes_to_ruby_platform?
|
||||
target_platform = ruby_platform_materializes_to_ruby_platform? ? platform : local_platform
|
||||
|
||||
GemHelpers.select_best_platform_match(source.specs.search(Dependency.new(name, version)), target_platform)
|
||||
GemHelpers.select_best_platform_match(source.specs.search([name, version]), target_platform)
|
||||
else
|
||||
source.specs.search(self)
|
||||
end
|
||||
|
@ -1,81 +1,128 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
#
|
||||
# This class implements the interface needed by PubGrub for resolution. It is
|
||||
# equivalent to the `PubGrub::BasicPackageSource` class provided by PubGrub by
|
||||
# default and used by the most simple PubGrub consumers.
|
||||
#
|
||||
class Resolver
|
||||
require_relative "vendored_molinillo"
|
||||
require_relative "vendored_pub_grub"
|
||||
require_relative "resolver/base"
|
||||
require_relative "resolver/spec_group"
|
||||
require_relative "resolver/package"
|
||||
require_relative "resolver/candidate"
|
||||
require_relative "resolver/root"
|
||||
|
||||
include GemHelpers
|
||||
|
||||
def initialize(source_requirements, base, gem_version_promoter, additional_base_requirements, platforms)
|
||||
def initialize(source_requirements, base, gem_version_promoter, additional_base_requirements)
|
||||
@source_requirements = source_requirements
|
||||
@base = Resolver::Base.new(base, additional_base_requirements)
|
||||
@resolver = Molinillo::Resolver.new(self, self)
|
||||
@results_for = {}
|
||||
@search_for = {}
|
||||
@platforms = platforms
|
||||
@resolving_only_for_ruby = platforms == [Gem::Platform::RUBY]
|
||||
@gem_version_promoter = gem_version_promoter
|
||||
end
|
||||
|
||||
def start(requirements, exclude_specs: [])
|
||||
@metadata_requirements, regular_requirements = requirements.partition {|dep| dep.name.end_with?("\0") }
|
||||
|
||||
def start(requirements, packages, exclude_specs: [])
|
||||
exclude_specs.each do |spec|
|
||||
remove_from_candidates(spec)
|
||||
end
|
||||
|
||||
requirements.each {|dep| prerelease_specified[dep.name] ||= dep.prerelease? }
|
||||
root = Resolver::Root.new(name_for_explicit_dependency_source)
|
||||
root_version = Resolver::Candidate.new(0)
|
||||
|
||||
verify_gemfile_dependencies_are_found!(requirements)
|
||||
result = @resolver.resolve(requirements).
|
||||
map(&:payload).
|
||||
reject {|sg| sg.name.end_with?("\0") }.
|
||||
map(&:to_specs).
|
||||
flatten
|
||||
|
||||
SpecSet.new(SpecSet.new(result).for(regular_requirements, false, @platforms))
|
||||
rescue Molinillo::VersionConflict => e
|
||||
conflicts = e.conflicts
|
||||
|
||||
deps_to_unlock = conflicts.values.inject([]) do |deps, conflict|
|
||||
deps |= conflict.requirement_trees.flatten.map {|req| base_requirements[req.name] }.compact
|
||||
@sorted_versions = Hash.new do |candidates, package|
|
||||
candidates[package] = if package.root?
|
||||
[root_version]
|
||||
else
|
||||
all_versions_for(package).sort
|
||||
end
|
||||
end
|
||||
|
||||
if deps_to_unlock.any?
|
||||
@base.unlock_deps(deps_to_unlock)
|
||||
reset_spec_cache
|
||||
root_dependencies = prepare_dependencies(requirements, packages)
|
||||
|
||||
@cached_dependencies = Hash.new do |dependencies, package|
|
||||
dependencies[package] = if package.root?
|
||||
{ root_version => root_dependencies }
|
||||
else
|
||||
Hash.new do |versions, version|
|
||||
versions[version] = to_dependency_hash(version.dependencies, packages)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
logger = Bundler::UI::Shell.new
|
||||
logger.level = debug? ? "debug" : "warn"
|
||||
solver = PubGrub::VersionSolver.new(:source => self, :root => root, :logger => logger)
|
||||
before_resolution
|
||||
result = solver.solve
|
||||
after_resolution
|
||||
result.map {|package, version| version.to_specs(package) }.flatten.uniq
|
||||
rescue PubGrub::SolveFailure => e
|
||||
incompatibility = e.incompatibility
|
||||
|
||||
names_to_unlock = []
|
||||
conflict_on_bundler = nil
|
||||
|
||||
while incompatibility.conflict?
|
||||
cause = incompatibility.cause
|
||||
incompatibility = cause.incompatibility
|
||||
|
||||
incompatibility.terms.each do |term|
|
||||
name = term.package.name
|
||||
names_to_unlock << name if base_requirements[name]
|
||||
next unless name == "bundler"
|
||||
|
||||
no_versions_incompat = [cause.incompatibility, cause.satisfier].find {|incompat| incompat.cause.is_a?(PubGrub::Incompatibility::NoVersions) }
|
||||
next unless no_versions_incompat
|
||||
|
||||
conflict_on_bundler ||= Gem::Requirement.new(no_versions_incompat.cause.constraint.constraint.constraint_string.split(","))
|
||||
end
|
||||
end
|
||||
|
||||
if names_to_unlock.any?
|
||||
@base.unlock_names(names_to_unlock)
|
||||
retry
|
||||
end
|
||||
|
||||
message = version_conflict_message(e)
|
||||
raise VersionConflict.new(conflicts.keys.uniq, message)
|
||||
rescue Molinillo::CircularDependencyError => e
|
||||
names = e.dependencies.sort_by(&:name).map {|d| "gem '#{d.name}'" }
|
||||
raise CyclicDependencyError, "Your bundle requires gems that depend" \
|
||||
" on each other, creating an infinite loop. Please remove" \
|
||||
" #{names.count > 1 ? "either " : ""}#{names.join(" or ")}" \
|
||||
" and try again."
|
||||
explanation = e.message
|
||||
|
||||
if conflict_on_bundler
|
||||
explanation << "\n\n"
|
||||
explanation << bundler_not_found_message(conflict_on_bundler)
|
||||
end
|
||||
|
||||
raise SolveFailure.new(explanation)
|
||||
end
|
||||
|
||||
include Molinillo::UI
|
||||
def parse_dependency(package, dependency)
|
||||
range = if repository_for(package).is_a?(Source::Gemspec)
|
||||
PubGrub::VersionRange.any
|
||||
else
|
||||
requirement_to_range(dependency)
|
||||
end
|
||||
|
||||
# Conveys debug information to the user.
|
||||
#
|
||||
# @param [Integer] depth the current depth of the resolution process.
|
||||
# @return [void]
|
||||
def debug(depth = 0)
|
||||
return unless debug?
|
||||
debug_info = yield
|
||||
debug_info = debug_info.inspect unless debug_info.is_a?(String)
|
||||
puts debug_info.split("\n").map {|s| depth == 0 ? "BUNDLER: #{s}" : "BUNDLER(#{depth}): #{s}" }
|
||||
PubGrub::VersionConstraint.new(package, :range => range)
|
||||
end
|
||||
|
||||
def versions_for(package, range=VersionRange.any)
|
||||
versions = range.select_versions(@sorted_versions[package])
|
||||
|
||||
sort_versions(package, versions)
|
||||
end
|
||||
|
||||
def no_versions_incompatibility_for(package, unsatisfied_term)
|
||||
cause = PubGrub::Incompatibility::NoVersions.new(unsatisfied_term)
|
||||
|
||||
custom_explanation = if package.name == "bundler"
|
||||
"the current Bundler version (#{Bundler::VERSION}) does not satisfy #{cause.constraint}"
|
||||
else
|
||||
"#{cause.constraint} could not be found in #{repository_for(package)}"
|
||||
end
|
||||
|
||||
PubGrub::Incompatibility.new([unsatisfied_term], :cause => cause, :custom_explanation => custom_explanation)
|
||||
end
|
||||
|
||||
def debug?
|
||||
return @debug_mode if defined?(@debug_mode)
|
||||
@debug_mode =
|
||||
ENV["BUNDLER_DEBUG_RESOLVER"] ||
|
||||
ENV["BUNDLER_DEBUG_RESOLVER"] ||
|
||||
ENV["BUNDLER_DEBUG_RESOLVER_TREE"] ||
|
||||
ENV["DEBUG_RESOLVER"] ||
|
||||
ENV["DEBUG_RESOLVER_TREE"] ||
|
||||
@ -90,63 +137,87 @@ module Bundler
|
||||
Bundler.ui.info ""
|
||||
end
|
||||
|
||||
def indicate_progress
|
||||
Bundler.ui.info ".", false unless debug?
|
||||
end
|
||||
def incompatibilities_for(package, version)
|
||||
package_deps = @cached_dependencies[package]
|
||||
sorted_versions = @sorted_versions[package]
|
||||
package_deps[version].map do |dep_package, dep_constraint|
|
||||
unless dep_constraint
|
||||
# falsey indicates this dependency was invalid
|
||||
cause = PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint.constraint_string)
|
||||
return [PubGrub::Incompatibility.new([PubGrub::Term.new(self_constraint, true)], :cause => cause)]
|
||||
end
|
||||
|
||||
include Molinillo::SpecificationProvider
|
||||
low = high = sorted_versions.index(version)
|
||||
|
||||
def dependencies_for(specification)
|
||||
specification.dependencies_for_activated_platforms
|
||||
end
|
||||
|
||||
def search_for(dependency)
|
||||
@search_for[dependency] ||= begin
|
||||
name = dependency.name
|
||||
locked_results = @base[name].select {|spec| requirement_satisfied_by?(dependency, nil, spec) }
|
||||
locked_requirement = base_requirements[name]
|
||||
results = results_for(dependency) + locked_results
|
||||
results = results.select {|spec| requirement_satisfied_by?(locked_requirement, nil, spec) } if locked_requirement
|
||||
dep_platforms = dependency.gem_platforms(@platforms)
|
||||
|
||||
@gem_version_promoter.sort_versions(dependency, results).group_by(&:version).reduce([]) do |groups, (_, specs)|
|
||||
relevant_platforms = dep_platforms.select {|platform| specs.any? {|spec| spec.match_platform(platform) } }
|
||||
next groups unless relevant_platforms.any?
|
||||
|
||||
ruby_specs = select_best_platform_match(specs, Gem::Platform::RUBY)
|
||||
if ruby_specs.any?
|
||||
spec_group_ruby = SpecGroup.new(ruby_specs, [Gem::Platform::RUBY])
|
||||
spec_group_ruby.force_ruby_platform = dependency.force_ruby_platform
|
||||
groups << spec_group_ruby
|
||||
# find version low such that all >= low share the same dep
|
||||
while low > 0 && package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint
|
||||
low -= 1
|
||||
end
|
||||
low =
|
||||
if low == 0
|
||||
nil
|
||||
else
|
||||
sorted_versions[low]
|
||||
end
|
||||
|
||||
next groups if @resolving_only_for_ruby || dependency.force_ruby_platform
|
||||
|
||||
platform_specs = relevant_platforms.flat_map {|platform| select_best_platform_match(specs, platform) }
|
||||
next groups if platform_specs == ruby_specs
|
||||
|
||||
spec_group = SpecGroup.new(platform_specs, relevant_platforms)
|
||||
groups << spec_group
|
||||
|
||||
groups
|
||||
# find version high such that all < high share the same dep
|
||||
while high < sorted_versions.length && package_deps[sorted_versions[high]][dep_package] == dep_constraint
|
||||
high += 1
|
||||
end
|
||||
high =
|
||||
if high == sorted_versions.length
|
||||
nil
|
||||
else
|
||||
sorted_versions[high]
|
||||
end
|
||||
|
||||
range = PubGrub::VersionRange.new(:min => low, :max => high, :include_min => true)
|
||||
|
||||
self_constraint = PubGrub::VersionConstraint.new(package, :range => range)
|
||||
|
||||
dep_term = PubGrub::Term.new(dep_constraint, false)
|
||||
|
||||
custom_explanation = if dep_package.meta? && package.root?
|
||||
"current #{dep_package} version is #{dep_constraint.constraint_string}"
|
||||
end
|
||||
|
||||
PubGrub::Incompatibility.new([PubGrub::Term.new(self_constraint, true), dep_term], :cause => :dependency, :custom_explanation => custom_explanation)
|
||||
end
|
||||
end
|
||||
|
||||
def index_for(dependency)
|
||||
source_for(dependency.name).specs
|
||||
def all_versions_for(package)
|
||||
name = package.name
|
||||
results = @base[name] + results_for(name)
|
||||
locked_requirement = base_requirements[name]
|
||||
results = results.select {|spec| requirement_satisfied_by?(locked_requirement, spec) } if locked_requirement
|
||||
|
||||
versions = results.group_by(&:version).reduce([]) do |groups, (version, specs)|
|
||||
platform_specs = package.platforms.flat_map {|platform| select_best_platform_match(specs, platform) }
|
||||
next groups if platform_specs.empty?
|
||||
|
||||
ruby_specs = select_best_platform_match(specs, Gem::Platform::RUBY)
|
||||
groups << Resolver::Candidate.new(version, :specs => ruby_specs) if ruby_specs.any?
|
||||
|
||||
next groups if platform_specs == ruby_specs
|
||||
|
||||
groups << Resolver::Candidate.new(version, :specs => platform_specs)
|
||||
|
||||
groups
|
||||
end
|
||||
|
||||
sort_versions(package, versions)
|
||||
end
|
||||
|
||||
def index_for(name)
|
||||
source_for(name).specs
|
||||
end
|
||||
|
||||
def source_for(name)
|
||||
@source_requirements[name] || @source_requirements[:default]
|
||||
end
|
||||
|
||||
def results_for(dependency)
|
||||
@results_for[dependency] ||= index_for(dependency).search(dependency)
|
||||
end
|
||||
|
||||
def name_for(dependency)
|
||||
dependency.name
|
||||
def results_for(name)
|
||||
index_for(name).search(name)
|
||||
end
|
||||
|
||||
def name_for_explicit_dependency_source
|
||||
@ -155,107 +226,66 @@ module Bundler
|
||||
"Gemfile"
|
||||
end
|
||||
|
||||
def requirement_satisfied_by?(requirement, activated, spec)
|
||||
requirement.matches_spec?(spec) || spec.source.is_a?(Source::Gemspec)
|
||||
end
|
||||
|
||||
def sort_dependencies(dependencies, activated, conflicts)
|
||||
dependencies.sort_by do |dependency|
|
||||
name = name_for(dependency)
|
||||
vertex = activated.vertex_named(name)
|
||||
[
|
||||
@base[name].any? ? 0 : 1,
|
||||
vertex.payload ? 0 : 1,
|
||||
vertex.root? ? 0 : 1,
|
||||
amount_constrained(dependency),
|
||||
conflicts[name] ? 0 : 1,
|
||||
vertex.payload ? 0 : search_for(dependency).count,
|
||||
]
|
||||
end
|
||||
def requirement_satisfied_by?(requirement, spec)
|
||||
requirement.satisfied_by?(spec.version) || spec.source.is_a?(Source::Gemspec)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sort_versions(package, versions)
|
||||
if versions.size > 1
|
||||
@gem_version_promoter.sort_versions(package, versions).reverse
|
||||
else
|
||||
versions
|
||||
end
|
||||
end
|
||||
|
||||
def repository_for(package)
|
||||
source_for(package.name)
|
||||
end
|
||||
|
||||
def base_requirements
|
||||
@base.base_requirements
|
||||
end
|
||||
|
||||
def prerelease_specified
|
||||
@gem_version_promoter.prerelease_specified
|
||||
end
|
||||
|
||||
def remove_from_candidates(spec)
|
||||
@base.delete(spec)
|
||||
|
||||
@results_for.keys.each do |dep|
|
||||
next unless dep.name == spec.name
|
||||
|
||||
@results_for[dep].reject {|s| s.name == spec.name && s.version == spec.version }
|
||||
end
|
||||
|
||||
reset_spec_cache
|
||||
end
|
||||
|
||||
def reset_spec_cache
|
||||
@search_for = {}
|
||||
@gem_version_promoter.reset
|
||||
def prepare_dependencies(requirements, packages)
|
||||
to_dependency_hash(requirements, packages).map do |dep_package, dep_constraint|
|
||||
name = dep_package.name
|
||||
next if dep_package.platforms.empty?
|
||||
next [dep_package, dep_constraint] if name == "bundler"
|
||||
next [dep_package, dep_constraint] unless versions_for(dep_package, dep_constraint.range).empty?
|
||||
next unless dep_package.current_platform?
|
||||
|
||||
raise GemNotFound, gem_not_found_message(dep_package, dep_constraint)
|
||||
end.compact.to_h
|
||||
end
|
||||
|
||||
# returns an integer \in (-\infty, 0]
|
||||
# a number closer to 0 means the dependency is less constraining
|
||||
#
|
||||
# dependencies w/ 0 or 1 possibilities (ignoring version requirements)
|
||||
# are given very negative values, so they _always_ sort first,
|
||||
# before dependencies that are unconstrained
|
||||
def amount_constrained(dependency)
|
||||
@amount_constrained ||= {}
|
||||
@amount_constrained[dependency.name] ||= if (base = @base[dependency.name]) && !base.empty?
|
||||
dependency.requirement.satisfied_by?(base.first.version) ? 0 : 1
|
||||
else
|
||||
all = index_for(dependency).search(dependency.name).size
|
||||
|
||||
if all <= 1
|
||||
all - 1_000_000
|
||||
else
|
||||
search = search_for(dependency)
|
||||
search = prerelease_specified[dependency.name] ? search.count : search.count {|s| !s.version.prerelease? }
|
||||
search - all
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def verify_gemfile_dependencies_are_found!(requirements)
|
||||
requirements.map! do |requirement|
|
||||
name = requirement.name
|
||||
next requirement if name == "bundler"
|
||||
next if requirement.gem_platforms(@platforms).empty?
|
||||
next requirement unless search_for(requirement).empty?
|
||||
next unless requirement.current_platform?
|
||||
|
||||
raise GemNotFound, gem_not_found_message(name, requirement, source_for(name))
|
||||
end.compact!
|
||||
end
|
||||
|
||||
def gem_not_found_message(name, requirement, source, extra_message = "")
|
||||
def gem_not_found_message(package, requirement)
|
||||
name = package.name
|
||||
source = source_for(name)
|
||||
specs = source.specs.search(name).sort_by {|s| [s.version, s.platform.to_s] }
|
||||
matching_part = name
|
||||
requirement_label = SharedHelpers.pretty_dependency(requirement)
|
||||
requirement_label = SharedHelpers.pretty_dependency(package.dependency)
|
||||
cache_message = begin
|
||||
" or in gems cached in #{Bundler.settings.app_cache_path}" if Bundler.app_cache.exist?
|
||||
rescue GemfileNotFound
|
||||
nil
|
||||
end
|
||||
specs_matching_requirement = specs.select {| spec| requirement.matches_spec?(spec) }
|
||||
specs_matching_requirement = specs.select {| spec| requirement_satisfied_by?(package.dependency.requirement, spec) }
|
||||
|
||||
if specs_matching_requirement.any?
|
||||
specs = specs_matching_requirement
|
||||
matching_part = requirement_label
|
||||
platforms = requirement.gem_platforms(@platforms)
|
||||
platforms = package.platforms
|
||||
platform_label = platforms.size == 1 ? "platform '#{platforms.first}" : "platforms '#{platforms.join("', '")}"
|
||||
requirement_label = "#{requirement_label}' with #{platform_label}"
|
||||
end
|
||||
|
||||
message = String.new("Could not find gem '#{requirement_label}'#{extra_message} in #{source}#{cache_message}.\n")
|
||||
message = String.new("Could not find gem '#{requirement_label}' in #{source}#{cache_message}.\n")
|
||||
|
||||
if specs.any?
|
||||
message << "\nThe source contains the following gems matching '#{matching_part}':\n"
|
||||
@ -265,116 +295,62 @@ module Bundler
|
||||
message
|
||||
end
|
||||
|
||||
def version_conflict_message(e)
|
||||
# only show essential conflicts, if possible
|
||||
conflicts = e.conflicts.dup
|
||||
def requirement_to_range(requirement)
|
||||
ranges = requirement.requirements.map do |(op, version)|
|
||||
ver = Resolver::Candidate.new(version)
|
||||
|
||||
if conflicts["bundler"]
|
||||
conflicts.replace("bundler" => conflicts["bundler"])
|
||||
else
|
||||
conflicts.delete_if do |_name, conflict|
|
||||
deps = conflict.requirement_trees.map(&:last).flatten(1)
|
||||
!Bundler::VersionRanges.empty?(*Bundler::VersionRanges.for_many(deps.map(&:requirement)))
|
||||
case op
|
||||
when "~>"
|
||||
name = "~> #{ver}"
|
||||
bump = Resolver::Candidate.new(version.bump.to_s + ".A")
|
||||
PubGrub::VersionRange.new(:name => name, :min => ver, :max => bump, :include_min => true)
|
||||
when ">"
|
||||
PubGrub::VersionRange.new(:min => ver)
|
||||
when ">="
|
||||
PubGrub::VersionRange.new(:min => ver, :include_min => true)
|
||||
when "<"
|
||||
PubGrub::VersionRange.new(:max => ver)
|
||||
when "<="
|
||||
PubGrub::VersionRange.new(:max => ver, :include_max => true)
|
||||
when "="
|
||||
PubGrub::VersionRange.new(:min => ver, :max => ver, :include_min => true, :include_max => true)
|
||||
when "!="
|
||||
PubGrub::VersionRange.new(:min => ver, :max => ver, :include_min => true, :include_max => true).invert
|
||||
else
|
||||
raise "bad version specifier: #{op}"
|
||||
end
|
||||
end
|
||||
|
||||
e = Molinillo::VersionConflict.new(conflicts, e.specification_provider) unless conflicts.empty?
|
||||
ranges.inject(&:intersect)
|
||||
end
|
||||
|
||||
e.message_with_trees(
|
||||
:full_message_for_conflict => lambda do |name, conflict|
|
||||
trees = conflict.requirement_trees
|
||||
def to_dependency_hash(dependencies, packages)
|
||||
dependencies.inject({}) do |deps, dep|
|
||||
package = packages[dep.name]
|
||||
|
||||
# called first, because we want to reduce the amount of work required to find maximal empty sets
|
||||
trees = trees.uniq {|t| t.flatten.map {|dep| [dep.name, dep.requirement] } }
|
||||
current_req = deps[package]
|
||||
new_req = parse_dependency(package, dep.requirement)
|
||||
|
||||
# bail out if tree size is too big for Array#combination to make any sense
|
||||
if trees.size <= 15
|
||||
maximal = 1.upto(trees.size).map do |size|
|
||||
trees.map(&:last).flatten(1).combination(size).to_a
|
||||
end.flatten(1).select do |deps|
|
||||
Bundler::VersionRanges.empty?(*Bundler::VersionRanges.for_many(deps.map(&:requirement)))
|
||||
end.min_by(&:size)
|
||||
|
||||
trees.reject! {|t| !maximal.include?(t.last) } if maximal
|
||||
|
||||
trees.sort_by! {|t| t.reverse.map(&:name) }
|
||||
end
|
||||
|
||||
if trees.size > 1 || name == "bundler"
|
||||
o = if name.end_with?("\0")
|
||||
String.new("Bundler found conflicting requirements for the #{name} version:")
|
||||
else
|
||||
String.new("Bundler could not find compatible versions for gem \"#{name}\":")
|
||||
end
|
||||
o << %(\n)
|
||||
o << %( In #{name_for_explicit_dependency_source}:\n)
|
||||
o << trees.map do |tree|
|
||||
t = "".dup
|
||||
depth = 2
|
||||
|
||||
base_tree = tree.first
|
||||
base_tree_name = base_tree.name
|
||||
|
||||
if base_tree_name.end_with?("\0")
|
||||
t = nil
|
||||
else
|
||||
tree.each do |req|
|
||||
t << " " * depth << SharedHelpers.pretty_dependency(req)
|
||||
unless tree.last == req
|
||||
if spec = conflict.activated_by_name[req.name]
|
||||
t << %( was resolved to #{spec.version}, which)
|
||||
end
|
||||
t << %( depends on)
|
||||
end
|
||||
t << %(\n)
|
||||
depth += 1
|
||||
end
|
||||
end
|
||||
t
|
||||
end.compact.join("\n")
|
||||
else
|
||||
o = String.new
|
||||
end
|
||||
|
||||
if name == "bundler"
|
||||
o << %(\n Current Bundler version:\n bundler (#{Bundler::VERSION}))
|
||||
|
||||
conflict_dependency = conflict.requirement
|
||||
conflict_requirement = conflict_dependency.requirement
|
||||
other_bundler_required = !conflict_requirement.satisfied_by?(Gem::Version.new(Bundler::VERSION))
|
||||
|
||||
if other_bundler_required
|
||||
o << "\n\n"
|
||||
|
||||
candidate_specs = source_for(:default_bundler).specs.search(conflict_dependency)
|
||||
if candidate_specs.any?
|
||||
target_version = candidate_specs.last.version
|
||||
new_command = [File.basename($PROGRAM_NAME), "_#{target_version}_", *ARGV].join(" ")
|
||||
o << "Your bundle requires a different version of Bundler than the one you're running.\n"
|
||||
o << "Install the necessary version with `gem install bundler:#{target_version}` and rerun bundler using `#{new_command}`\n"
|
||||
else
|
||||
o << "Your bundle requires a different version of Bundler than the one you're running, and that version could not be found.\n"
|
||||
end
|
||||
end
|
||||
elsif name.end_with?("\0")
|
||||
o << %(\n Current #{name} version:\n #{SharedHelpers.pretty_dependency(@metadata_requirements.find {|req| req.name == name })}\n\n)
|
||||
elsif !conflict.existing
|
||||
o << "\n"
|
||||
|
||||
relevant_source = conflict.requirement.source || source_for(name)
|
||||
|
||||
extra_message = if trees.first.size > 1
|
||||
", which is required by gem '#{SharedHelpers.pretty_dependency(trees.first[-2])}',"
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
o << gem_not_found_message(name, conflict.requirement, relevant_source, extra_message)
|
||||
end
|
||||
|
||||
o
|
||||
deps[package] = if current_req
|
||||
current_req.intersect(new_req)
|
||||
else
|
||||
new_req
|
||||
end
|
||||
)
|
||||
|
||||
deps
|
||||
end
|
||||
end
|
||||
|
||||
def bundler_not_found_message(conflict_dependency)
|
||||
candidate_specs = source_for(:default_bundler).specs.search("bundler").select {|spec| requirement_satisfied_by?(conflict_dependency, spec) }
|
||||
if candidate_specs.any?
|
||||
target_version = candidate_specs.last.version
|
||||
new_command = [File.basename($PROGRAM_NAME), "_#{target_version}_", *ARGV].join(" ")
|
||||
"Your bundle requires a different version of Bundler than the one you're running.\n" \
|
||||
"Install the necessary version with `gem install bundler:#{target_version}` and rerun bundler using `#{new_command}`\n"
|
||||
else
|
||||
"Your bundle requires a different version of Bundler than the one you're running, and that version could not be found.\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -20,15 +20,11 @@ module Bundler
|
||||
@base_requirements ||= build_base_requirements
|
||||
end
|
||||
|
||||
def unlock_deps(deps)
|
||||
exact, lower_bound = deps.partition(&:specific?)
|
||||
def unlock_names(names)
|
||||
names.each do |name|
|
||||
@base.delete_by_name(name)
|
||||
|
||||
exact.each do |exact_dep|
|
||||
@base.delete_by_name_and_version(exact_dep.name, exact_dep.requirement.requirements.first.last)
|
||||
end
|
||||
|
||||
lower_bound.each do |lower_bound_dep|
|
||||
@additional_base_requirements.delete(lower_bound_dep)
|
||||
@additional_base_requirements.reject! {|dep| dep.name == name }
|
||||
end
|
||||
|
||||
@base_requirements = nil
|
||||
@ -39,10 +35,10 @@ module Bundler
|
||||
def build_base_requirements
|
||||
base_requirements = {}
|
||||
@base.each do |ls|
|
||||
dep = Dependency.new(ls.name, ls.version)
|
||||
base_requirements[ls.name] = dep
|
||||
req = Gem::Requirement.new(ls.version)
|
||||
base_requirements[ls.name] = req
|
||||
end
|
||||
@additional_base_requirements.each {|d| base_requirements[d.name] = d }
|
||||
@additional_base_requirements.each {|d| base_requirements[d.name] = d.requirement }
|
||||
base_requirements
|
||||
end
|
||||
end
|
||||
|
92
lib/bundler/resolver/candidate.rb
Normal file
92
lib/bundler/resolver/candidate.rb
Normal file
@ -0,0 +1,92 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "spec_group"
|
||||
|
||||
module Bundler
|
||||
class Resolver
|
||||
#
|
||||
# This class is a PubGrub compatible "Version" class that takes Bundler
|
||||
# resolution complexities into account.
|
||||
#
|
||||
# Each Resolver::Candidate has a underlying `Gem::Version` plus a set of
|
||||
# platforms. For example, 1.1.0-x86_64-linux is a different resolution candidate
|
||||
# from 1.1.0 (generic). This is because different platform variants of the
|
||||
# same gem version can bring different dependencies, so they need to be
|
||||
# considered separately.
|
||||
#
|
||||
# Some candidates may also keep some information explicitly about the
|
||||
# package the refer to. These candidates are referred to as "canonical" and
|
||||
# are used when materializing resolution results back into RubyGems
|
||||
# specifications that can be installed, written to lock files, and so on.
|
||||
#
|
||||
class Candidate
|
||||
include Comparable
|
||||
|
||||
attr_reader :version
|
||||
|
||||
def initialize(version, specs: [])
|
||||
@spec_group = Resolver::SpecGroup.new(specs)
|
||||
@platforms = specs.map(&:platform).sort_by(&:to_s).uniq
|
||||
@version = Gem::Version.new(version)
|
||||
@ruby_only = @platforms == [Gem::Platform::RUBY]
|
||||
end
|
||||
|
||||
def dependencies
|
||||
@spec_group.dependencies
|
||||
end
|
||||
|
||||
def to_specs(package)
|
||||
return [] if package.meta?
|
||||
|
||||
@spec_group.to_specs(package.force_ruby_platform?)
|
||||
end
|
||||
|
||||
def prerelease?
|
||||
@version.prerelease?
|
||||
end
|
||||
|
||||
def segments
|
||||
@version.segments
|
||||
end
|
||||
|
||||
def sort_obj
|
||||
[@version, @ruby_only ? -1 : 1]
|
||||
end
|
||||
|
||||
def canonical?
|
||||
!@spec_group.empty?
|
||||
end
|
||||
|
||||
def <=>(other)
|
||||
return unless other.is_a?(self.class)
|
||||
return @version <=> other.version unless canonical? && other.canonical?
|
||||
|
||||
sort_obj <=> other.sort_obj
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
return unless other.is_a?(self.class)
|
||||
return @version == other.version unless canonical? && other.canonical?
|
||||
|
||||
sort_obj == other.sort_obj
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
return unless other.is_a?(self.class)
|
||||
return @version.eql?(other.version) unless canonical? || other.canonical?
|
||||
|
||||
sort_obj.eql?(other.sort_obj)
|
||||
end
|
||||
|
||||
def hash
|
||||
sort_obj.hash
|
||||
end
|
||||
|
||||
def to_s
|
||||
return @version.to_s if @platforms.empty? || @ruby_only
|
||||
|
||||
"#{@version} (#{@platforms.join(", ")})"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
67
lib/bundler/resolver/package.rb
Normal file
67
lib/bundler/resolver/package.rb
Normal file
@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Resolver
|
||||
#
|
||||
# Represents a gem being resolved, in a format PubGrub likes.
|
||||
#
|
||||
# The class holds the following information:
|
||||
#
|
||||
# * Platforms this gem will be resolved on.
|
||||
# * The locked version of this gem resolution should favor (if any).
|
||||
# * Whether the gem should be unlocked to its latest version.
|
||||
# * The dependency explicit set in the Gemfile for this gem (if any).
|
||||
#
|
||||
class Package
|
||||
attr_reader :name, :platforms, :dependency
|
||||
|
||||
def initialize(name, platforms, locked_specs, unlock, dependency: nil)
|
||||
@name = name
|
||||
@platforms = platforms
|
||||
@locked_specs = locked_specs
|
||||
@unlock = unlock
|
||||
@dependency = dependency
|
||||
end
|
||||
|
||||
def to_s
|
||||
@name.delete("\0")
|
||||
end
|
||||
|
||||
def root?
|
||||
false
|
||||
end
|
||||
|
||||
def meta?
|
||||
@name.end_with?("\0")
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
self.class == other.class && @name == other.name
|
||||
end
|
||||
|
||||
def hash
|
||||
@name.hash
|
||||
end
|
||||
|
||||
def locked_version
|
||||
@locked_specs[name].first&.version
|
||||
end
|
||||
|
||||
def unlock?
|
||||
@unlock.empty? || @unlock.include?(name)
|
||||
end
|
||||
|
||||
def prerelease_specified?
|
||||
@dependency&.prerelease?
|
||||
end
|
||||
|
||||
def force_ruby_platform?
|
||||
@dependency&.force_ruby_platform
|
||||
end
|
||||
|
||||
def current_platform?
|
||||
@dependency&.current_platform?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
25
lib/bundler/resolver/root.rb
Normal file
25
lib/bundler/resolver/root.rb
Normal file
@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "package"
|
||||
|
||||
module Bundler
|
||||
class Resolver
|
||||
#
|
||||
# Represents the Gemfile from the resolver's perspective. It's the root
|
||||
# package and Gemfile entries depend on it.
|
||||
#
|
||||
class Root < Package
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def meta?
|
||||
true
|
||||
end
|
||||
|
||||
def root?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -3,20 +3,27 @@
|
||||
module Bundler
|
||||
class Resolver
|
||||
class SpecGroup
|
||||
attr_accessor :name, :version, :source
|
||||
attr_accessor :activated_platforms, :force_ruby_platform
|
||||
|
||||
def initialize(specs, relevant_platforms)
|
||||
@exemplary_spec = specs.first
|
||||
@name = @exemplary_spec.name
|
||||
@version = @exemplary_spec.version
|
||||
@source = @exemplary_spec.source
|
||||
|
||||
@activated_platforms = relevant_platforms
|
||||
def initialize(specs)
|
||||
@specs = specs
|
||||
end
|
||||
|
||||
def to_specs
|
||||
def empty?
|
||||
@specs.empty?
|
||||
end
|
||||
|
||||
def name
|
||||
@name ||= exemplary_spec.name
|
||||
end
|
||||
|
||||
def version
|
||||
@version ||= exemplary_spec.version
|
||||
end
|
||||
|
||||
def source
|
||||
@source ||= exemplary_spec.source
|
||||
end
|
||||
|
||||
def to_specs(force_ruby_platform)
|
||||
@specs.map do |s|
|
||||
lazy_spec = LazySpecification.new(name, version, s.platform, source)
|
||||
lazy_spec.force_ruby_platform = force_ruby_platform
|
||||
@ -26,44 +33,27 @@ module Bundler
|
||||
end
|
||||
|
||||
def to_s
|
||||
activated_platforms_string = sorted_activated_platforms.join(", ")
|
||||
"#{name} (#{version}) (#{activated_platforms_string})"
|
||||
sorted_spec_names.join(", ")
|
||||
end
|
||||
|
||||
def dependencies_for_activated_platforms
|
||||
@dependencies_for_activated_platforms ||= @specs.map do |spec|
|
||||
def dependencies
|
||||
@dependencies ||= @specs.map do |spec|
|
||||
__dependencies(spec) + metadata_dependencies(spec)
|
||||
end.flatten.uniq
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
return unless other.is_a?(SpecGroup)
|
||||
name == other.name &&
|
||||
version == other.version &&
|
||||
sorted_activated_platforms == other.sorted_activated_platforms &&
|
||||
source == other.source
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
return unless other.is_a?(SpecGroup)
|
||||
name.eql?(other.name) &&
|
||||
version.eql?(other.version) &&
|
||||
sorted_activated_platforms.eql?(other.sorted_activated_platforms) &&
|
||||
source.eql?(other.source)
|
||||
end
|
||||
|
||||
def hash
|
||||
name.hash ^ version.hash ^ sorted_activated_platforms.hash ^ source.hash
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def sorted_activated_platforms
|
||||
activated_platforms.sort_by(&:to_s)
|
||||
def sorted_spec_names
|
||||
@sorted_spec_names ||= @specs.map(&:full_name).sort
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def exemplary_spec
|
||||
@specs.first
|
||||
end
|
||||
|
||||
def __dependencies(spec)
|
||||
dependencies = []
|
||||
spec.dependencies.each do |dep|
|
||||
|
@ -15,7 +15,6 @@ module Bundler
|
||||
s.version = VERSION
|
||||
s.license = "MIT"
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.source = self
|
||||
s.authors = ["bundler team"]
|
||||
s.bindir = "exe"
|
||||
s.homepage = "https://bundler.io"
|
||||
|
@ -122,8 +122,8 @@ module Bundler
|
||||
@specs.detect {|spec| spec.name == name && spec.match_platform(platform) }
|
||||
end
|
||||
|
||||
def delete_by_name_and_version(name, version)
|
||||
@specs.reject! {|spec| spec.name == name && spec.version == version }
|
||||
def delete_by_name(name)
|
||||
@specs.reject! {|spec| spec.name == name }
|
||||
@lookup = nil
|
||||
@sorted = nil
|
||||
end
|
||||
@ -165,7 +165,7 @@ module Bundler
|
||||
cgems = extract_circular_gems(error)
|
||||
raise CyclicDependencyError, "Your bundle requires gems that depend" \
|
||||
" on each other, creating an infinite loop. Please remove either" \
|
||||
" gem '#{cgems[1]}' or gem '#{cgems[0]}' and try again."
|
||||
" gem '#{cgems[0]}' or gem '#{cgems[1]}' and try again."
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -20,29 +20,52 @@ module Bundler
|
||||
@shell.set_color(string, *color)
|
||||
end
|
||||
|
||||
def info(msg, newline = nil)
|
||||
tell_me(msg, nil, newline) if level("info")
|
||||
def info(msg = nil, newline = nil)
|
||||
return unless info?
|
||||
|
||||
tell_me(msg || yield, nil, newline)
|
||||
end
|
||||
|
||||
def confirm(msg, newline = nil)
|
||||
tell_me(msg, :green, newline) if level("confirm")
|
||||
def confirm(msg = nil, newline = nil)
|
||||
return unless confirm?
|
||||
|
||||
tell_me(msg || yield, :green, newline)
|
||||
end
|
||||
|
||||
def warn(msg, newline = nil, color = :yellow)
|
||||
return unless level("warn")
|
||||
def warn(msg = nil, newline = nil, color = :yellow)
|
||||
return unless warn?
|
||||
return if @warning_history.include? msg
|
||||
@warning_history << msg
|
||||
|
||||
tell_err(msg, color, newline)
|
||||
tell_err(msg || yield, color, newline)
|
||||
end
|
||||
|
||||
def error(msg, newline = nil, color = :red)
|
||||
return unless level("error")
|
||||
tell_err(msg, color, newline)
|
||||
def error(msg = nil, newline = nil, color = :red)
|
||||
return unless error?
|
||||
|
||||
tell_err(msg || yield, color, newline)
|
||||
end
|
||||
|
||||
def debug(msg, newline = nil)
|
||||
tell_me(msg, nil, newline) if debug?
|
||||
def debug(msg = nil, newline = nil)
|
||||
return unless debug?
|
||||
|
||||
tell_me(msg || yield, nil, newline)
|
||||
end
|
||||
|
||||
def info?
|
||||
level("info")
|
||||
end
|
||||
|
||||
def confirm?
|
||||
level("confirm")
|
||||
end
|
||||
|
||||
def warn?
|
||||
level("warn")
|
||||
end
|
||||
|
||||
def error?
|
||||
level("error")
|
||||
end
|
||||
|
||||
def debug?
|
||||
|
@ -13,30 +13,46 @@ module Bundler
|
||||
string
|
||||
end
|
||||
|
||||
def info(message, newline = nil)
|
||||
def info(message = nil, newline = nil)
|
||||
end
|
||||
|
||||
def confirm(message, newline = nil)
|
||||
def confirm(message = nil, newline = nil)
|
||||
end
|
||||
|
||||
def warn(message, newline = nil)
|
||||
def warn(message = nil, newline = nil)
|
||||
@warnings |= [message]
|
||||
end
|
||||
|
||||
def error(message, newline = nil)
|
||||
def error(message = nil, newline = nil)
|
||||
end
|
||||
|
||||
def debug(message, newline = nil)
|
||||
def debug(message = nil, newline = nil)
|
||||
end
|
||||
|
||||
def confirm?
|
||||
false
|
||||
end
|
||||
|
||||
def error?
|
||||
false
|
||||
end
|
||||
|
||||
def debug?
|
||||
false
|
||||
end
|
||||
|
||||
def info?
|
||||
false
|
||||
end
|
||||
|
||||
def quiet?
|
||||
false
|
||||
end
|
||||
|
||||
def warn?
|
||||
false
|
||||
end
|
||||
|
||||
def ask(message)
|
||||
end
|
||||
|
||||
|
11
lib/bundler/vendor/molinillo/lib/molinillo.rb
vendored
11
lib/bundler/vendor/molinillo/lib/molinillo.rb
vendored
@ -1,11 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'molinillo/gem_metadata'
|
||||
require_relative 'molinillo/errors'
|
||||
require_relative 'molinillo/resolver'
|
||||
require_relative 'molinillo/modules/ui'
|
||||
require_relative 'molinillo/modules/specification_provider'
|
||||
|
||||
# Bundler::Molinillo is a generic dependency resolution algorithm.
|
||||
module Bundler::Molinillo
|
||||
end
|
@ -1,57 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::Molinillo
|
||||
# @!visibility private
|
||||
module Delegates
|
||||
# Delegates all {Bundler::Molinillo::ResolutionState} methods to a `#state` property.
|
||||
module ResolutionState
|
||||
# (see Bundler::Molinillo::ResolutionState#name)
|
||||
def name
|
||||
current_state = state || Bundler::Molinillo::ResolutionState.empty
|
||||
current_state.name
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::ResolutionState#requirements)
|
||||
def requirements
|
||||
current_state = state || Bundler::Molinillo::ResolutionState.empty
|
||||
current_state.requirements
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::ResolutionState#activated)
|
||||
def activated
|
||||
current_state = state || Bundler::Molinillo::ResolutionState.empty
|
||||
current_state.activated
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::ResolutionState#requirement)
|
||||
def requirement
|
||||
current_state = state || Bundler::Molinillo::ResolutionState.empty
|
||||
current_state.requirement
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::ResolutionState#possibilities)
|
||||
def possibilities
|
||||
current_state = state || Bundler::Molinillo::ResolutionState.empty
|
||||
current_state.possibilities
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::ResolutionState#depth)
|
||||
def depth
|
||||
current_state = state || Bundler::Molinillo::ResolutionState.empty
|
||||
current_state.depth
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::ResolutionState#conflicts)
|
||||
def conflicts
|
||||
current_state = state || Bundler::Molinillo::ResolutionState.empty
|
||||
current_state.conflicts
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::ResolutionState#unused_unwind_options)
|
||||
def unused_unwind_options
|
||||
current_state = state || Bundler::Molinillo::ResolutionState.empty
|
||||
current_state.unused_unwind_options
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,88 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::Molinillo
|
||||
module Delegates
|
||||
# Delegates all {Bundler::Molinillo::SpecificationProvider} methods to a
|
||||
# `#specification_provider` property.
|
||||
module SpecificationProvider
|
||||
# (see Bundler::Molinillo::SpecificationProvider#search_for)
|
||||
def search_for(dependency)
|
||||
with_no_such_dependency_error_handling do
|
||||
specification_provider.search_for(dependency)
|
||||
end
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::SpecificationProvider#dependencies_for)
|
||||
def dependencies_for(specification)
|
||||
with_no_such_dependency_error_handling do
|
||||
specification_provider.dependencies_for(specification)
|
||||
end
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::SpecificationProvider#requirement_satisfied_by?)
|
||||
def requirement_satisfied_by?(requirement, activated, spec)
|
||||
with_no_such_dependency_error_handling do
|
||||
specification_provider.requirement_satisfied_by?(requirement, activated, spec)
|
||||
end
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::SpecificationProvider#dependencies_equal?)
|
||||
def dependencies_equal?(dependencies, other_dependencies)
|
||||
with_no_such_dependency_error_handling do
|
||||
specification_provider.dependencies_equal?(dependencies, other_dependencies)
|
||||
end
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::SpecificationProvider#name_for)
|
||||
def name_for(dependency)
|
||||
with_no_such_dependency_error_handling do
|
||||
specification_provider.name_for(dependency)
|
||||
end
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::SpecificationProvider#name_for_explicit_dependency_source)
|
||||
def name_for_explicit_dependency_source
|
||||
with_no_such_dependency_error_handling do
|
||||
specification_provider.name_for_explicit_dependency_source
|
||||
end
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::SpecificationProvider#name_for_locking_dependency_source)
|
||||
def name_for_locking_dependency_source
|
||||
with_no_such_dependency_error_handling do
|
||||
specification_provider.name_for_locking_dependency_source
|
||||
end
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::SpecificationProvider#sort_dependencies)
|
||||
def sort_dependencies(dependencies, activated, conflicts)
|
||||
with_no_such_dependency_error_handling do
|
||||
specification_provider.sort_dependencies(dependencies, activated, conflicts)
|
||||
end
|
||||
end
|
||||
|
||||
# (see Bundler::Molinillo::SpecificationProvider#allow_missing?)
|
||||
def allow_missing?(dependency)
|
||||
with_no_such_dependency_error_handling do
|
||||
specification_provider.allow_missing?(dependency)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Ensures any raised {NoSuchDependencyError} has its
|
||||
# {NoSuchDependencyError#required_by} set.
|
||||
# @yield
|
||||
def with_no_such_dependency_error_handling
|
||||
yield
|
||||
rescue NoSuchDependencyError => error
|
||||
if state
|
||||
vertex = activated.vertex_named(name_for(error.dependency))
|
||||
error.required_by += vertex.incoming_edges.map { |e| e.origin.name }
|
||||
error.required_by << name_for_explicit_dependency_source unless vertex.explicit_requirements.empty?
|
||||
end
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,255 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../../../vendored_tsort'
|
||||
|
||||
require_relative 'dependency_graph/log'
|
||||
require_relative 'dependency_graph/vertex'
|
||||
|
||||
module Bundler::Molinillo
|
||||
# A directed acyclic graph that is tuned to hold named dependencies
|
||||
class DependencyGraph
|
||||
include Enumerable
|
||||
|
||||
# Enumerates through the vertices of the graph.
|
||||
# @return [Array<Vertex>] The graph's vertices.
|
||||
def each
|
||||
return vertices.values.each unless block_given?
|
||||
vertices.values.each { |v| yield v }
|
||||
end
|
||||
|
||||
include Bundler::TSort
|
||||
|
||||
# @!visibility private
|
||||
alias tsort_each_node each
|
||||
|
||||
# @!visibility private
|
||||
def tsort_each_child(vertex, &block)
|
||||
vertex.successors.each(&block)
|
||||
end
|
||||
|
||||
# Topologically sorts the given vertices.
|
||||
# @param [Enumerable<Vertex>] vertices the vertices to be sorted, which must
|
||||
# all belong to the same graph.
|
||||
# @return [Array<Vertex>] The sorted vertices.
|
||||
def self.tsort(vertices)
|
||||
Bundler::TSort.tsort(
|
||||
lambda { |b| vertices.each(&b) },
|
||||
lambda { |v, &b| (v.successors & vertices).each(&b) }
|
||||
)
|
||||
end
|
||||
|
||||
# A directed edge of a {DependencyGraph}
|
||||
# @attr [Vertex] origin The origin of the directed edge
|
||||
# @attr [Vertex] destination The destination of the directed edge
|
||||
# @attr [Object] requirement The requirement the directed edge represents
|
||||
Edge = Struct.new(:origin, :destination, :requirement)
|
||||
|
||||
# @return [{String => Vertex}] the vertices of the dependency graph, keyed
|
||||
# by {Vertex#name}
|
||||
attr_reader :vertices
|
||||
|
||||
# @return [Log] the op log for this graph
|
||||
attr_reader :log
|
||||
|
||||
# Initializes an empty dependency graph
|
||||
def initialize
|
||||
@vertices = {}
|
||||
@log = Log.new
|
||||
end
|
||||
|
||||
# Tags the current state of the dependency as the given tag
|
||||
# @param [Object] tag an opaque tag for the current state of the graph
|
||||
# @return [Void]
|
||||
def tag(tag)
|
||||
log.tag(self, tag)
|
||||
end
|
||||
|
||||
# Rewinds the graph to the state tagged as `tag`
|
||||
# @param [Object] tag the tag to rewind to
|
||||
# @return [Void]
|
||||
def rewind_to(tag)
|
||||
log.rewind_to(self, tag)
|
||||
end
|
||||
|
||||
# Initializes a copy of a {DependencyGraph}, ensuring that all {#vertices}
|
||||
# are properly copied.
|
||||
# @param [DependencyGraph] other the graph to copy.
|
||||
def initialize_copy(other)
|
||||
super
|
||||
@vertices = {}
|
||||
@log = other.log.dup
|
||||
traverse = lambda do |new_v, old_v|
|
||||
return if new_v.outgoing_edges.size == old_v.outgoing_edges.size
|
||||
old_v.outgoing_edges.each do |edge|
|
||||
destination = add_vertex(edge.destination.name, edge.destination.payload)
|
||||
add_edge_no_circular(new_v, destination, edge.requirement)
|
||||
traverse.call(destination, edge.destination)
|
||||
end
|
||||
end
|
||||
other.vertices.each do |name, vertex|
|
||||
new_vertex = add_vertex(name, vertex.payload, vertex.root?)
|
||||
new_vertex.explicit_requirements.replace(vertex.explicit_requirements)
|
||||
traverse.call(new_vertex, vertex)
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String] a string suitable for debugging
|
||||
def inspect
|
||||
"#{self.class}:#{vertices.values.inspect}"
|
||||
end
|
||||
|
||||
# @param [Hash] options options for dot output.
|
||||
# @return [String] Returns a dot format representation of the graph
|
||||
def to_dot(options = {})
|
||||
edge_label = options.delete(:edge_label)
|
||||
raise ArgumentError, "Unknown options: #{options.keys}" unless options.empty?
|
||||
|
||||
dot_vertices = []
|
||||
dot_edges = []
|
||||
vertices.each do |n, v|
|
||||
dot_vertices << " #{n} [label=\"{#{n}|#{v.payload}}\"]"
|
||||
v.outgoing_edges.each do |e|
|
||||
label = edge_label ? edge_label.call(e) : e.requirement
|
||||
dot_edges << " #{e.origin.name} -> #{e.destination.name} [label=#{label.to_s.dump}]"
|
||||
end
|
||||
end
|
||||
|
||||
dot_vertices.uniq!
|
||||
dot_vertices.sort!
|
||||
dot_edges.uniq!
|
||||
dot_edges.sort!
|
||||
|
||||
dot = dot_vertices.unshift('digraph G {').push('') + dot_edges.push('}')
|
||||
dot.join("\n")
|
||||
end
|
||||
|
||||
# @param [DependencyGraph] other
|
||||
# @return [Boolean] whether the two dependency graphs are equal, determined
|
||||
# by a recursive traversal of each {#root_vertices} and its
|
||||
# {Vertex#successors}
|
||||
def ==(other)
|
||||
return false unless other
|
||||
return true if equal?(other)
|
||||
vertices.each do |name, vertex|
|
||||
other_vertex = other.vertex_named(name)
|
||||
return false unless other_vertex
|
||||
return false unless vertex.payload == other_vertex.payload
|
||||
return false unless other_vertex.successors.to_set == vertex.successors.to_set
|
||||
end
|
||||
end
|
||||
|
||||
# @param [String] name
|
||||
# @param [Object] payload
|
||||
# @param [Array<String>] parent_names
|
||||
# @param [Object] requirement the requirement that is requiring the child
|
||||
# @return [void]
|
||||
def add_child_vertex(name, payload, parent_names, requirement)
|
||||
root = !parent_names.delete(nil) { true }
|
||||
vertex = add_vertex(name, payload, root)
|
||||
vertex.explicit_requirements << requirement if root
|
||||
parent_names.each do |parent_name|
|
||||
parent_vertex = vertex_named(parent_name)
|
||||
add_edge(parent_vertex, vertex, requirement)
|
||||
end
|
||||
vertex
|
||||
end
|
||||
|
||||
# Adds a vertex with the given name, or updates the existing one.
|
||||
# @param [String] name
|
||||
# @param [Object] payload
|
||||
# @return [Vertex] the vertex that was added to `self`
|
||||
def add_vertex(name, payload, root = false)
|
||||
log.add_vertex(self, name, payload, root)
|
||||
end
|
||||
|
||||
# Detaches the {#vertex_named} `name` {Vertex} from the graph, recursively
|
||||
# removing any non-root vertices that were orphaned in the process
|
||||
# @param [String] name
|
||||
# @return [Array<Vertex>] the vertices which have been detached
|
||||
def detach_vertex_named(name)
|
||||
log.detach_vertex_named(self, name)
|
||||
end
|
||||
|
||||
# @param [String] name
|
||||
# @return [Vertex,nil] the vertex with the given name
|
||||
def vertex_named(name)
|
||||
vertices[name]
|
||||
end
|
||||
|
||||
# @param [String] name
|
||||
# @return [Vertex,nil] the root vertex with the given name
|
||||
def root_vertex_named(name)
|
||||
vertex = vertex_named(name)
|
||||
vertex if vertex && vertex.root?
|
||||
end
|
||||
|
||||
# Adds a new {Edge} to the dependency graph
|
||||
# @param [Vertex] origin
|
||||
# @param [Vertex] destination
|
||||
# @param [Object] requirement the requirement that this edge represents
|
||||
# @return [Edge] the added edge
|
||||
def add_edge(origin, destination, requirement)
|
||||
if destination.path_to?(origin)
|
||||
raise CircularDependencyError.new(path(destination, origin))
|
||||
end
|
||||
add_edge_no_circular(origin, destination, requirement)
|
||||
end
|
||||
|
||||
# Deletes an {Edge} from the dependency graph
|
||||
# @param [Edge] edge
|
||||
# @return [Void]
|
||||
def delete_edge(edge)
|
||||
log.delete_edge(self, edge.origin.name, edge.destination.name, edge.requirement)
|
||||
end
|
||||
|
||||
# Sets the payload of the vertex with the given name
|
||||
# @param [String] name the name of the vertex
|
||||
# @param [Object] payload the payload
|
||||
# @return [Void]
|
||||
def set_payload(name, payload)
|
||||
log.set_payload(self, name, payload)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Adds a new {Edge} to the dependency graph without checking for
|
||||
# circularity.
|
||||
# @param (see #add_edge)
|
||||
# @return (see #add_edge)
|
||||
def add_edge_no_circular(origin, destination, requirement)
|
||||
log.add_edge_no_circular(self, origin.name, destination.name, requirement)
|
||||
end
|
||||
|
||||
# Returns the path between two vertices
|
||||
# @raise [ArgumentError] if there is no path between the vertices
|
||||
# @param [Vertex] from
|
||||
# @param [Vertex] to
|
||||
# @return [Array<Vertex>] the shortest path from `from` to `to`
|
||||
def path(from, to)
|
||||
distances = Hash.new(vertices.size + 1)
|
||||
distances[from.name] = 0
|
||||
predecessors = {}
|
||||
each do |vertex|
|
||||
vertex.successors.each do |successor|
|
||||
if distances[successor.name] > distances[vertex.name] + 1
|
||||
distances[successor.name] = distances[vertex.name] + 1
|
||||
predecessors[successor] = vertex
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
path = [to]
|
||||
while before = predecessors[to]
|
||||
path << before
|
||||
to = before
|
||||
break if to == from
|
||||
end
|
||||
|
||||
unless path.last.equal?(from)
|
||||
raise ArgumentError, "There is no path from #{from.name} to #{to.name}"
|
||||
end
|
||||
|
||||
path.reverse
|
||||
end
|
||||
end
|
||||
end
|
@ -1,36 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::Molinillo
|
||||
class DependencyGraph
|
||||
# An action that modifies a {DependencyGraph} that is reversible.
|
||||
# @abstract
|
||||
class Action
|
||||
# rubocop:disable Lint/UnusedMethodArgument
|
||||
|
||||
# @return [Symbol] The name of the action.
|
||||
def self.action_name
|
||||
raise 'Abstract'
|
||||
end
|
||||
|
||||
# Performs the action on the given graph.
|
||||
# @param [DependencyGraph] graph the graph to perform the action on.
|
||||
# @return [Void]
|
||||
def up(graph)
|
||||
raise 'Abstract'
|
||||
end
|
||||
|
||||
# Reverses the action on the given graph.
|
||||
# @param [DependencyGraph] graph the graph to reverse the action on.
|
||||
# @return [Void]
|
||||
def down(graph)
|
||||
raise 'Abstract'
|
||||
end
|
||||
|
||||
# @return [Action,Nil] The previous action
|
||||
attr_accessor :previous
|
||||
|
||||
# @return [Action,Nil] The next action
|
||||
attr_accessor :next
|
||||
end
|
||||
end
|
||||
end
|
@ -1,66 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'action'
|
||||
module Bundler::Molinillo
|
||||
class DependencyGraph
|
||||
# @!visibility private
|
||||
# (see DependencyGraph#add_edge_no_circular)
|
||||
class AddEdgeNoCircular < Action
|
||||
# @!group Action
|
||||
|
||||
# (see Action.action_name)
|
||||
def self.action_name
|
||||
:add_vertex
|
||||
end
|
||||
|
||||
# (see Action#up)
|
||||
def up(graph)
|
||||
edge = make_edge(graph)
|
||||
edge.origin.outgoing_edges << edge
|
||||
edge.destination.incoming_edges << edge
|
||||
edge
|
||||
end
|
||||
|
||||
# (see Action#down)
|
||||
def down(graph)
|
||||
edge = make_edge(graph)
|
||||
delete_first(edge.origin.outgoing_edges, edge)
|
||||
delete_first(edge.destination.incoming_edges, edge)
|
||||
end
|
||||
|
||||
# @!group AddEdgeNoCircular
|
||||
|
||||
# @return [String] the name of the origin of the edge
|
||||
attr_reader :origin
|
||||
|
||||
# @return [String] the name of the destination of the edge
|
||||
attr_reader :destination
|
||||
|
||||
# @return [Object] the requirement that the edge represents
|
||||
attr_reader :requirement
|
||||
|
||||
# @param [DependencyGraph] graph the graph to find vertices from
|
||||
# @return [Edge] The edge this action adds
|
||||
def make_edge(graph)
|
||||
Edge.new(graph.vertex_named(origin), graph.vertex_named(destination), requirement)
|
||||
end
|
||||
|
||||
# Initialize an action to add an edge to a dependency graph
|
||||
# @param [String] origin the name of the origin of the edge
|
||||
# @param [String] destination the name of the destination of the edge
|
||||
# @param [Object] requirement the requirement that the edge represents
|
||||
def initialize(origin, destination, requirement)
|
||||
@origin = origin
|
||||
@destination = destination
|
||||
@requirement = requirement
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_first(array, item)
|
||||
return unless index = array.index(item)
|
||||
array.delete_at(index)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,62 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'action'
|
||||
module Bundler::Molinillo
|
||||
class DependencyGraph
|
||||
# @!visibility private
|
||||
# (see DependencyGraph#add_vertex)
|
||||
class AddVertex < Action # :nodoc:
|
||||
# @!group Action
|
||||
|
||||
# (see Action.action_name)
|
||||
def self.action_name
|
||||
:add_vertex
|
||||
end
|
||||
|
||||
# (see Action#up)
|
||||
def up(graph)
|
||||
if existing = graph.vertices[name]
|
||||
@existing_payload = existing.payload
|
||||
@existing_root = existing.root
|
||||
end
|
||||
vertex = existing || Vertex.new(name, payload)
|
||||
graph.vertices[vertex.name] = vertex
|
||||
vertex.payload ||= payload
|
||||
vertex.root ||= root
|
||||
vertex
|
||||
end
|
||||
|
||||
# (see Action#down)
|
||||
def down(graph)
|
||||
if defined?(@existing_payload)
|
||||
vertex = graph.vertices[name]
|
||||
vertex.payload = @existing_payload
|
||||
vertex.root = @existing_root
|
||||
else
|
||||
graph.vertices.delete(name)
|
||||
end
|
||||
end
|
||||
|
||||
# @!group AddVertex
|
||||
|
||||
# @return [String] the name of the vertex
|
||||
attr_reader :name
|
||||
|
||||
# @return [Object] the payload for the vertex
|
||||
attr_reader :payload
|
||||
|
||||
# @return [Boolean] whether the vertex is root or not
|
||||
attr_reader :root
|
||||
|
||||
# Initialize an action to add a vertex to a dependency graph
|
||||
# @param [String] name the name of the vertex
|
||||
# @param [Object] payload the payload for the vertex
|
||||
# @param [Boolean] root whether the vertex is root or not
|
||||
def initialize(name, payload, root)
|
||||
@name = name
|
||||
@payload = payload
|
||||
@root = root
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,63 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'action'
|
||||
module Bundler::Molinillo
|
||||
class DependencyGraph
|
||||
# @!visibility private
|
||||
# (see DependencyGraph#delete_edge)
|
||||
class DeleteEdge < Action
|
||||
# @!group Action
|
||||
|
||||
# (see Action.action_name)
|
||||
def self.action_name
|
||||
:delete_edge
|
||||
end
|
||||
|
||||
# (see Action#up)
|
||||
def up(graph)
|
||||
edge = make_edge(graph)
|
||||
edge.origin.outgoing_edges.delete(edge)
|
||||
edge.destination.incoming_edges.delete(edge)
|
||||
end
|
||||
|
||||
# (see Action#down)
|
||||
def down(graph)
|
||||
edge = make_edge(graph)
|
||||
edge.origin.outgoing_edges << edge
|
||||
edge.destination.incoming_edges << edge
|
||||
edge
|
||||
end
|
||||
|
||||
# @!group DeleteEdge
|
||||
|
||||
# @return [String] the name of the origin of the edge
|
||||
attr_reader :origin_name
|
||||
|
||||
# @return [String] the name of the destination of the edge
|
||||
attr_reader :destination_name
|
||||
|
||||
# @return [Object] the requirement that the edge represents
|
||||
attr_reader :requirement
|
||||
|
||||
# @param [DependencyGraph] graph the graph to find vertices from
|
||||
# @return [Edge] The edge this action adds
|
||||
def make_edge(graph)
|
||||
Edge.new(
|
||||
graph.vertex_named(origin_name),
|
||||
graph.vertex_named(destination_name),
|
||||
requirement
|
||||
)
|
||||
end
|
||||
|
||||
# Initialize an action to add an edge to a dependency graph
|
||||
# @param [String] origin_name the name of the origin of the edge
|
||||
# @param [String] destination_name the name of the destination of the edge
|
||||
# @param [Object] requirement the requirement that the edge represents
|
||||
def initialize(origin_name, destination_name, requirement)
|
||||
@origin_name = origin_name
|
||||
@destination_name = destination_name
|
||||
@requirement = requirement
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,61 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'action'
|
||||
module Bundler::Molinillo
|
||||
class DependencyGraph
|
||||
# @!visibility private
|
||||
# @see DependencyGraph#detach_vertex_named
|
||||
class DetachVertexNamed < Action
|
||||
# @!group Action
|
||||
|
||||
# (see Action#name)
|
||||
def self.action_name
|
||||
:add_vertex
|
||||
end
|
||||
|
||||
# (see Action#up)
|
||||
def up(graph)
|
||||
return [] unless @vertex = graph.vertices.delete(name)
|
||||
|
||||
removed_vertices = [@vertex]
|
||||
@vertex.outgoing_edges.each do |e|
|
||||
v = e.destination
|
||||
v.incoming_edges.delete(e)
|
||||
if !v.root? && v.incoming_edges.empty?
|
||||
removed_vertices.concat graph.detach_vertex_named(v.name)
|
||||
end
|
||||
end
|
||||
|
||||
@vertex.incoming_edges.each do |e|
|
||||
v = e.origin
|
||||
v.outgoing_edges.delete(e)
|
||||
end
|
||||
|
||||
removed_vertices
|
||||
end
|
||||
|
||||
# (see Action#down)
|
||||
def down(graph)
|
||||
return unless @vertex
|
||||
graph.vertices[@vertex.name] = @vertex
|
||||
@vertex.outgoing_edges.each do |e|
|
||||
e.destination.incoming_edges << e
|
||||
end
|
||||
@vertex.incoming_edges.each do |e|
|
||||
e.origin.outgoing_edges << e
|
||||
end
|
||||
end
|
||||
|
||||
# @!group DetachVertexNamed
|
||||
|
||||
# @return [String] the name of the vertex to detach
|
||||
attr_reader :name
|
||||
|
||||
# Initialize an action to detach a vertex from a dependency graph
|
||||
# @param [String] name the name of the vertex to detach
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,126 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'add_edge_no_circular'
|
||||
require_relative 'add_vertex'
|
||||
require_relative 'delete_edge'
|
||||
require_relative 'detach_vertex_named'
|
||||
require_relative 'set_payload'
|
||||
require_relative 'tag'
|
||||
|
||||
module Bundler::Molinillo
|
||||
class DependencyGraph
|
||||
# A log for dependency graph actions
|
||||
class Log
|
||||
# Initializes an empty log
|
||||
def initialize
|
||||
@current_action = @first_action = nil
|
||||
end
|
||||
|
||||
# @!macro [new] action
|
||||
# {include:DependencyGraph#$0}
|
||||
# @param [Graph] graph the graph to perform the action on
|
||||
# @param (see DependencyGraph#$0)
|
||||
# @return (see DependencyGraph#$0)
|
||||
|
||||
# @macro action
|
||||
def tag(graph, tag)
|
||||
push_action(graph, Tag.new(tag))
|
||||
end
|
||||
|
||||
# @macro action
|
||||
def add_vertex(graph, name, payload, root)
|
||||
push_action(graph, AddVertex.new(name, payload, root))
|
||||
end
|
||||
|
||||
# @macro action
|
||||
def detach_vertex_named(graph, name)
|
||||
push_action(graph, DetachVertexNamed.new(name))
|
||||
end
|
||||
|
||||
# @macro action
|
||||
def add_edge_no_circular(graph, origin, destination, requirement)
|
||||
push_action(graph, AddEdgeNoCircular.new(origin, destination, requirement))
|
||||
end
|
||||
|
||||
# {include:DependencyGraph#delete_edge}
|
||||
# @param [Graph] graph the graph to perform the action on
|
||||
# @param [String] origin_name
|
||||
# @param [String] destination_name
|
||||
# @param [Object] requirement
|
||||
# @return (see DependencyGraph#delete_edge)
|
||||
def delete_edge(graph, origin_name, destination_name, requirement)
|
||||
push_action(graph, DeleteEdge.new(origin_name, destination_name, requirement))
|
||||
end
|
||||
|
||||
# @macro action
|
||||
def set_payload(graph, name, payload)
|
||||
push_action(graph, SetPayload.new(name, payload))
|
||||
end
|
||||
|
||||
# Pops the most recent action from the log and undoes the action
|
||||
# @param [DependencyGraph] graph
|
||||
# @return [Action] the action that was popped off the log
|
||||
def pop!(graph)
|
||||
return unless action = @current_action
|
||||
unless @current_action = action.previous
|
||||
@first_action = nil
|
||||
end
|
||||
action.down(graph)
|
||||
action
|
||||
end
|
||||
|
||||
extend Enumerable
|
||||
|
||||
# @!visibility private
|
||||
# Enumerates each action in the log
|
||||
# @yield [Action]
|
||||
def each
|
||||
return enum_for unless block_given?
|
||||
action = @first_action
|
||||
loop do
|
||||
break unless action
|
||||
yield action
|
||||
action = action.next
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# @!visibility private
|
||||
# Enumerates each action in the log in reverse order
|
||||
# @yield [Action]
|
||||
def reverse_each
|
||||
return enum_for(:reverse_each) unless block_given?
|
||||
action = @current_action
|
||||
loop do
|
||||
break unless action
|
||||
yield action
|
||||
action = action.previous
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# @macro action
|
||||
def rewind_to(graph, tag)
|
||||
loop do
|
||||
action = pop!(graph)
|
||||
raise "No tag #{tag.inspect} found" unless action
|
||||
break if action.class.action_name == :tag && action.tag == tag
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Adds the given action to the log, running the action
|
||||
# @param [DependencyGraph] graph
|
||||
# @param [Action] action
|
||||
# @return The value returned by `action.up`
|
||||
def push_action(graph, action)
|
||||
action.previous = @current_action
|
||||
@current_action.next = action if @current_action
|
||||
@current_action = action
|
||||
@first_action ||= action
|
||||
action.up(graph)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,46 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'action'
|
||||
module Bundler::Molinillo
|
||||
class DependencyGraph
|
||||
# @!visibility private
|
||||
# @see DependencyGraph#set_payload
|
||||
class SetPayload < Action # :nodoc:
|
||||
# @!group Action
|
||||
|
||||
# (see Action.action_name)
|
||||
def self.action_name
|
||||
:set_payload
|
||||
end
|
||||
|
||||
# (see Action#up)
|
||||
def up(graph)
|
||||
vertex = graph.vertex_named(name)
|
||||
@old_payload = vertex.payload
|
||||
vertex.payload = payload
|
||||
end
|
||||
|
||||
# (see Action#down)
|
||||
def down(graph)
|
||||
graph.vertex_named(name).payload = @old_payload
|
||||
end
|
||||
|
||||
# @!group SetPayload
|
||||
|
||||
# @return [String] the name of the vertex
|
||||
attr_reader :name
|
||||
|
||||
# @return [Object] the payload for the vertex
|
||||
attr_reader :payload
|
||||
|
||||
# Initialize an action to add set the payload for a vertex in a dependency
|
||||
# graph
|
||||
# @param [String] name the name of the vertex
|
||||
# @param [Object] payload the payload for the vertex
|
||||
def initialize(name, payload)
|
||||
@name = name
|
||||
@payload = payload
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,36 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'action'
|
||||
module Bundler::Molinillo
|
||||
class DependencyGraph
|
||||
# @!visibility private
|
||||
# @see DependencyGraph#tag
|
||||
class Tag < Action
|
||||
# @!group Action
|
||||
|
||||
# (see Action.action_name)
|
||||
def self.action_name
|
||||
:tag
|
||||
end
|
||||
|
||||
# (see Action#up)
|
||||
def up(graph)
|
||||
end
|
||||
|
||||
# (see Action#down)
|
||||
def down(graph)
|
||||
end
|
||||
|
||||
# @!group Tag
|
||||
|
||||
# @return [Object] An opaque tag
|
||||
attr_reader :tag
|
||||
|
||||
# Initialize an action to tag a state of a dependency graph
|
||||
# @param [Object] tag an opaque tag
|
||||
def initialize(tag)
|
||||
@tag = tag
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,164 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::Molinillo
|
||||
class DependencyGraph
|
||||
# A vertex in a {DependencyGraph} that encapsulates a {#name} and a
|
||||
# {#payload}
|
||||
class Vertex
|
||||
# @return [String] the name of the vertex
|
||||
attr_accessor :name
|
||||
|
||||
# @return [Object] the payload the vertex holds
|
||||
attr_accessor :payload
|
||||
|
||||
# @return [Array<Object>] the explicit requirements that required
|
||||
# this vertex
|
||||
attr_reader :explicit_requirements
|
||||
|
||||
# @return [Boolean] whether the vertex is considered a root vertex
|
||||
attr_accessor :root
|
||||
alias root? root
|
||||
|
||||
# Initializes a vertex with the given name and payload.
|
||||
# @param [String] name see {#name}
|
||||
# @param [Object] payload see {#payload}
|
||||
def initialize(name, payload)
|
||||
@name = name.frozen? ? name : name.dup.freeze
|
||||
@payload = payload
|
||||
@explicit_requirements = []
|
||||
@outgoing_edges = []
|
||||
@incoming_edges = []
|
||||
end
|
||||
|
||||
# @return [Array<Object>] all of the requirements that required
|
||||
# this vertex
|
||||
def requirements
|
||||
(incoming_edges.map(&:requirement) + explicit_requirements).uniq
|
||||
end
|
||||
|
||||
# @return [Array<Edge>] the edges of {#graph} that have `self` as their
|
||||
# {Edge#origin}
|
||||
attr_accessor :outgoing_edges
|
||||
|
||||
# @return [Array<Edge>] the edges of {#graph} that have `self` as their
|
||||
# {Edge#destination}
|
||||
attr_accessor :incoming_edges
|
||||
|
||||
# @return [Array<Vertex>] the vertices of {#graph} that have an edge with
|
||||
# `self` as their {Edge#destination}
|
||||
def predecessors
|
||||
incoming_edges.map(&:origin)
|
||||
end
|
||||
|
||||
# @return [Set<Vertex>] the vertices of {#graph} where `self` is a
|
||||
# {#descendent?}
|
||||
def recursive_predecessors
|
||||
_recursive_predecessors
|
||||
end
|
||||
|
||||
# @param [Set<Vertex>] vertices the set to add the predecessors to
|
||||
# @return [Set<Vertex>] the vertices of {#graph} where `self` is a
|
||||
# {#descendent?}
|
||||
def _recursive_predecessors(vertices = new_vertex_set)
|
||||
incoming_edges.each do |edge|
|
||||
vertex = edge.origin
|
||||
next unless vertices.add?(vertex)
|
||||
vertex._recursive_predecessors(vertices)
|
||||
end
|
||||
|
||||
vertices
|
||||
end
|
||||
protected :_recursive_predecessors
|
||||
|
||||
# @return [Array<Vertex>] the vertices of {#graph} that have an edge with
|
||||
# `self` as their {Edge#origin}
|
||||
def successors
|
||||
outgoing_edges.map(&:destination)
|
||||
end
|
||||
|
||||
# @return [Set<Vertex>] the vertices of {#graph} where `self` is an
|
||||
# {#ancestor?}
|
||||
def recursive_successors
|
||||
_recursive_successors
|
||||
end
|
||||
|
||||
# @param [Set<Vertex>] vertices the set to add the successors to
|
||||
# @return [Set<Vertex>] the vertices of {#graph} where `self` is an
|
||||
# {#ancestor?}
|
||||
def _recursive_successors(vertices = new_vertex_set)
|
||||
outgoing_edges.each do |edge|
|
||||
vertex = edge.destination
|
||||
next unless vertices.add?(vertex)
|
||||
vertex._recursive_successors(vertices)
|
||||
end
|
||||
|
||||
vertices
|
||||
end
|
||||
protected :_recursive_successors
|
||||
|
||||
# @return [String] a string suitable for debugging
|
||||
def inspect
|
||||
"#{self.class}:#{name}(#{payload.inspect})"
|
||||
end
|
||||
|
||||
# @return [Boolean] whether the two vertices are equal, determined
|
||||
# by a recursive traversal of each {Vertex#successors}
|
||||
def ==(other)
|
||||
return true if equal?(other)
|
||||
shallow_eql?(other) &&
|
||||
successors.to_set == other.successors.to_set
|
||||
end
|
||||
|
||||
# @param [Vertex] other the other vertex to compare to
|
||||
# @return [Boolean] whether the two vertices are equal, determined
|
||||
# solely by {#name} and {#payload} equality
|
||||
def shallow_eql?(other)
|
||||
return true if equal?(other)
|
||||
other &&
|
||||
name == other.name &&
|
||||
payload == other.payload
|
||||
end
|
||||
|
||||
alias eql? ==
|
||||
|
||||
# @return [Fixnum] a hash for the vertex based upon its {#name}
|
||||
def hash
|
||||
name.hash
|
||||
end
|
||||
|
||||
# Is there a path from `self` to `other` following edges in the
|
||||
# dependency graph?
|
||||
# @return whether there is a path following edges within this {#graph}
|
||||
def path_to?(other)
|
||||
_path_to?(other)
|
||||
end
|
||||
|
||||
alias descendent? path_to?
|
||||
|
||||
# @param [Vertex] other the vertex to check if there's a path to
|
||||
# @param [Set<Vertex>] visited the vertices of {#graph} that have been visited
|
||||
# @return [Boolean] whether there is a path to `other` from `self`
|
||||
def _path_to?(other, visited = new_vertex_set)
|
||||
return false unless visited.add?(self)
|
||||
return true if equal?(other)
|
||||
successors.any? { |v| v._path_to?(other, visited) }
|
||||
end
|
||||
protected :_path_to?
|
||||
|
||||
# Is there a path from `other` to `self` following edges in the
|
||||
# dependency graph?
|
||||
# @return whether there is a path following edges within this {#graph}
|
||||
def ancestor?(other)
|
||||
other.path_to?(self)
|
||||
end
|
||||
|
||||
alias is_reachable_from? ancestor?
|
||||
|
||||
def new_vertex_set
|
||||
require 'set'
|
||||
Set.new
|
||||
end
|
||||
private :new_vertex_set
|
||||
end
|
||||
end
|
||||
end
|
149
lib/bundler/vendor/molinillo/lib/molinillo/errors.rb
vendored
149
lib/bundler/vendor/molinillo/lib/molinillo/errors.rb
vendored
@ -1,149 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::Molinillo
|
||||
# An error that occurred during the resolution process
|
||||
class ResolverError < StandardError; end
|
||||
|
||||
# An error caused by searching for a dependency that is completely unknown,
|
||||
# i.e. has no versions available whatsoever.
|
||||
class NoSuchDependencyError < ResolverError
|
||||
# @return [Object] the dependency that could not be found
|
||||
attr_accessor :dependency
|
||||
|
||||
# @return [Array<Object>] the specifications that depended upon {#dependency}
|
||||
attr_accessor :required_by
|
||||
|
||||
# Initializes a new error with the given missing dependency.
|
||||
# @param [Object] dependency @see {#dependency}
|
||||
# @param [Array<Object>] required_by @see {#required_by}
|
||||
def initialize(dependency, required_by = [])
|
||||
@dependency = dependency
|
||||
@required_by = required_by.uniq
|
||||
super()
|
||||
end
|
||||
|
||||
# The error message for the missing dependency, including the specifications
|
||||
# that had this dependency.
|
||||
def message
|
||||
sources = required_by.map { |r| "`#{r}`" }.join(' and ')
|
||||
message = "Unable to find a specification for `#{dependency}`"
|
||||
message += " depended upon by #{sources}" unless sources.empty?
|
||||
message
|
||||
end
|
||||
end
|
||||
|
||||
# An error caused by attempting to fulfil a dependency that was circular
|
||||
#
|
||||
# @note This exception will be thrown if and only if a {Vertex} is added to a
|
||||
# {DependencyGraph} that has a {DependencyGraph::Vertex#path_to?} an
|
||||
# existing {DependencyGraph::Vertex}
|
||||
class CircularDependencyError < ResolverError
|
||||
# [Set<Object>] the dependencies responsible for causing the error
|
||||
attr_reader :dependencies
|
||||
|
||||
# Initializes a new error with the given circular vertices.
|
||||
# @param [Array<DependencyGraph::Vertex>] vertices the vertices in the dependency
|
||||
# that caused the error
|
||||
def initialize(vertices)
|
||||
super "There is a circular dependency between #{vertices.map(&:name).join(' and ')}"
|
||||
@dependencies = vertices.map { |vertex| vertex.payload.possibilities.last }.to_set
|
||||
end
|
||||
end
|
||||
|
||||
# An error caused by conflicts in version
|
||||
class VersionConflict < ResolverError
|
||||
# @return [{String => Resolution::Conflict}] the conflicts that caused
|
||||
# resolution to fail
|
||||
attr_reader :conflicts
|
||||
|
||||
# @return [SpecificationProvider] the specification provider used during
|
||||
# resolution
|
||||
attr_reader :specification_provider
|
||||
|
||||
# Initializes a new error with the given version conflicts.
|
||||
# @param [{String => Resolution::Conflict}] conflicts see {#conflicts}
|
||||
# @param [SpecificationProvider] specification_provider see {#specification_provider}
|
||||
def initialize(conflicts, specification_provider)
|
||||
pairs = []
|
||||
conflicts.values.flat_map(&:requirements).each do |conflicting|
|
||||
conflicting.each do |source, conflict_requirements|
|
||||
conflict_requirements.each do |c|
|
||||
pairs << [c, source]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
super "Unable to satisfy the following requirements:\n\n" \
|
||||
"#{pairs.map { |r, d| "- `#{r}` required by `#{d}`" }.join("\n")}"
|
||||
|
||||
@conflicts = conflicts
|
||||
@specification_provider = specification_provider
|
||||
end
|
||||
|
||||
require_relative 'delegates/specification_provider'
|
||||
include Delegates::SpecificationProvider
|
||||
|
||||
# @return [String] An error message that includes requirement trees,
|
||||
# which is much more detailed & customizable than the default message
|
||||
# @param [Hash] opts the options to create a message with.
|
||||
# @option opts [String] :solver_name The user-facing name of the solver
|
||||
# @option opts [String] :possibility_type The generic name of a possibility
|
||||
# @option opts [Proc] :reduce_trees A proc that reduced the list of requirement trees
|
||||
# @option opts [Proc] :printable_requirement A proc that pretty-prints requirements
|
||||
# @option opts [Proc] :additional_message_for_conflict A proc that appends additional
|
||||
# messages for each conflict
|
||||
# @option opts [Proc] :version_for_spec A proc that returns the version number for a
|
||||
# possibility
|
||||
def message_with_trees(opts = {})
|
||||
solver_name = opts.delete(:solver_name) { self.class.name.split('::').first }
|
||||
possibility_type = opts.delete(:possibility_type) { 'possibility named' }
|
||||
reduce_trees = opts.delete(:reduce_trees) { proc { |trees| trees.uniq.sort_by(&:to_s) } }
|
||||
printable_requirement = opts.delete(:printable_requirement) { proc { |req| req.to_s } }
|
||||
additional_message_for_conflict = opts.delete(:additional_message_for_conflict) { proc {} }
|
||||
version_for_spec = opts.delete(:version_for_spec) { proc(&:to_s) }
|
||||
incompatible_version_message_for_conflict = opts.delete(:incompatible_version_message_for_conflict) do
|
||||
proc do |name, _conflict|
|
||||
%(#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":)
|
||||
end
|
||||
end
|
||||
|
||||
full_message_for_conflict = opts.delete(:full_message_for_conflict) do
|
||||
proc do |name, conflict|
|
||||
o = "\n".dup << incompatible_version_message_for_conflict.call(name, conflict) << "\n"
|
||||
if conflict.locked_requirement
|
||||
o << %( In snapshot (#{name_for_locking_dependency_source}):\n)
|
||||
o << %( #{printable_requirement.call(conflict.locked_requirement)}\n)
|
||||
o << %(\n)
|
||||
end
|
||||
o << %( In #{name_for_explicit_dependency_source}:\n)
|
||||
trees = reduce_trees.call(conflict.requirement_trees)
|
||||
|
||||
o << trees.map do |tree|
|
||||
t = ''.dup
|
||||
depth = 2
|
||||
tree.each do |req|
|
||||
t << ' ' * depth << printable_requirement.call(req)
|
||||
unless tree.last == req
|
||||
if spec = conflict.activated_by_name[name_for(req)]
|
||||
t << %( was resolved to #{version_for_spec.call(spec)}, which)
|
||||
end
|
||||
t << %( depends on)
|
||||
end
|
||||
t << %(\n)
|
||||
depth += 1
|
||||
end
|
||||
t
|
||||
end.join("\n")
|
||||
|
||||
additional_message_for_conflict.call(o, name, conflict)
|
||||
|
||||
o
|
||||
end
|
||||
end
|
||||
|
||||
conflicts.sort.reduce(''.dup) do |o, (name, conflict)|
|
||||
o << full_message_for_conflict.call(name, conflict)
|
||||
end.strip
|
||||
end
|
||||
end
|
||||
end
|
@ -1,6 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::Molinillo
|
||||
# The version of Bundler::Molinillo.
|
||||
VERSION = '0.8.0'.freeze
|
||||
end
|
@ -1,112 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::Molinillo
|
||||
# Provides information about specifications and dependencies to the resolver,
|
||||
# allowing the {Resolver} class to remain generic while still providing power
|
||||
# and flexibility.
|
||||
#
|
||||
# This module contains the methods that users of Bundler::Molinillo must to implement,
|
||||
# using knowledge of their own model classes.
|
||||
module SpecificationProvider
|
||||
# Search for the specifications that match the given dependency.
|
||||
# The specifications in the returned array will be considered in reverse
|
||||
# order, so the latest version ought to be last.
|
||||
# @note This method should be 'pure', i.e. the return value should depend
|
||||
# only on the `dependency` parameter.
|
||||
#
|
||||
# @param [Object] dependency
|
||||
# @return [Array<Object>] the specifications that satisfy the given
|
||||
# `dependency`.
|
||||
def search_for(dependency)
|
||||
[]
|
||||
end
|
||||
|
||||
# Returns the dependencies of `specification`.
|
||||
# @note This method should be 'pure', i.e. the return value should depend
|
||||
# only on the `specification` parameter.
|
||||
#
|
||||
# @param [Object] specification
|
||||
# @return [Array<Object>] the dependencies that are required by the given
|
||||
# `specification`.
|
||||
def dependencies_for(specification)
|
||||
[]
|
||||
end
|
||||
|
||||
# Determines whether the given `requirement` is satisfied by the given
|
||||
# `spec`, in the context of the current `activated` dependency graph.
|
||||
#
|
||||
# @param [Object] requirement
|
||||
# @param [DependencyGraph] activated the current dependency graph in the
|
||||
# resolution process.
|
||||
# @param [Object] spec
|
||||
# @return [Boolean] whether `requirement` is satisfied by `spec` in the
|
||||
# context of the current `activated` dependency graph.
|
||||
def requirement_satisfied_by?(requirement, activated, spec)
|
||||
true
|
||||
end
|
||||
|
||||
# Determines whether two arrays of dependencies are equal, and thus can be
|
||||
# grouped.
|
||||
#
|
||||
# @param [Array<Object>] dependencies
|
||||
# @param [Array<Object>] other_dependencies
|
||||
# @return [Boolean] whether `dependencies` and `other_dependencies` should
|
||||
# be considered equal.
|
||||
def dependencies_equal?(dependencies, other_dependencies)
|
||||
dependencies == other_dependencies
|
||||
end
|
||||
|
||||
# Returns the name for the given `dependency`.
|
||||
# @note This method should be 'pure', i.e. the return value should depend
|
||||
# only on the `dependency` parameter.
|
||||
#
|
||||
# @param [Object] dependency
|
||||
# @return [String] the name for the given `dependency`.
|
||||
def name_for(dependency)
|
||||
dependency.to_s
|
||||
end
|
||||
|
||||
# @return [String] the name of the source of explicit dependencies, i.e.
|
||||
# those passed to {Resolver#resolve} directly.
|
||||
def name_for_explicit_dependency_source
|
||||
'user-specified dependency'
|
||||
end
|
||||
|
||||
# @return [String] the name of the source of 'locked' dependencies, i.e.
|
||||
# those passed to {Resolver#resolve} directly as the `base`
|
||||
def name_for_locking_dependency_source
|
||||
'Lockfile'
|
||||
end
|
||||
|
||||
# Sort dependencies so that the ones that are easiest to resolve are first.
|
||||
# Easiest to resolve is (usually) defined by:
|
||||
# 1) Is this dependency already activated?
|
||||
# 2) How relaxed are the requirements?
|
||||
# 3) Are there any conflicts for this dependency?
|
||||
# 4) How many possibilities are there to satisfy this dependency?
|
||||
#
|
||||
# @param [Array<Object>] dependencies
|
||||
# @param [DependencyGraph] activated the current dependency graph in the
|
||||
# resolution process.
|
||||
# @param [{String => Array<Conflict>}] conflicts
|
||||
# @return [Array<Object>] a sorted copy of `dependencies`.
|
||||
def sort_dependencies(dependencies, activated, conflicts)
|
||||
dependencies.sort_by do |dependency|
|
||||
name = name_for(dependency)
|
||||
[
|
||||
activated.vertex_named(name).payload ? 0 : 1,
|
||||
conflicts[name] ? 0 : 1,
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
# Returns whether this dependency, which has no possible matching
|
||||
# specifications, can safely be ignored.
|
||||
#
|
||||
# @param [Object] dependency
|
||||
# @return [Boolean] whether this dependency can safely be skipped.
|
||||
def allow_missing?(dependency)
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
@ -1,67 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::Molinillo
|
||||
# Conveys information about the resolution process to a user.
|
||||
module UI
|
||||
# The {IO} object that should be used to print output. `STDOUT`, by default.
|
||||
#
|
||||
# @return [IO]
|
||||
def output
|
||||
STDOUT
|
||||
end
|
||||
|
||||
# Called roughly every {#progress_rate}, this method should convey progress
|
||||
# to the user.
|
||||
#
|
||||
# @return [void]
|
||||
def indicate_progress
|
||||
output.print '.' unless debug?
|
||||
end
|
||||
|
||||
# How often progress should be conveyed to the user via
|
||||
# {#indicate_progress}, in seconds. A third of a second, by default.
|
||||
#
|
||||
# @return [Float]
|
||||
def progress_rate
|
||||
0.33
|
||||
end
|
||||
|
||||
# Called before resolution begins.
|
||||
#
|
||||
# @return [void]
|
||||
def before_resolution
|
||||
output.print 'Resolving dependencies...'
|
||||
end
|
||||
|
||||
# Called after resolution ends (either successfully or with an error).
|
||||
# By default, prints a newline.
|
||||
#
|
||||
# @return [void]
|
||||
def after_resolution
|
||||
output.puts
|
||||
end
|
||||
|
||||
# Conveys debug information to the user.
|
||||
#
|
||||
# @param [Integer] depth the current depth of the resolution process.
|
||||
# @return [void]
|
||||
def debug(depth = 0)
|
||||
if debug?
|
||||
debug_info = yield
|
||||
debug_info = debug_info.inspect unless debug_info.is_a?(String)
|
||||
debug_info = debug_info.split("\n").map { |s| ":#{depth.to_s.rjust 4}: #{s}" }
|
||||
output.puts debug_info
|
||||
end
|
||||
end
|
||||
|
||||
# Whether or not debug messages should be printed.
|
||||
# By default, whether or not the `MOLINILLO_DEBUG` environment variable is
|
||||
# set.
|
||||
#
|
||||
# @return [Boolean]
|
||||
def debug?
|
||||
return @debug_mode if defined?(@debug_mode)
|
||||
@debug_mode = ENV['MOLINILLO_DEBUG']
|
||||
end
|
||||
end
|
||||
end
|
@ -1,839 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::Molinillo
|
||||
class Resolver
|
||||
# A specific resolution from a given {Resolver}
|
||||
class Resolution
|
||||
# A conflict that the resolution process encountered
|
||||
# @attr [Object] requirement the requirement that immediately led to the conflict
|
||||
# @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict
|
||||
# @attr [Object, nil] existing the existing spec that was in conflict with
|
||||
# the {#possibility}
|
||||
# @attr [Object] possibility_set the set of specs that was unable to be
|
||||
# activated due to a conflict.
|
||||
# @attr [Object] locked_requirement the relevant locking requirement.
|
||||
# @attr [Array<Array<Object>>] requirement_trees the different requirement
|
||||
# trees that led to every requirement for the conflicting name.
|
||||
# @attr [{String=>Object}] activated_by_name the already-activated specs.
|
||||
# @attr [Object] underlying_error an error that has occurred during resolution, and
|
||||
# will be raised at the end of it if no resolution is found.
|
||||
Conflict = Struct.new(
|
||||
:requirement,
|
||||
:requirements,
|
||||
:existing,
|
||||
:possibility_set,
|
||||
:locked_requirement,
|
||||
:requirement_trees,
|
||||
:activated_by_name,
|
||||
:underlying_error
|
||||
)
|
||||
|
||||
class Conflict
|
||||
# @return [Object] a spec that was unable to be activated due to a conflict
|
||||
def possibility
|
||||
possibility_set && possibility_set.latest_version
|
||||
end
|
||||
end
|
||||
|
||||
# A collection of possibility states that share the same dependencies
|
||||
# @attr [Array] dependencies the dependencies for this set of possibilities
|
||||
# @attr [Array] possibilities the possibilities
|
||||
PossibilitySet = Struct.new(:dependencies, :possibilities)
|
||||
|
||||
class PossibilitySet
|
||||
# String representation of the possibility set, for debugging
|
||||
def to_s
|
||||
"[#{possibilities.join(', ')}]"
|
||||
end
|
||||
|
||||
# @return [Object] most up-to-date dependency in the possibility set
|
||||
def latest_version
|
||||
possibilities.last
|
||||
end
|
||||
end
|
||||
|
||||
# Details of the state to unwind to when a conflict occurs, and the cause of the unwind
|
||||
# @attr [Integer] state_index the index of the state to unwind to
|
||||
# @attr [Object] state_requirement the requirement of the state we're unwinding to
|
||||
# @attr [Array] requirement_tree for the requirement we're relaxing
|
||||
# @attr [Array] conflicting_requirements the requirements that combined to cause the conflict
|
||||
# @attr [Array] requirement_trees for the conflict
|
||||
# @attr [Array] requirements_unwound_to_instead array of unwind requirements that were chosen over this unwind
|
||||
UnwindDetails = Struct.new(
|
||||
:state_index,
|
||||
:state_requirement,
|
||||
:requirement_tree,
|
||||
:conflicting_requirements,
|
||||
:requirement_trees,
|
||||
:requirements_unwound_to_instead
|
||||
)
|
||||
|
||||
class UnwindDetails
|
||||
include Comparable
|
||||
|
||||
# We compare UnwindDetails when choosing which state to unwind to. If
|
||||
# two options have the same state_index we prefer the one most
|
||||
# removed from a requirement that caused the conflict. Both options
|
||||
# would unwind to the same state, but a `grandparent` option will
|
||||
# filter out fewer of its possibilities after doing so - where a state
|
||||
# is both a `parent` and a `grandparent` to requirements that have
|
||||
# caused a conflict this is the correct behaviour.
|
||||
# @param [UnwindDetail] other UnwindDetail to be compared
|
||||
# @return [Integer] integer specifying ordering
|
||||
def <=>(other)
|
||||
if state_index > other.state_index
|
||||
1
|
||||
elsif state_index == other.state_index
|
||||
reversed_requirement_tree_index <=> other.reversed_requirement_tree_index
|
||||
else
|
||||
-1
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Integer] index of state requirement in reversed requirement tree
|
||||
# (the conflicting requirement itself will be at position 0)
|
||||
def reversed_requirement_tree_index
|
||||
@reversed_requirement_tree_index ||=
|
||||
if state_requirement
|
||||
requirement_tree.reverse.index(state_requirement)
|
||||
else
|
||||
999_999
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Boolean] where the requirement of the state we're unwinding
|
||||
# to directly caused the conflict. Note: in this case, it is
|
||||
# impossible for the state we're unwinding to to be a parent of
|
||||
# any of the other conflicting requirements (or we would have
|
||||
# circularity)
|
||||
def unwinding_to_primary_requirement?
|
||||
requirement_tree.last == state_requirement
|
||||
end
|
||||
|
||||
# @return [Array] array of sub-dependencies to avoid when choosing a
|
||||
# new possibility for the state we've unwound to. Only relevant for
|
||||
# non-primary unwinds
|
||||
def sub_dependencies_to_avoid
|
||||
@requirements_to_avoid ||=
|
||||
requirement_trees.map do |tree|
|
||||
index = tree.index(state_requirement)
|
||||
tree[index + 1] if index
|
||||
end.compact
|
||||
end
|
||||
|
||||
# @return [Array] array of all the requirements that led to the need for
|
||||
# this unwind
|
||||
def all_requirements
|
||||
@all_requirements ||= requirement_trees.flatten(1)
|
||||
end
|
||||
end
|
||||
|
||||
# @return [SpecificationProvider] the provider that knows about
|
||||
# dependencies, requirements, specifications, versions, etc.
|
||||
attr_reader :specification_provider
|
||||
|
||||
# @return [UI] the UI that knows how to communicate feedback about the
|
||||
# resolution process back to the user
|
||||
attr_reader :resolver_ui
|
||||
|
||||
# @return [DependencyGraph] the base dependency graph to which
|
||||
# dependencies should be 'locked'
|
||||
attr_reader :base
|
||||
|
||||
# @return [Array] the dependencies that were explicitly required
|
||||
attr_reader :original_requested
|
||||
|
||||
# Initializes a new resolution.
|
||||
# @param [SpecificationProvider] specification_provider
|
||||
# see {#specification_provider}
|
||||
# @param [UI] resolver_ui see {#resolver_ui}
|
||||
# @param [Array] requested see {#original_requested}
|
||||
# @param [DependencyGraph] base see {#base}
|
||||
def initialize(specification_provider, resolver_ui, requested, base)
|
||||
@specification_provider = specification_provider
|
||||
@resolver_ui = resolver_ui
|
||||
@original_requested = requested
|
||||
@base = base
|
||||
@states = []
|
||||
@iteration_counter = 0
|
||||
@parents_of = Hash.new { |h, k| h[k] = [] }
|
||||
end
|
||||
|
||||
# Resolves the {#original_requested} dependencies into a full dependency
|
||||
# graph
|
||||
# @raise [ResolverError] if successful resolution is impossible
|
||||
# @return [DependencyGraph] the dependency graph of successfully resolved
|
||||
# dependencies
|
||||
def resolve
|
||||
start_resolution
|
||||
|
||||
while state
|
||||
break if !state.requirement && state.requirements.empty?
|
||||
indicate_progress
|
||||
if state.respond_to?(:pop_possibility_state) # DependencyState
|
||||
debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" }
|
||||
state.pop_possibility_state.tap do |s|
|
||||
if s
|
||||
states.push(s)
|
||||
activated.tag(s)
|
||||
end
|
||||
end
|
||||
end
|
||||
process_topmost_state
|
||||
end
|
||||
|
||||
resolve_activated_specs
|
||||
ensure
|
||||
end_resolution
|
||||
end
|
||||
|
||||
# @return [Integer] the number of resolver iterations in between calls to
|
||||
# {#resolver_ui}'s {UI#indicate_progress} method
|
||||
attr_accessor :iteration_rate
|
||||
private :iteration_rate
|
||||
|
||||
# @return [Time] the time at which resolution began
|
||||
attr_accessor :started_at
|
||||
private :started_at
|
||||
|
||||
# @return [Array<ResolutionState>] the stack of states for the resolution
|
||||
attr_accessor :states
|
||||
private :states
|
||||
|
||||
private
|
||||
|
||||
# Sets up the resolution process
|
||||
# @return [void]
|
||||
def start_resolution
|
||||
@started_at = Time.now
|
||||
|
||||
push_initial_state
|
||||
|
||||
debug { "Starting resolution (#{@started_at})\nUser-requested dependencies: #{original_requested}" }
|
||||
resolver_ui.before_resolution
|
||||
end
|
||||
|
||||
def resolve_activated_specs
|
||||
activated.vertices.each do |_, vertex|
|
||||
next unless vertex.payload
|
||||
|
||||
latest_version = vertex.payload.possibilities.reverse_each.find do |possibility|
|
||||
vertex.requirements.all? { |req| requirement_satisfied_by?(req, activated, possibility) }
|
||||
end
|
||||
|
||||
activated.set_payload(vertex.name, latest_version)
|
||||
end
|
||||
activated.freeze
|
||||
end
|
||||
|
||||
# Ends the resolution process
|
||||
# @return [void]
|
||||
def end_resolution
|
||||
resolver_ui.after_resolution
|
||||
debug do
|
||||
"Finished resolution (#{@iteration_counter} steps) " \
|
||||
"(Took #{(ended_at = Time.now) - @started_at} seconds) (#{ended_at})"
|
||||
end
|
||||
debug { 'Unactivated: ' + Hash[activated.vertices.reject { |_n, v| v.payload }].keys.join(', ') } if state
|
||||
debug { 'Activated: ' + Hash[activated.vertices.select { |_n, v| v.payload }].keys.join(', ') } if state
|
||||
end
|
||||
|
||||
require_relative 'state'
|
||||
require_relative 'modules/specification_provider'
|
||||
|
||||
require_relative 'delegates/resolution_state'
|
||||
require_relative 'delegates/specification_provider'
|
||||
|
||||
include Bundler::Molinillo::Delegates::ResolutionState
|
||||
include Bundler::Molinillo::Delegates::SpecificationProvider
|
||||
|
||||
# Processes the topmost available {RequirementState} on the stack
|
||||
# @return [void]
|
||||
def process_topmost_state
|
||||
if possibility
|
||||
attempt_to_activate
|
||||
else
|
||||
create_conflict
|
||||
unwind_for_conflict
|
||||
end
|
||||
rescue CircularDependencyError => underlying_error
|
||||
create_conflict(underlying_error)
|
||||
unwind_for_conflict
|
||||
end
|
||||
|
||||
# @return [Object] the current possibility that the resolution is trying
|
||||
# to activate
|
||||
def possibility
|
||||
possibilities.last
|
||||
end
|
||||
|
||||
# @return [RequirementState] the current state the resolution is
|
||||
# operating upon
|
||||
def state
|
||||
states.last
|
||||
end
|
||||
|
||||
# Creates and pushes the initial state for the resolution, based upon the
|
||||
# {#requested} dependencies
|
||||
# @return [void]
|
||||
def push_initial_state
|
||||
graph = DependencyGraph.new.tap do |dg|
|
||||
original_requested.each do |requested|
|
||||
vertex = dg.add_vertex(name_for(requested), nil, true)
|
||||
vertex.explicit_requirements << requested
|
||||
end
|
||||
dg.tag(:initial_state)
|
||||
end
|
||||
|
||||
push_state_for_requirements(original_requested, true, graph)
|
||||
end
|
||||
|
||||
# Unwinds the states stack because a conflict has been encountered
|
||||
# @return [void]
|
||||
def unwind_for_conflict
|
||||
details_for_unwind = build_details_for_unwind
|
||||
unwind_options = unused_unwind_options
|
||||
debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" }
|
||||
conflicts.tap do |c|
|
||||
sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1)
|
||||
raise_error_unless_state(c)
|
||||
activated.rewind_to(sliced_states.first || :initial_state) if sliced_states
|
||||
state.conflicts = c
|
||||
state.unused_unwind_options = unwind_options
|
||||
filter_possibilities_after_unwind(details_for_unwind)
|
||||
index = states.size - 1
|
||||
@parents_of.each { |_, a| a.reject! { |i| i >= index } }
|
||||
state.unused_unwind_options.reject! { |uw| uw.state_index >= index }
|
||||
end
|
||||
end
|
||||
|
||||
# Raises a VersionConflict error, or any underlying error, if there is no
|
||||
# current state
|
||||
# @return [void]
|
||||
def raise_error_unless_state(conflicts)
|
||||
return if state
|
||||
|
||||
error = conflicts.values.map(&:underlying_error).compact.first
|
||||
raise error || VersionConflict.new(conflicts, specification_provider)
|
||||
end
|
||||
|
||||
# @return [UnwindDetails] Details of the nearest index to which we could unwind
|
||||
def build_details_for_unwind
|
||||
# Get the possible unwinds for the current conflict
|
||||
current_conflict = conflicts[name]
|
||||
binding_requirements = binding_requirements_for_conflict(current_conflict)
|
||||
unwind_details = unwind_options_for_requirements(binding_requirements)
|
||||
|
||||
last_detail_for_current_unwind = unwind_details.sort.last
|
||||
current_detail = last_detail_for_current_unwind
|
||||
|
||||
# Look for past conflicts that could be unwound to affect the
|
||||
# requirement tree for the current conflict
|
||||
all_reqs = last_detail_for_current_unwind.all_requirements
|
||||
all_reqs_size = all_reqs.size
|
||||
relevant_unused_unwinds = unused_unwind_options.select do |alternative|
|
||||
diff_reqs = all_reqs - alternative.requirements_unwound_to_instead
|
||||
next if diff_reqs.size == all_reqs_size
|
||||
# Find the highest index unwind whilst looping through
|
||||
current_detail = alternative if alternative > current_detail
|
||||
alternative
|
||||
end
|
||||
|
||||
# Add the current unwind options to the `unused_unwind_options` array.
|
||||
# The "used" option will be filtered out during `unwind_for_conflict`.
|
||||
state.unused_unwind_options += unwind_details.reject { |detail| detail.state_index == -1 }
|
||||
|
||||
# Update the requirements_unwound_to_instead on any relevant unused unwinds
|
||||
relevant_unused_unwinds.each do |d|
|
||||
(d.requirements_unwound_to_instead << current_detail.state_requirement).uniq!
|
||||
end
|
||||
unwind_details.each do |d|
|
||||
(d.requirements_unwound_to_instead << current_detail.state_requirement).uniq!
|
||||
end
|
||||
|
||||
current_detail
|
||||
end
|
||||
|
||||
# @param [Array<Object>] binding_requirements array of requirements that combine to create a conflict
|
||||
# @return [Array<UnwindDetails>] array of UnwindDetails that have a chance
|
||||
# of resolving the passed requirements
|
||||
def unwind_options_for_requirements(binding_requirements)
|
||||
unwind_details = []
|
||||
|
||||
trees = []
|
||||
binding_requirements.reverse_each do |r|
|
||||
partial_tree = [r]
|
||||
trees << partial_tree
|
||||
unwind_details << UnwindDetails.new(-1, nil, partial_tree, binding_requirements, trees, [])
|
||||
|
||||
# If this requirement has alternative possibilities, check if any would
|
||||
# satisfy the other requirements that created this conflict
|
||||
requirement_state = find_state_for(r)
|
||||
if conflict_fixing_possibilities?(requirement_state, binding_requirements)
|
||||
unwind_details << UnwindDetails.new(
|
||||
states.index(requirement_state),
|
||||
r,
|
||||
partial_tree,
|
||||
binding_requirements,
|
||||
trees,
|
||||
[]
|
||||
)
|
||||
end
|
||||
|
||||
# Next, look at the parent of this requirement, and check if the requirement
|
||||
# could have been avoided if an alternative PossibilitySet had been chosen
|
||||
parent_r = parent_of(r)
|
||||
next if parent_r.nil?
|
||||
partial_tree.unshift(parent_r)
|
||||
requirement_state = find_state_for(parent_r)
|
||||
if requirement_state.possibilities.any? { |set| !set.dependencies.include?(r) }
|
||||
unwind_details << UnwindDetails.new(
|
||||
states.index(requirement_state),
|
||||
parent_r,
|
||||
partial_tree,
|
||||
binding_requirements,
|
||||
trees,
|
||||
[]
|
||||
)
|
||||
end
|
||||
|
||||
# Finally, look at the grandparent and up of this requirement, looking
|
||||
# for any possibilities that wouldn't create their parent requirement
|
||||
grandparent_r = parent_of(parent_r)
|
||||
until grandparent_r.nil?
|
||||
partial_tree.unshift(grandparent_r)
|
||||
requirement_state = find_state_for(grandparent_r)
|
||||
if requirement_state.possibilities.any? { |set| !set.dependencies.include?(parent_r) }
|
||||
unwind_details << UnwindDetails.new(
|
||||
states.index(requirement_state),
|
||||
grandparent_r,
|
||||
partial_tree,
|
||||
binding_requirements,
|
||||
trees,
|
||||
[]
|
||||
)
|
||||
end
|
||||
parent_r = grandparent_r
|
||||
grandparent_r = parent_of(parent_r)
|
||||
end
|
||||
end
|
||||
|
||||
unwind_details
|
||||
end
|
||||
|
||||
# @param [DependencyState] state
|
||||
# @param [Array] binding_requirements array of requirements
|
||||
# @return [Boolean] whether or not the given state has any possibilities
|
||||
# that could satisfy the given requirements
|
||||
def conflict_fixing_possibilities?(state, binding_requirements)
|
||||
return false unless state
|
||||
|
||||
state.possibilities.any? do |possibility_set|
|
||||
possibility_set.possibilities.any? do |poss|
|
||||
possibility_satisfies_requirements?(poss, binding_requirements)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Filter's a state's possibilities to remove any that would not fix the
|
||||
# conflict we've just rewound from
|
||||
# @param [UnwindDetails] unwind_details details of the conflict just
|
||||
# unwound from
|
||||
# @return [void]
|
||||
def filter_possibilities_after_unwind(unwind_details)
|
||||
return unless state && !state.possibilities.empty?
|
||||
|
||||
if unwind_details.unwinding_to_primary_requirement?
|
||||
filter_possibilities_for_primary_unwind(unwind_details)
|
||||
else
|
||||
filter_possibilities_for_parent_unwind(unwind_details)
|
||||
end
|
||||
end
|
||||
|
||||
# Filter's a state's possibilities to remove any that would not satisfy
|
||||
# the requirements in the conflict we've just rewound from
|
||||
# @param [UnwindDetails] unwind_details details of the conflict just unwound from
|
||||
# @return [void]
|
||||
def filter_possibilities_for_primary_unwind(unwind_details)
|
||||
unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index }
|
||||
unwinds_to_state << unwind_details
|
||||
unwind_requirement_sets = unwinds_to_state.map(&:conflicting_requirements)
|
||||
|
||||
state.possibilities.reject! do |possibility_set|
|
||||
possibility_set.possibilities.none? do |poss|
|
||||
unwind_requirement_sets.any? do |requirements|
|
||||
possibility_satisfies_requirements?(poss, requirements)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @param [Object] possibility a single possibility
|
||||
# @param [Array] requirements an array of requirements
|
||||
# @return [Boolean] whether the possibility satisfies all of the
|
||||
# given requirements
|
||||
def possibility_satisfies_requirements?(possibility, requirements)
|
||||
name = name_for(possibility)
|
||||
|
||||
activated.tag(:swap)
|
||||
activated.set_payload(name, possibility) if activated.vertex_named(name)
|
||||
satisfied = requirements.all? { |r| requirement_satisfied_by?(r, activated, possibility) }
|
||||
activated.rewind_to(:swap)
|
||||
|
||||
satisfied
|
||||
end
|
||||
|
||||
# Filter's a state's possibilities to remove any that would (eventually)
|
||||
# create a requirement in the conflict we've just rewound from
|
||||
# @param [UnwindDetails] unwind_details details of the conflict just unwound from
|
||||
# @return [void]
|
||||
def filter_possibilities_for_parent_unwind(unwind_details)
|
||||
unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index }
|
||||
unwinds_to_state << unwind_details
|
||||
|
||||
primary_unwinds = unwinds_to_state.select(&:unwinding_to_primary_requirement?).uniq
|
||||
parent_unwinds = unwinds_to_state.uniq - primary_unwinds
|
||||
|
||||
allowed_possibility_sets = primary_unwinds.flat_map do |unwind|
|
||||
states[unwind.state_index].possibilities.select do |possibility_set|
|
||||
possibility_set.possibilities.any? do |poss|
|
||||
possibility_satisfies_requirements?(poss, unwind.conflicting_requirements)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
requirements_to_avoid = parent_unwinds.flat_map(&:sub_dependencies_to_avoid)
|
||||
|
||||
state.possibilities.reject! do |possibility_set|
|
||||
!allowed_possibility_sets.include?(possibility_set) &&
|
||||
(requirements_to_avoid - possibility_set.dependencies).empty?
|
||||
end
|
||||
end
|
||||
|
||||
# @param [Conflict] conflict
|
||||
# @return [Array] minimal array of requirements that would cause the passed
|
||||
# conflict to occur.
|
||||
def binding_requirements_for_conflict(conflict)
|
||||
return [conflict.requirement] if conflict.possibility.nil?
|
||||
|
||||
possible_binding_requirements = conflict.requirements.values.flatten(1).uniq
|
||||
|
||||
# When there's a `CircularDependency` error the conflicting requirement
|
||||
# (the one causing the circular) won't be `conflict.requirement`
|
||||
# (which won't be for the right state, because we won't have created it,
|
||||
# because it's circular).
|
||||
# We need to make sure we have that requirement in the conflict's list,
|
||||
# otherwise we won't be able to unwind properly, so we just return all
|
||||
# the requirements for the conflict.
|
||||
return possible_binding_requirements if conflict.underlying_error
|
||||
|
||||
possibilities = search_for(conflict.requirement)
|
||||
|
||||
# If all the requirements together don't filter out all possibilities,
|
||||
# then the only two requirements we need to consider are the initial one
|
||||
# (where the dependency's version was first chosen) and the last
|
||||
if binding_requirement_in_set?(nil, possible_binding_requirements, possibilities)
|
||||
return [conflict.requirement, requirement_for_existing_name(name_for(conflict.requirement))].compact
|
||||
end
|
||||
|
||||
# Loop through the possible binding requirements, removing each one
|
||||
# that doesn't bind. Use a `reverse_each` as we want the earliest set of
|
||||
# binding requirements, and don't use `reject!` as we wish to refine the
|
||||
# array *on each iteration*.
|
||||
binding_requirements = possible_binding_requirements.dup
|
||||
possible_binding_requirements.reverse_each do |req|
|
||||
next if req == conflict.requirement
|
||||
unless binding_requirement_in_set?(req, binding_requirements, possibilities)
|
||||
binding_requirements -= [req]
|
||||
end
|
||||
end
|
||||
|
||||
binding_requirements
|
||||
end
|
||||
|
||||
# @param [Object] requirement we wish to check
|
||||
# @param [Array] possible_binding_requirements array of requirements
|
||||
# @param [Array] possibilities array of possibilities the requirements will be used to filter
|
||||
# @return [Boolean] whether or not the given requirement is required to filter
|
||||
# out all elements of the array of possibilities.
|
||||
def binding_requirement_in_set?(requirement, possible_binding_requirements, possibilities)
|
||||
possibilities.any? do |poss|
|
||||
possibility_satisfies_requirements?(poss, possible_binding_requirements - [requirement])
|
||||
end
|
||||
end
|
||||
|
||||
# @param [Object] requirement
|
||||
# @return [Object] the requirement that led to `requirement` being added
|
||||
# to the list of requirements.
|
||||
def parent_of(requirement)
|
||||
return unless requirement
|
||||
return unless index = @parents_of[requirement].last
|
||||
return unless parent_state = @states[index]
|
||||
parent_state.requirement
|
||||
end
|
||||
|
||||
# @param [String] name
|
||||
# @return [Object] the requirement that led to a version of a possibility
|
||||
# with the given name being activated.
|
||||
def requirement_for_existing_name(name)
|
||||
return nil unless vertex = activated.vertex_named(name)
|
||||
return nil unless vertex.payload
|
||||
states.find { |s| s.name == name }.requirement
|
||||
end
|
||||
|
||||
# @param [Object] requirement
|
||||
# @return [ResolutionState] the state whose `requirement` is the given
|
||||
# `requirement`.
|
||||
def find_state_for(requirement)
|
||||
return nil unless requirement
|
||||
states.find { |i| requirement == i.requirement }
|
||||
end
|
||||
|
||||
# @param [Object] underlying_error
|
||||
# @return [Conflict] a {Conflict} that reflects the failure to activate
|
||||
# the {#possibility} in conjunction with the current {#state}
|
||||
def create_conflict(underlying_error = nil)
|
||||
vertex = activated.vertex_named(name)
|
||||
locked_requirement = locked_requirement_named(name)
|
||||
|
||||
requirements = {}
|
||||
unless vertex.explicit_requirements.empty?
|
||||
requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements
|
||||
end
|
||||
requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement
|
||||
vertex.incoming_edges.each do |edge|
|
||||
(requirements[edge.origin.payload.latest_version] ||= []).unshift(edge.requirement)
|
||||
end
|
||||
|
||||
activated_by_name = {}
|
||||
activated.each { |v| activated_by_name[v.name] = v.payload.latest_version if v.payload }
|
||||
conflicts[name] = Conflict.new(
|
||||
requirement,
|
||||
requirements,
|
||||
vertex.payload && vertex.payload.latest_version,
|
||||
possibility,
|
||||
locked_requirement,
|
||||
requirement_trees,
|
||||
activated_by_name,
|
||||
underlying_error
|
||||
)
|
||||
end
|
||||
|
||||
# @return [Array<Array<Object>>] The different requirement
|
||||
# trees that led to every requirement for the current spec.
|
||||
def requirement_trees
|
||||
vertex = activated.vertex_named(name)
|
||||
vertex.requirements.map { |r| requirement_tree_for(r) }
|
||||
end
|
||||
|
||||
# @param [Object] requirement
|
||||
# @return [Array<Object>] the list of requirements that led to
|
||||
# `requirement` being required.
|
||||
def requirement_tree_for(requirement)
|
||||
tree = []
|
||||
while requirement
|
||||
tree.unshift(requirement)
|
||||
requirement = parent_of(requirement)
|
||||
end
|
||||
tree
|
||||
end
|
||||
|
||||
# Indicates progress roughly once every second
|
||||
# @return [void]
|
||||
def indicate_progress
|
||||
@iteration_counter += 1
|
||||
@progress_rate ||= resolver_ui.progress_rate
|
||||
if iteration_rate.nil?
|
||||
if Time.now - started_at >= @progress_rate
|
||||
self.iteration_rate = @iteration_counter
|
||||
end
|
||||
end
|
||||
|
||||
if iteration_rate && (@iteration_counter % iteration_rate) == 0
|
||||
resolver_ui.indicate_progress
|
||||
end
|
||||
end
|
||||
|
||||
# Calls the {#resolver_ui}'s {UI#debug} method
|
||||
# @param [Integer] depth the depth of the {#states} stack
|
||||
# @param [Proc] block a block that yields a {#to_s}
|
||||
# @return [void]
|
||||
def debug(depth = 0, &block)
|
||||
resolver_ui.debug(depth, &block)
|
||||
end
|
||||
|
||||
# Attempts to activate the current {#possibility}
|
||||
# @return [void]
|
||||
def attempt_to_activate
|
||||
debug(depth) { 'Attempting to activate ' + possibility.to_s }
|
||||
existing_vertex = activated.vertex_named(name)
|
||||
if existing_vertex.payload
|
||||
debug(depth) { "Found existing spec (#{existing_vertex.payload})" }
|
||||
attempt_to_filter_existing_spec(existing_vertex)
|
||||
else
|
||||
latest = possibility.latest_version
|
||||
possibility.possibilities.select! do |possibility|
|
||||
requirement_satisfied_by?(requirement, activated, possibility)
|
||||
end
|
||||
if possibility.latest_version.nil?
|
||||
# ensure there's a possibility for better error messages
|
||||
possibility.possibilities << latest if latest
|
||||
create_conflict
|
||||
unwind_for_conflict
|
||||
else
|
||||
activate_new_spec
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Attempts to update the existing vertex's `PossibilitySet` with a filtered version
|
||||
# @return [void]
|
||||
def attempt_to_filter_existing_spec(vertex)
|
||||
filtered_set = filtered_possibility_set(vertex)
|
||||
if !filtered_set.possibilities.empty?
|
||||
activated.set_payload(name, filtered_set)
|
||||
new_requirements = requirements.dup
|
||||
push_state_for_requirements(new_requirements, false)
|
||||
else
|
||||
create_conflict
|
||||
debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" }
|
||||
unwind_for_conflict
|
||||
end
|
||||
end
|
||||
|
||||
# Generates a filtered version of the existing vertex's `PossibilitySet` using the
|
||||
# current state's `requirement`
|
||||
# @param [Object] vertex existing vertex
|
||||
# @return [PossibilitySet] filtered possibility set
|
||||
def filtered_possibility_set(vertex)
|
||||
PossibilitySet.new(vertex.payload.dependencies, vertex.payload.possibilities & possibility.possibilities)
|
||||
end
|
||||
|
||||
# @param [String] requirement_name the spec name to search for
|
||||
# @return [Object] the locked spec named `requirement_name`, if one
|
||||
# is found on {#base}
|
||||
def locked_requirement_named(requirement_name)
|
||||
vertex = base.vertex_named(requirement_name)
|
||||
vertex && vertex.payload
|
||||
end
|
||||
|
||||
# Add the current {#possibility} to the dependency graph of the current
|
||||
# {#state}
|
||||
# @return [void]
|
||||
def activate_new_spec
|
||||
conflicts.delete(name)
|
||||
debug(depth) { "Activated #{name} at #{possibility}" }
|
||||
activated.set_payload(name, possibility)
|
||||
require_nested_dependencies_for(possibility)
|
||||
end
|
||||
|
||||
# Requires the dependencies that the recently activated spec has
|
||||
# @param [Object] possibility_set the PossibilitySet that has just been
|
||||
# activated
|
||||
# @return [void]
|
||||
def require_nested_dependencies_for(possibility_set)
|
||||
nested_dependencies = dependencies_for(possibility_set.latest_version)
|
||||
debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" }
|
||||
nested_dependencies.each do |d|
|
||||
activated.add_child_vertex(name_for(d), nil, [name_for(possibility_set.latest_version)], d)
|
||||
parent_index = states.size - 1
|
||||
parents = @parents_of[d]
|
||||
parents << parent_index if parents.empty?
|
||||
end
|
||||
|
||||
push_state_for_requirements(requirements + nested_dependencies, !nested_dependencies.empty?)
|
||||
end
|
||||
|
||||
# Pushes a new {DependencyState} that encapsulates both existing and new
|
||||
# requirements
|
||||
# @param [Array] new_requirements
|
||||
# @param [Boolean] requires_sort
|
||||
# @param [Object] new_activated
|
||||
# @return [void]
|
||||
def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated)
|
||||
new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort
|
||||
new_requirement = nil
|
||||
loop do
|
||||
new_requirement = new_requirements.shift
|
||||
break if new_requirement.nil? || states.none? { |s| s.requirement == new_requirement }
|
||||
end
|
||||
new_name = new_requirement ? name_for(new_requirement) : ''.freeze
|
||||
possibilities = possibilities_for_requirement(new_requirement)
|
||||
handle_missing_or_push_dependency_state DependencyState.new(
|
||||
new_name, new_requirements, new_activated,
|
||||
new_requirement, possibilities, depth, conflicts.dup, unused_unwind_options.dup
|
||||
)
|
||||
end
|
||||
|
||||
# Checks a proposed requirement with any existing locked requirement
|
||||
# before generating an array of possibilities for it.
|
||||
# @param [Object] requirement the proposed requirement
|
||||
# @param [Object] activated
|
||||
# @return [Array] possibilities
|
||||
def possibilities_for_requirement(requirement, activated = self.activated)
|
||||
return [] unless requirement
|
||||
if locked_requirement_named(name_for(requirement))
|
||||
return locked_requirement_possibility_set(requirement, activated)
|
||||
end
|
||||
|
||||
group_possibilities(search_for(requirement))
|
||||
end
|
||||
|
||||
# @param [Object] requirement the proposed requirement
|
||||
# @param [Object] activated
|
||||
# @return [Array] possibility set containing only the locked requirement, if any
|
||||
def locked_requirement_possibility_set(requirement, activated = self.activated)
|
||||
all_possibilities = search_for(requirement)
|
||||
locked_requirement = locked_requirement_named(name_for(requirement))
|
||||
|
||||
# Longwinded way to build a possibilities array with either the locked
|
||||
# requirement or nothing in it. Required, since the API for
|
||||
# locked_requirement isn't guaranteed.
|
||||
locked_possibilities = all_possibilities.select do |possibility|
|
||||
requirement_satisfied_by?(locked_requirement, activated, possibility)
|
||||
end
|
||||
|
||||
group_possibilities(locked_possibilities)
|
||||
end
|
||||
|
||||
# Build an array of PossibilitySets, with each element representing a group of
|
||||
# dependency versions that all have the same sub-dependency version constraints
|
||||
# and are contiguous.
|
||||
# @param [Array] possibilities an array of possibilities
|
||||
# @return [Array<PossibilitySet>] an array of possibility sets
|
||||
def group_possibilities(possibilities)
|
||||
possibility_sets = []
|
||||
current_possibility_set = nil
|
||||
|
||||
possibilities.reverse_each do |possibility|
|
||||
dependencies = dependencies_for(possibility)
|
||||
if current_possibility_set && dependencies_equal?(current_possibility_set.dependencies, dependencies)
|
||||
current_possibility_set.possibilities.unshift(possibility)
|
||||
else
|
||||
possibility_sets.unshift(PossibilitySet.new(dependencies, [possibility]))
|
||||
current_possibility_set = possibility_sets.first
|
||||
end
|
||||
end
|
||||
|
||||
possibility_sets
|
||||
end
|
||||
|
||||
# Pushes a new {DependencyState}.
|
||||
# If the {#specification_provider} says to
|
||||
# {SpecificationProvider#allow_missing?} that particular requirement, and
|
||||
# there are no possibilities for that requirement, then `state` is not
|
||||
# pushed, and the vertex in {#activated} is removed, and we continue
|
||||
# resolving the remaining requirements.
|
||||
# @param [DependencyState] state
|
||||
# @return [void]
|
||||
def handle_missing_or_push_dependency_state(state)
|
||||
if state.requirement && state.possibilities.empty? && allow_missing?(state.requirement)
|
||||
state.activated.detach_vertex_named(state.name)
|
||||
push_state_for_requirements(state.requirements.dup, false, state.activated)
|
||||
else
|
||||
states.push(state).tap { activated.tag(state) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,46 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'dependency_graph'
|
||||
|
||||
module Bundler::Molinillo
|
||||
# This class encapsulates a dependency resolver.
|
||||
# The resolver is responsible for determining which set of dependencies to
|
||||
# activate, with feedback from the {#specification_provider}
|
||||
#
|
||||
#
|
||||
class Resolver
|
||||
require_relative 'resolution'
|
||||
|
||||
# @return [SpecificationProvider] the specification provider used
|
||||
# in the resolution process
|
||||
attr_reader :specification_provider
|
||||
|
||||
# @return [UI] the UI module used to communicate back to the user
|
||||
# during the resolution process
|
||||
attr_reader :resolver_ui
|
||||
|
||||
# Initializes a new resolver.
|
||||
# @param [SpecificationProvider] specification_provider
|
||||
# see {#specification_provider}
|
||||
# @param [UI] resolver_ui
|
||||
# see {#resolver_ui}
|
||||
def initialize(specification_provider, resolver_ui)
|
||||
@specification_provider = specification_provider
|
||||
@resolver_ui = resolver_ui
|
||||
end
|
||||
|
||||
# Resolves the requested dependencies into a {DependencyGraph},
|
||||
# locking to the base dependency graph (if specified)
|
||||
# @param [Array] requested an array of 'requested' dependencies that the
|
||||
# {#specification_provider} can understand
|
||||
# @param [DependencyGraph,nil] base the base dependency graph to which
|
||||
# dependencies should be 'locked'
|
||||
def resolve(requested, base = DependencyGraph.new)
|
||||
Resolution.new(specification_provider,
|
||||
resolver_ui,
|
||||
requested,
|
||||
base).
|
||||
resolve
|
||||
end
|
||||
end
|
||||
end
|
@ -1,58 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::Molinillo
|
||||
# A state that a {Resolution} can be in
|
||||
# @attr [String] name the name of the current requirement
|
||||
# @attr [Array<Object>] requirements currently unsatisfied requirements
|
||||
# @attr [DependencyGraph] activated the graph of activated dependencies
|
||||
# @attr [Object] requirement the current requirement
|
||||
# @attr [Object] possibilities the possibilities to satisfy the current requirement
|
||||
# @attr [Integer] depth the depth of the resolution
|
||||
# @attr [Hash] conflicts unresolved conflicts, indexed by dependency name
|
||||
# @attr [Array<UnwindDetails>] unused_unwind_options unwinds for previous conflicts that weren't explored
|
||||
ResolutionState = Struct.new(
|
||||
:name,
|
||||
:requirements,
|
||||
:activated,
|
||||
:requirement,
|
||||
:possibilities,
|
||||
:depth,
|
||||
:conflicts,
|
||||
:unused_unwind_options
|
||||
)
|
||||
|
||||
class ResolutionState
|
||||
# Returns an empty resolution state
|
||||
# @return [ResolutionState] an empty state
|
||||
def self.empty
|
||||
new(nil, [], DependencyGraph.new, nil, nil, 0, {}, [])
|
||||
end
|
||||
end
|
||||
|
||||
# A state that encapsulates a set of {#requirements} with an {Array} of
|
||||
# possibilities
|
||||
class DependencyState < ResolutionState
|
||||
# Removes a possibility from `self`
|
||||
# @return [PossibilityState] a state with a single possibility,
|
||||
# the possibility that was removed from `self`
|
||||
def pop_possibility_state
|
||||
PossibilityState.new(
|
||||
name,
|
||||
requirements.dup,
|
||||
activated,
|
||||
requirement,
|
||||
[possibilities.pop],
|
||||
depth + 1,
|
||||
conflicts.dup,
|
||||
unused_unwind_options.dup
|
||||
).tap do |state|
|
||||
state.activated.tag(state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A state that encapsulates a single possibility to fulfill the given
|
||||
# {#requirement}
|
||||
class PossibilityState < ResolutionState
|
||||
end
|
||||
end
|
21
lib/bundler/vendor/pub_grub/LICENSE.txt
vendored
Normal file
21
lib/bundler/vendor/pub_grub/LICENSE.txt
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 John Hawthorn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
31
lib/bundler/vendor/pub_grub/lib/pub_grub.rb
vendored
Normal file
31
lib/bundler/vendor/pub_grub/lib/pub_grub.rb
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
require_relative "pub_grub/package"
|
||||
require_relative "pub_grub/static_package_source"
|
||||
require_relative "pub_grub/term"
|
||||
require_relative "pub_grub/version_range"
|
||||
require_relative "pub_grub/version_constraint"
|
||||
require_relative "pub_grub/version_union"
|
||||
require_relative "pub_grub/version_solver"
|
||||
require_relative "pub_grub/incompatibility"
|
||||
require_relative 'pub_grub/solve_failure'
|
||||
require_relative 'pub_grub/failure_writer'
|
||||
require_relative 'pub_grub/version'
|
||||
|
||||
module Bundler::PubGrub
|
||||
class << self
|
||||
attr_writer :logger
|
||||
|
||||
def logger
|
||||
@logger || default_logger
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_logger
|
||||
require "logger"
|
||||
|
||||
logger = ::Logger.new(STDERR)
|
||||
logger.level = $DEBUG ? ::Logger::DEBUG : ::Logger::WARN
|
||||
@logger = logger
|
||||
end
|
||||
end
|
||||
end
|
20
lib/bundler/vendor/pub_grub/lib/pub_grub/assignment.rb
vendored
Normal file
20
lib/bundler/vendor/pub_grub/lib/pub_grub/assignment.rb
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
module Bundler::PubGrub
|
||||
class Assignment
|
||||
attr_reader :term, :cause, :decision_level, :index
|
||||
def initialize(term, cause, decision_level, index)
|
||||
@term = term
|
||||
@cause = cause
|
||||
@decision_level = decision_level
|
||||
@index = index
|
||||
end
|
||||
|
||||
def self.decision(package, version, decision_level, index)
|
||||
term = Term.new(VersionConstraint.exact(package, version), true)
|
||||
new(term, :decision, decision_level, index)
|
||||
end
|
||||
|
||||
def decision?
|
||||
cause == :decision
|
||||
end
|
||||
end
|
||||
end
|
189
lib/bundler/vendor/pub_grub/lib/pub_grub/basic_package_source.rb
vendored
Normal file
189
lib/bundler/vendor/pub_grub/lib/pub_grub/basic_package_source.rb
vendored
Normal file
@ -0,0 +1,189 @@
|
||||
require_relative 'version_constraint'
|
||||
require_relative 'incompatibility'
|
||||
|
||||
module Bundler::PubGrub
|
||||
# Types:
|
||||
#
|
||||
# Where possible, Bundler::PubGrub will accept user-defined types, so long as they quack.
|
||||
#
|
||||
# ## "Package":
|
||||
#
|
||||
# This class will be used to represent the various packages being solved for.
|
||||
# .to_s will be called when displaying errors and debugging info, it should
|
||||
# probably return the package's name.
|
||||
# It must also have a reasonable definition of #== and #hash
|
||||
#
|
||||
# Example classes: String ("rails")
|
||||
#
|
||||
#
|
||||
# ## "Version":
|
||||
#
|
||||
# This class will be used to represent a single version number.
|
||||
#
|
||||
# Versions don't need to store their associated package, however they will
|
||||
# only be compared against other versions of the same package.
|
||||
#
|
||||
# It must be Comparible (and implement <=> reasonably)
|
||||
#
|
||||
# Example classes: Gem::Version, Integer
|
||||
#
|
||||
#
|
||||
# ## "Dependency"
|
||||
#
|
||||
# This class represents the requirement one package has on another. It is
|
||||
# returned by dependencies_for(package, version) and will be passed to
|
||||
# parse_dependency to convert it to a format Bundler::PubGrub understands.
|
||||
#
|
||||
# It must also have a reasonable definition of #==
|
||||
#
|
||||
# Example classes: String ("~> 1.0"), Gem::Requirement
|
||||
#
|
||||
class BasicPackageSource
|
||||
# Override me!
|
||||
#
|
||||
# This is called per package to find all possible versions of a package.
|
||||
#
|
||||
# It is called at most once per-package
|
||||
#
|
||||
# Returns: Array of versions for a package, in preferred order of selection
|
||||
def all_versions_for(package)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Override me!
|
||||
#
|
||||
# Returns: Hash in the form of { package => requirement, ... }
|
||||
def dependencies_for(package, version)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Override me!
|
||||
#
|
||||
# Convert a (user-defined) dependency into a format Bundler::PubGrub understands.
|
||||
#
|
||||
# Package is passed to this method but for many implementations is not
|
||||
# needed.
|
||||
#
|
||||
# Returns: either a Bundler::PubGrub::VersionRange, Bundler::PubGrub::VersionUnion, or a
|
||||
# Bundler::PubGrub::VersionConstraint
|
||||
def parse_dependency(package, dependency)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Override me!
|
||||
#
|
||||
# If not overridden, this will call dependencies_for with the root package.
|
||||
#
|
||||
# Returns: Hash in the form of { package => requirement, ... } (see dependencies_for)
|
||||
def root_dependencies
|
||||
dependencies_for(@root_package, @root_version)
|
||||
end
|
||||
|
||||
# Override me (maybe)
|
||||
#
|
||||
# If not overridden, the order returned by all_versions_for will be used
|
||||
#
|
||||
# Returns: Array of versions in preferred order
|
||||
def sort_versions_by_preferred(package, sorted_versions)
|
||||
indexes = @version_indexes[package]
|
||||
sorted_versions.sort_by { |version| indexes[version] }
|
||||
end
|
||||
|
||||
def initialize
|
||||
@root_package = Package.root
|
||||
@root_version = Package.root_version
|
||||
|
||||
@cached_versions = Hash.new do |h,k|
|
||||
if k == @root_package
|
||||
h[k] = [@root_version]
|
||||
else
|
||||
h[k] = all_versions_for(k)
|
||||
end
|
||||
end
|
||||
@sorted_versions = Hash.new { |h,k| h[k] = @cached_versions[k].sort }
|
||||
@version_indexes = Hash.new { |h,k| h[k] = @cached_versions[k].each.with_index.to_h }
|
||||
|
||||
@cached_dependencies = Hash.new do |packages, package|
|
||||
if package == @root_package
|
||||
packages[package] = {
|
||||
@root_version => root_dependencies
|
||||
}
|
||||
else
|
||||
packages[package] = Hash.new do |versions, version|
|
||||
versions[version] = dependencies_for(package, version)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def versions_for(package, range=VersionRange.any)
|
||||
versions = range.select_versions(@sorted_versions[package])
|
||||
|
||||
# Conditional avoids (among other things) calling
|
||||
# sort_versions_by_preferred with the root package
|
||||
if versions.size > 1
|
||||
sort_versions_by_preferred(package, versions)
|
||||
else
|
||||
versions
|
||||
end
|
||||
end
|
||||
|
||||
def no_versions_incompatibility_for(_package, unsatisfied_term)
|
||||
cause = Incompatibility::NoVersions.new(unsatisfied_term)
|
||||
|
||||
Incompatibility.new([unsatisfied_term], cause: cause)
|
||||
end
|
||||
|
||||
def incompatibilities_for(package, version)
|
||||
package_deps = @cached_dependencies[package]
|
||||
sorted_versions = @sorted_versions[package]
|
||||
package_deps[version].map do |dep_package, dep_constraint_name|
|
||||
low = high = sorted_versions.index(version)
|
||||
|
||||
# find version low such that all >= low share the same dep
|
||||
while low > 0 &&
|
||||
package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint_name
|
||||
low -= 1
|
||||
end
|
||||
low =
|
||||
if low == 0
|
||||
nil
|
||||
else
|
||||
sorted_versions[low]
|
||||
end
|
||||
|
||||
# find version high such that all < high share the same dep
|
||||
while high < sorted_versions.length &&
|
||||
package_deps[sorted_versions[high]][dep_package] == dep_constraint_name
|
||||
high += 1
|
||||
end
|
||||
high =
|
||||
if high == sorted_versions.length
|
||||
nil
|
||||
else
|
||||
sorted_versions[high]
|
||||
end
|
||||
|
||||
range = VersionRange.new(min: low, max: high, include_min: true)
|
||||
|
||||
self_constraint = VersionConstraint.new(package, range: range)
|
||||
|
||||
if !@packages.include?(dep_package)
|
||||
# no such package -> this version is invalid
|
||||
end
|
||||
|
||||
dep_constraint = parse_dependency(dep_package, dep_constraint_name)
|
||||
if !dep_constraint
|
||||
# falsey indicates this dependency was invalid
|
||||
cause = Bundler::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint_name)
|
||||
return [Incompatibility.new([Term.new(self_constraint, true)], cause: cause)]
|
||||
elsif !dep_constraint.is_a?(VersionConstraint)
|
||||
# Upgrade range/union to VersionConstraint
|
||||
dep_constraint = VersionConstraint.new(dep_package, range: dep_constraint)
|
||||
end
|
||||
|
||||
Incompatibility.new([Term.new(self_constraint, true), Term.new(dep_constraint, false)], cause: :dependency)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
182
lib/bundler/vendor/pub_grub/lib/pub_grub/failure_writer.rb
vendored
Normal file
182
lib/bundler/vendor/pub_grub/lib/pub_grub/failure_writer.rb
vendored
Normal file
@ -0,0 +1,182 @@
|
||||
module Bundler::PubGrub
|
||||
class FailureWriter
|
||||
def initialize(root)
|
||||
@root = root
|
||||
|
||||
# { Incompatibility => Integer }
|
||||
@derivations = {}
|
||||
|
||||
# [ [ String, Integer or nil ] ]
|
||||
@lines = []
|
||||
|
||||
# { Incompatibility => Integer }
|
||||
@line_numbers = {}
|
||||
|
||||
count_derivations(root)
|
||||
end
|
||||
|
||||
def write
|
||||
return @root.to_s unless @root.conflict?
|
||||
|
||||
visit(@root)
|
||||
|
||||
padding = @line_numbers.empty? ? 0 : "(#{@line_numbers.values.last}) ".length
|
||||
|
||||
@lines.map do |message, number|
|
||||
next "" if message.empty?
|
||||
|
||||
lead = number ? "(#{number}) " : ""
|
||||
lead = lead.ljust(padding)
|
||||
message = message.gsub("\n", "\n" + " " * (padding + 2))
|
||||
"#{lead}#{message}"
|
||||
end.join("\n")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def write_line(incompatibility, message, numbered:)
|
||||
if numbered
|
||||
number = @line_numbers.length + 1
|
||||
@line_numbers[incompatibility] = number
|
||||
end
|
||||
|
||||
@lines << [message, number]
|
||||
end
|
||||
|
||||
def visit(incompatibility, conclusion: false)
|
||||
raise unless incompatibility.conflict?
|
||||
|
||||
numbered = conclusion || @derivations[incompatibility] > 1;
|
||||
conjunction = conclusion || incompatibility == @root ? "So," : "And"
|
||||
|
||||
cause = incompatibility.cause
|
||||
|
||||
if cause.conflict.conflict? && cause.other.conflict?
|
||||
conflict_line = @line_numbers[cause.conflict]
|
||||
other_line = @line_numbers[cause.other]
|
||||
|
||||
if conflict_line && other_line
|
||||
write_line(
|
||||
incompatibility,
|
||||
"Because #{cause.conflict} (#{conflict_line})\nand #{cause.other} (#{other_line}),\n#{incompatibility}.",
|
||||
numbered: numbered
|
||||
)
|
||||
elsif conflict_line || other_line
|
||||
with_line = conflict_line ? cause.conflict : cause.other
|
||||
without_line = conflict_line ? cause.other : cause.conflict
|
||||
line = @line_numbers[with_line]
|
||||
|
||||
visit(without_line);
|
||||
write_line(
|
||||
incompatibility,
|
||||
"#{conjunction} because #{with_line} (#{line}),\n#{incompatibility}.",
|
||||
numbered: numbered
|
||||
)
|
||||
else
|
||||
single_line_conflict = single_line?(cause.conflict.cause)
|
||||
single_line_other = single_line?(cause.other.cause)
|
||||
|
||||
if single_line_conflict || single_line_other
|
||||
first = single_line_other ? cause.conflict : cause.other
|
||||
second = single_line_other ? cause.other : cause.conflict
|
||||
visit(first)
|
||||
visit(second)
|
||||
write_line(
|
||||
incompatibility,
|
||||
"Thus, #{incompatibility}.",
|
||||
numbered: numbered
|
||||
)
|
||||
else
|
||||
visit(cause.conflict, conclusion: true)
|
||||
@lines << ["", nil]
|
||||
visit(cause.other)
|
||||
|
||||
write_line(
|
||||
incompatibility,
|
||||
"#{conjunction} because #{cause.conflict} (#{@line_numbers[cause.conflict]}),\n#{incompatibility}.",
|
||||
numbered: numbered
|
||||
)
|
||||
end
|
||||
end
|
||||
elsif cause.conflict.conflict? || cause.other.conflict?
|
||||
derived = cause.conflict.conflict? ? cause.conflict : cause.other
|
||||
ext = cause.conflict.conflict? ? cause.other : cause.conflict
|
||||
|
||||
derived_line = @line_numbers[derived]
|
||||
if derived_line
|
||||
write_line(
|
||||
incompatibility,
|
||||
"Because #{ext}\nand #{derived} (#{derived_line}),\n#{incompatibility}.",
|
||||
numbered: numbered
|
||||
)
|
||||
elsif collapsible?(derived)
|
||||
derived_cause = derived.cause
|
||||
if derived_cause.conflict.conflict?
|
||||
collapsed_derived = derived_cause.conflict
|
||||
collapsed_ext = derived_cause.other
|
||||
else
|
||||
collapsed_derived = derived_cause.other
|
||||
collapsed_ext = derived_cause.conflict
|
||||
end
|
||||
|
||||
visit(collapsed_derived)
|
||||
|
||||
write_line(
|
||||
incompatibility,
|
||||
"#{conjunction} because #{collapsed_ext}\nand #{ext},\n#{incompatibility}.",
|
||||
numbered: numbered
|
||||
)
|
||||
else
|
||||
visit(derived)
|
||||
write_line(
|
||||
incompatibility,
|
||||
"#{conjunction} because #{ext},\n#{incompatibility}.",
|
||||
numbered: numbered
|
||||
)
|
||||
end
|
||||
else
|
||||
write_line(
|
||||
incompatibility,
|
||||
"Because #{cause.conflict}\nand #{cause.other},\n#{incompatibility}.",
|
||||
numbered: numbered
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def single_line?(cause)
|
||||
!cause.conflict.conflict? && !cause.other.conflict?
|
||||
end
|
||||
|
||||
def collapsible?(incompatibility)
|
||||
return false if @derivations[incompatibility] > 1
|
||||
|
||||
cause = incompatibility.cause
|
||||
# If incompatibility is derived from two derived incompatibilities,
|
||||
# there are too many transitive causes to display concisely.
|
||||
return false if cause.conflict.conflict? && cause.other.conflict?
|
||||
|
||||
# If incompatibility is derived from two external incompatibilities, it
|
||||
# tends to be confusing to collapse it.
|
||||
return false unless cause.conflict.conflict? || cause.other.conflict?
|
||||
|
||||
# If incompatibility's internal cause is numbered, collapsing it would
|
||||
# get too noisy.
|
||||
complex = cause.conflict.conflict? ? cause.conflict : cause.other
|
||||
|
||||
!@line_numbers.has_key?(complex)
|
||||
end
|
||||
|
||||
def count_derivations(incompatibility)
|
||||
if @derivations.has_key?(incompatibility)
|
||||
@derivations[incompatibility] += 1
|
||||
else
|
||||
@derivations[incompatibility] = 1
|
||||
if incompatibility.conflict?
|
||||
cause = incompatibility.cause
|
||||
count_derivations(cause.conflict)
|
||||
count_derivations(cause.other)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
146
lib/bundler/vendor/pub_grub/lib/pub_grub/incompatibility.rb
vendored
Normal file
146
lib/bundler/vendor/pub_grub/lib/pub_grub/incompatibility.rb
vendored
Normal file
@ -0,0 +1,146 @@
|
||||
module Bundler::PubGrub
|
||||
class Incompatibility
|
||||
ConflictCause = Struct.new(:incompatibility, :satisfier) do
|
||||
alias_method :conflict, :incompatibility
|
||||
alias_method :other, :satisfier
|
||||
end
|
||||
|
||||
InvalidDependency = Struct.new(:package, :constraint) do
|
||||
end
|
||||
|
||||
NoVersions = Struct.new(:constraint) do
|
||||
end
|
||||
|
||||
attr_reader :terms, :cause
|
||||
|
||||
def initialize(terms, cause:, custom_explanation: nil)
|
||||
@cause = cause
|
||||
@terms = cleanup_terms(terms)
|
||||
@custom_explanation = custom_explanation
|
||||
|
||||
if cause == :dependency && @terms.length != 2
|
||||
raise ArgumentError, "a dependency Incompatibility must have exactly two terms. Got #{@terms.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def hash
|
||||
cause.hash ^ terms.hash
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
cause.eql?(other.cause) &&
|
||||
terms.eql?(other.terms)
|
||||
end
|
||||
|
||||
def failure?
|
||||
terms.empty? || (terms.length == 1 && Package.root?(terms[0].package) && terms[0].positive?)
|
||||
end
|
||||
|
||||
def conflict?
|
||||
ConflictCause === cause
|
||||
end
|
||||
|
||||
# Returns all external incompatibilities in this incompatibility's
|
||||
# derivation graph
|
||||
def external_incompatibilities
|
||||
if conflict?
|
||||
[
|
||||
cause.conflict,
|
||||
cause.other
|
||||
].flat_map(&:external_incompatibilities)
|
||||
else
|
||||
[this]
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
return @custom_explanation if @custom_explanation
|
||||
|
||||
case cause
|
||||
when :root
|
||||
"(root dependency)"
|
||||
when :dependency
|
||||
"#{terms[0].to_s(allow_every: true)} depends on #{terms[1].invert}"
|
||||
when Bundler::PubGrub::Incompatibility::InvalidDependency
|
||||
"#{terms[0].to_s(allow_every: true)} depends on unknown package #{cause.package}"
|
||||
when Bundler::PubGrub::Incompatibility::NoVersions
|
||||
"no versions satisfy #{cause.constraint}"
|
||||
when Bundler::PubGrub::Incompatibility::ConflictCause
|
||||
if failure?
|
||||
"version solving has failed"
|
||||
elsif terms.length == 1
|
||||
term = terms[0]
|
||||
if term.positive?
|
||||
"#{terms[0].to_s(allow_every: true)} is forbidden"
|
||||
else
|
||||
"#{terms[0].invert} is required"
|
||||
end
|
||||
else
|
||||
if terms.all?(&:positive?)
|
||||
if terms.length == 2
|
||||
"#{terms[0].to_s(allow_every: true)} is incompatible with #{terms[1]}"
|
||||
else
|
||||
"one of #{terms.map(&:to_s).join(" or ")} must be false"
|
||||
end
|
||||
elsif terms.all?(&:negative?)
|
||||
if terms.length == 2
|
||||
"either #{terms[0].invert} or #{terms[1].invert}"
|
||||
else
|
||||
"one of #{terms.map(&:invert).join(" or ")} must be true";
|
||||
end
|
||||
else
|
||||
positive = terms.select(&:positive?)
|
||||
negative = terms.select(&:negative?).map(&:invert)
|
||||
|
||||
if positive.length == 1
|
||||
"#{positive[0].to_s(allow_every: true)} requires #{negative.join(" or ")}"
|
||||
else
|
||||
"if #{positive.join(" and ")} then #{negative.join(" or ")}"
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
raise "unhandled cause: #{cause.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{to_s}>"
|
||||
end
|
||||
|
||||
def pretty_print(q)
|
||||
q.group 2, "#<#{self.class}", ">" do
|
||||
q.breakable
|
||||
q.text to_s
|
||||
|
||||
q.breakable
|
||||
q.text " caused by "
|
||||
q.pp @cause
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cleanup_terms(terms)
|
||||
terms.each do |term|
|
||||
raise "#{term.inspect} must be a term" unless term.is_a?(Term)
|
||||
end
|
||||
|
||||
if terms.length != 1 && ConflictCause === cause
|
||||
terms = terms.reject do |term|
|
||||
term.positive? && Package.root?(term.package)
|
||||
end
|
||||
end
|
||||
|
||||
# Optimized simple cases
|
||||
return terms if terms.length <= 1
|
||||
return terms if terms.length == 2 && terms[0].package != terms[1].package
|
||||
|
||||
terms.group_by(&:package).map do |package, common_terms|
|
||||
common_terms.inject do |acc, term|
|
||||
acc.intersect(term)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
43
lib/bundler/vendor/pub_grub/lib/pub_grub/package.rb
vendored
Normal file
43
lib/bundler/vendor/pub_grub/lib/pub_grub/package.rb
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::PubGrub
|
||||
class Package
|
||||
|
||||
attr_reader :name
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{name.inspect}>"
|
||||
end
|
||||
|
||||
def <=>(other)
|
||||
name <=> other.name
|
||||
end
|
||||
|
||||
ROOT = Package.new(:root)
|
||||
ROOT_VERSION = 0
|
||||
|
||||
def self.root
|
||||
ROOT
|
||||
end
|
||||
|
||||
def self.root_version
|
||||
ROOT_VERSION
|
||||
end
|
||||
|
||||
def self.root?(package)
|
||||
if package.respond_to?(:root?)
|
||||
package.root?
|
||||
else
|
||||
package == root
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
name.to_s
|
||||
end
|
||||
end
|
||||
end
|
121
lib/bundler/vendor/pub_grub/lib/pub_grub/partial_solution.rb
vendored
Normal file
121
lib/bundler/vendor/pub_grub/lib/pub_grub/partial_solution.rb
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
require_relative 'assignment'
|
||||
|
||||
module Bundler::PubGrub
|
||||
class PartialSolution
|
||||
attr_reader :assignments, :decisions
|
||||
attr_reader :attempted_solutions
|
||||
|
||||
def initialize
|
||||
reset!
|
||||
|
||||
@attempted_solutions = 1
|
||||
@backtracking = false
|
||||
end
|
||||
|
||||
def decision_level
|
||||
@decisions.length
|
||||
end
|
||||
|
||||
def relation(term)
|
||||
package = term.package
|
||||
return :overlap if !@terms.key?(package)
|
||||
|
||||
@relation_cache[package][term] ||=
|
||||
@terms[package].relation(term)
|
||||
end
|
||||
|
||||
def satisfies?(term)
|
||||
relation(term) == :subset
|
||||
end
|
||||
|
||||
def derive(term, cause)
|
||||
add_assignment(Assignment.new(term, cause, decision_level, assignments.length))
|
||||
end
|
||||
|
||||
def satisfier(term)
|
||||
assignment =
|
||||
@assignments_by[term.package].bsearch do |assignment_by|
|
||||
@cumulative_assignments[assignment_by].satisfies?(term)
|
||||
end
|
||||
|
||||
assignment || raise("#{term} unsatisfied")
|
||||
end
|
||||
|
||||
# A list of unsatisfied terms
|
||||
def unsatisfied
|
||||
@required.keys.reject do |package|
|
||||
@decisions.key?(package)
|
||||
end.map do |package|
|
||||
@terms[package]
|
||||
end
|
||||
end
|
||||
|
||||
def decide(package, version)
|
||||
@attempted_solutions += 1 if @backtracking
|
||||
@backtracking = false;
|
||||
|
||||
decisions[package] = version
|
||||
assignment = Assignment.decision(package, version, decision_level, assignments.length)
|
||||
add_assignment(assignment)
|
||||
end
|
||||
|
||||
def backtrack(previous_level)
|
||||
@backtracking = true
|
||||
|
||||
new_assignments = assignments.select do |assignment|
|
||||
assignment.decision_level <= previous_level
|
||||
end
|
||||
|
||||
new_decisions = Hash[decisions.first(previous_level)]
|
||||
|
||||
reset!
|
||||
|
||||
@decisions = new_decisions
|
||||
|
||||
new_assignments.each do |assignment|
|
||||
add_assignment(assignment)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset!
|
||||
# { Array<Assignment> }
|
||||
@assignments = []
|
||||
|
||||
# { Package => Array<Assignment> }
|
||||
@assignments_by = Hash.new { |h,k| h[k] = [] }
|
||||
@cumulative_assignments = {}.compare_by_identity
|
||||
|
||||
# { Package => Package::Version }
|
||||
@decisions = {}
|
||||
|
||||
# { Package => Term }
|
||||
@terms = {}
|
||||
@relation_cache = Hash.new { |h,k| h[k] = {} }
|
||||
|
||||
# { Package => Boolean }
|
||||
@required = {}
|
||||
end
|
||||
|
||||
def add_assignment(assignment)
|
||||
term = assignment.term
|
||||
package = term.package
|
||||
|
||||
@assignments << assignment
|
||||
@assignments_by[package] << assignment
|
||||
|
||||
@required[package] = true if term.positive?
|
||||
|
||||
if @terms.key?(package)
|
||||
old_term = @terms[package]
|
||||
@terms[package] = old_term.intersect(term)
|
||||
else
|
||||
@terms[package] = term
|
||||
end
|
||||
@relation_cache[package].clear
|
||||
|
||||
@cumulative_assignments[assignment] = @terms[package]
|
||||
end
|
||||
end
|
||||
end
|
45
lib/bundler/vendor/pub_grub/lib/pub_grub/rubygems.rb
vendored
Normal file
45
lib/bundler/vendor/pub_grub/lib/pub_grub/rubygems.rb
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
module Bundler::PubGrub
|
||||
module RubyGems
|
||||
extend self
|
||||
|
||||
def requirement_to_range(requirement)
|
||||
ranges = requirement.requirements.map do |(op, ver)|
|
||||
case op
|
||||
when "~>"
|
||||
name = "~> #{ver}"
|
||||
bump = ver.class.new(ver.bump.to_s + ".A")
|
||||
VersionRange.new(name: name, min: ver, max: bump, include_min: true)
|
||||
when ">"
|
||||
VersionRange.new(min: ver)
|
||||
when ">="
|
||||
VersionRange.new(min: ver, include_min: true)
|
||||
when "<"
|
||||
VersionRange.new(max: ver)
|
||||
when "<="
|
||||
VersionRange.new(max: ver, include_max: true)
|
||||
when "="
|
||||
VersionRange.new(min: ver, max: ver, include_min: true, include_max: true)
|
||||
when "!="
|
||||
VersionRange.new(min: ver, max: ver, include_min: true, include_max: true).invert
|
||||
else
|
||||
raise "bad version specifier: #{op}"
|
||||
end
|
||||
end
|
||||
|
||||
ranges.inject(&:intersect)
|
||||
end
|
||||
|
||||
def requirement_to_constraint(package, requirement)
|
||||
Bundler::PubGrub::VersionConstraint.new(package, range: requirement_to_range(requirement))
|
||||
end
|
||||
|
||||
def parse_range(dep)
|
||||
requirement_to_range(Gem::Requirement.new(dep))
|
||||
end
|
||||
|
||||
def parse_constraint(package, dep)
|
||||
range = parse_range(dep)
|
||||
Bundler::PubGrub::VersionConstraint.new(package, range: range)
|
||||
end
|
||||
end
|
||||
end
|
19
lib/bundler/vendor/pub_grub/lib/pub_grub/solve_failure.rb
vendored
Normal file
19
lib/bundler/vendor/pub_grub/lib/pub_grub/solve_failure.rb
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
require_relative 'failure_writer'
|
||||
|
||||
module Bundler::PubGrub
|
||||
class SolveFailure < StandardError
|
||||
attr_reader :incompatibility
|
||||
|
||||
def initialize(incompatibility)
|
||||
@incompatibility = incompatibility
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Could not find compatible versions\n\n#{explanation}"
|
||||
end
|
||||
|
||||
def explanation
|
||||
@explanation ||= FailureWriter.new(@incompatibility).write
|
||||
end
|
||||
end
|
||||
end
|
53
lib/bundler/vendor/pub_grub/lib/pub_grub/static_package_source.rb
vendored
Normal file
53
lib/bundler/vendor/pub_grub/lib/pub_grub/static_package_source.rb
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
require_relative 'package'
|
||||
require_relative 'version_constraint'
|
||||
require_relative 'incompatibility'
|
||||
require_relative 'basic_package_source'
|
||||
|
||||
module Bundler::PubGrub
|
||||
class StaticPackageSource < BasicPackageSource
|
||||
class DSL
|
||||
def initialize(packages, root_deps)
|
||||
@packages = packages
|
||||
@root_deps = root_deps
|
||||
end
|
||||
|
||||
def root(deps:)
|
||||
@root_deps.update(deps)
|
||||
end
|
||||
|
||||
def add(name, version, deps: {})
|
||||
version = Gem::Version.new(version)
|
||||
@packages[name] ||= {}
|
||||
raise ArgumentError, "#{name} #{version} declared twice" if @packages[name].key?(version)
|
||||
@packages[name][version] = deps
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@root_deps = {}
|
||||
@packages = {}
|
||||
|
||||
yield DSL.new(@packages, @root_deps)
|
||||
|
||||
super()
|
||||
end
|
||||
|
||||
def all_versions_for(package)
|
||||
@packages[package].keys
|
||||
end
|
||||
|
||||
def root_dependencies
|
||||
@root_deps
|
||||
end
|
||||
|
||||
def dependencies_for(package, version)
|
||||
@packages[package][version]
|
||||
end
|
||||
|
||||
def parse_dependency(package, dependency)
|
||||
return false unless @packages.key?(package)
|
||||
|
||||
Bundler::PubGrub::RubyGems.parse_constraint(package, dependency)
|
||||
end
|
||||
end
|
||||
end
|
105
lib/bundler/vendor/pub_grub/lib/pub_grub/term.rb
vendored
Normal file
105
lib/bundler/vendor/pub_grub/lib/pub_grub/term.rb
vendored
Normal file
@ -0,0 +1,105 @@
|
||||
module Bundler::PubGrub
|
||||
class Term
|
||||
attr_reader :package, :constraint, :positive
|
||||
|
||||
def initialize(constraint, positive)
|
||||
@constraint = constraint
|
||||
@package = @constraint.package
|
||||
@positive = positive
|
||||
end
|
||||
|
||||
def to_s(allow_every: false)
|
||||
if positive
|
||||
@constraint.to_s(allow_every: allow_every)
|
||||
else
|
||||
"not #{@constraint}"
|
||||
end
|
||||
end
|
||||
|
||||
def hash
|
||||
constraint.hash ^ positive.hash
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
positive == other.positive &&
|
||||
constraint.eql?(other.constraint)
|
||||
end
|
||||
|
||||
def invert
|
||||
self.class.new(@constraint, !@positive)
|
||||
end
|
||||
alias_method :inverse, :invert
|
||||
|
||||
def intersect(other)
|
||||
raise ArgumentError, "packages must match" if package != other.package
|
||||
|
||||
if positive? && other.positive?
|
||||
self.class.new(constraint.intersect(other.constraint), true)
|
||||
elsif negative? && other.negative?
|
||||
self.class.new(constraint.union(other.constraint), false)
|
||||
else
|
||||
positive = positive? ? self : other
|
||||
negative = negative? ? self : other
|
||||
self.class.new(positive.constraint.intersect(negative.constraint.invert), true)
|
||||
end
|
||||
end
|
||||
|
||||
def difference(other)
|
||||
intersect(other.invert)
|
||||
end
|
||||
|
||||
def relation(other)
|
||||
if positive? && other.positive?
|
||||
constraint.relation(other.constraint)
|
||||
elsif negative? && other.positive?
|
||||
if constraint.allows_all?(other.constraint)
|
||||
:disjoint
|
||||
else
|
||||
:overlap
|
||||
end
|
||||
elsif positive? && other.negative?
|
||||
if !other.constraint.allows_any?(constraint)
|
||||
:subset
|
||||
elsif other.constraint.allows_all?(constraint)
|
||||
:disjoint
|
||||
else
|
||||
:overlap
|
||||
end
|
||||
elsif negative? && other.negative?
|
||||
if constraint.allows_all?(other.constraint)
|
||||
:subset
|
||||
else
|
||||
:overlap
|
||||
end
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
def normalized_constraint
|
||||
@normalized_constraint ||= positive ? constraint : constraint.invert
|
||||
end
|
||||
|
||||
def satisfies?(other)
|
||||
raise ArgumentError, "packages must match" unless package == other.package
|
||||
|
||||
relation(other) == :subset
|
||||
end
|
||||
|
||||
def positive?
|
||||
@positive
|
||||
end
|
||||
|
||||
def negative?
|
||||
!positive?
|
||||
end
|
||||
|
||||
def empty?
|
||||
@empty ||= normalized_constraint.empty?
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{self}>"
|
||||
end
|
||||
end
|
||||
end
|
3
lib/bundler/vendor/pub_grub/lib/pub_grub/version.rb
vendored
Normal file
3
lib/bundler/vendor/pub_grub/lib/pub_grub/version.rb
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
module Bundler::PubGrub
|
||||
VERSION = "0.5.0"
|
||||
end
|
124
lib/bundler/vendor/pub_grub/lib/pub_grub/version_constraint.rb
vendored
Normal file
124
lib/bundler/vendor/pub_grub/lib/pub_grub/version_constraint.rb
vendored
Normal file
@ -0,0 +1,124 @@
|
||||
require_relative 'version_range'
|
||||
|
||||
module Bundler::PubGrub
|
||||
class VersionConstraint
|
||||
attr_reader :package, :range
|
||||
|
||||
# @param package [Bundler::PubGrub::Package]
|
||||
# @param range [Bundler::PubGrub::VersionRange]
|
||||
def initialize(package, range: nil)
|
||||
@package = package
|
||||
@range = range
|
||||
end
|
||||
|
||||
def hash
|
||||
package.hash ^ range.hash
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
package.eql?(other.package) &&
|
||||
range.eql?(other.range)
|
||||
end
|
||||
|
||||
class << self
|
||||
def exact(package, version)
|
||||
range = VersionRange.new(min: version, max: version, include_min: true, include_max: true)
|
||||
new(package, range: range)
|
||||
end
|
||||
|
||||
def any(package)
|
||||
new(package, range: VersionRange.any)
|
||||
end
|
||||
|
||||
def empty(package)
|
||||
new(package, range: VersionRange.empty)
|
||||
end
|
||||
end
|
||||
|
||||
def intersect(other)
|
||||
unless package == other.package
|
||||
raise ArgumentError, "Can only intersect between VersionConstraint of the same package"
|
||||
end
|
||||
|
||||
self.class.new(package, range: range.intersect(other.range))
|
||||
end
|
||||
|
||||
def union(other)
|
||||
unless package == other.package
|
||||
raise ArgumentError, "Can only intersect between VersionConstraint of the same package"
|
||||
end
|
||||
|
||||
self.class.new(package, range: range.union(other.range))
|
||||
end
|
||||
|
||||
def invert
|
||||
new_range = range.invert
|
||||
self.class.new(package, range: new_range)
|
||||
end
|
||||
|
||||
def difference(other)
|
||||
intersect(other.invert)
|
||||
end
|
||||
|
||||
def allows_all?(other)
|
||||
range.allows_all?(other.range)
|
||||
end
|
||||
|
||||
def allows_any?(other)
|
||||
range.intersects?(other.range)
|
||||
end
|
||||
|
||||
def subset?(other)
|
||||
other.allows_all?(self)
|
||||
end
|
||||
|
||||
def overlap?(other)
|
||||
other.allows_any?(self)
|
||||
end
|
||||
|
||||
def disjoint?(other)
|
||||
!overlap?(other)
|
||||
end
|
||||
|
||||
def relation(other)
|
||||
if subset?(other)
|
||||
:subset
|
||||
elsif overlap?(other)
|
||||
:overlap
|
||||
else
|
||||
:disjoint
|
||||
end
|
||||
end
|
||||
|
||||
def to_s(allow_every: false)
|
||||
if Package.root?(package)
|
||||
package.to_s
|
||||
elsif allow_every && any?
|
||||
"every version of #{package}"
|
||||
else
|
||||
"#{package} #{constraint_string}"
|
||||
end
|
||||
end
|
||||
|
||||
def constraint_string
|
||||
if any?
|
||||
">= 0"
|
||||
else
|
||||
range.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def empty?
|
||||
range.empty?
|
||||
end
|
||||
|
||||
# Does this match every version of the package
|
||||
def any?
|
||||
range.any?
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{self}>"
|
||||
end
|
||||
end
|
||||
end
|
409
lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb
vendored
Normal file
409
lib/bundler/vendor/pub_grub/lib/pub_grub/version_range.rb
vendored
Normal file
@ -0,0 +1,409 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::PubGrub
|
||||
class VersionRange
|
||||
attr_reader :min, :max, :include_min, :include_max
|
||||
|
||||
alias_method :include_min?, :include_min
|
||||
alias_method :include_max?, :include_max
|
||||
|
||||
class Empty < VersionRange
|
||||
undef_method :min, :max
|
||||
undef_method :include_min, :include_min?
|
||||
undef_method :include_max, :include_max?
|
||||
|
||||
def initialize
|
||||
end
|
||||
|
||||
def empty?
|
||||
true
|
||||
end
|
||||
|
||||
def eql?
|
||||
other.empty?
|
||||
end
|
||||
|
||||
def hash
|
||||
[].hash
|
||||
end
|
||||
|
||||
def intersects?(_)
|
||||
false
|
||||
end
|
||||
|
||||
def intersect(other)
|
||||
self
|
||||
end
|
||||
|
||||
def allows_all?(other)
|
||||
other.empty?
|
||||
end
|
||||
|
||||
def include?(_)
|
||||
false
|
||||
end
|
||||
|
||||
def any?
|
||||
false
|
||||
end
|
||||
|
||||
def to_s
|
||||
"(no versions)"
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.class == self.class
|
||||
end
|
||||
|
||||
def invert
|
||||
VersionRange.any
|
||||
end
|
||||
|
||||
def select_versions(_)
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
EMPTY = Empty.new
|
||||
|
||||
def self.empty
|
||||
EMPTY
|
||||
end
|
||||
|
||||
def self.any
|
||||
new
|
||||
end
|
||||
|
||||
def initialize(min: nil, max: nil, include_min: false, include_max: false, name: nil)
|
||||
@min = min
|
||||
@max = max
|
||||
@include_min = include_min
|
||||
@include_max = include_max
|
||||
@name = name
|
||||
end
|
||||
|
||||
def hash
|
||||
@hash ||= min.hash ^ max.hash ^ include_min.hash ^ include_max.hash
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
if other.is_a?(VersionRange)
|
||||
min.eql?(other.min) &&
|
||||
max.eql?(other.max) &&
|
||||
include_min.eql?(other.include_min) &&
|
||||
include_max.eql?(other.include_max)
|
||||
else
|
||||
ranges.eql?(other.ranges)
|
||||
end
|
||||
end
|
||||
|
||||
def ranges
|
||||
[self]
|
||||
end
|
||||
|
||||
def include?(version)
|
||||
compare_version(version) == 0
|
||||
end
|
||||
|
||||
# Partitions passed versions into [lower, within, higher]
|
||||
#
|
||||
# versions must be sorted
|
||||
def partition_versions(versions)
|
||||
min_index =
|
||||
if !min || versions.empty?
|
||||
0
|
||||
elsif include_min?
|
||||
(0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= min }
|
||||
else
|
||||
(0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > min }
|
||||
end
|
||||
|
||||
lower = versions.slice(0, min_index)
|
||||
versions = versions.slice(min_index, versions.size)
|
||||
|
||||
max_index =
|
||||
if !max || versions.empty?
|
||||
versions.size
|
||||
elsif include_max?
|
||||
(0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > max }
|
||||
else
|
||||
(0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= max }
|
||||
end
|
||||
|
||||
[
|
||||
lower,
|
||||
versions.slice(0, max_index),
|
||||
versions.slice(max_index, versions.size)
|
||||
]
|
||||
end
|
||||
|
||||
# Returns versions which are included by this range.
|
||||
#
|
||||
# versions must be sorted
|
||||
def select_versions(versions)
|
||||
return versions if any?
|
||||
|
||||
partition_versions(versions)[1]
|
||||
end
|
||||
|
||||
def compare_version(version)
|
||||
if min
|
||||
case version <=> min
|
||||
when -1
|
||||
return -1
|
||||
when 0
|
||||
return -1 if !include_min
|
||||
when 1
|
||||
end
|
||||
end
|
||||
|
||||
if max
|
||||
case version <=> max
|
||||
when -1
|
||||
when 0
|
||||
return 1 if !include_max
|
||||
when 1
|
||||
return 1
|
||||
end
|
||||
end
|
||||
|
||||
0
|
||||
end
|
||||
|
||||
def strictly_lower?(other)
|
||||
return false if !max || !other.min
|
||||
|
||||
case max <=> other.min
|
||||
when 0
|
||||
!include_max || !other.include_min
|
||||
when -1
|
||||
true
|
||||
when 1
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def strictly_higher?(other)
|
||||
other.strictly_lower?(self)
|
||||
end
|
||||
|
||||
def intersects?(other)
|
||||
return false if other.empty?
|
||||
return other.intersects?(self) if other.is_a?(VersionUnion)
|
||||
!strictly_lower?(other) && !strictly_higher?(other)
|
||||
end
|
||||
alias_method :allows_any?, :intersects?
|
||||
|
||||
def intersect(other)
|
||||
return other if other.empty?
|
||||
return other.intersect(self) if other.is_a?(VersionUnion)
|
||||
|
||||
min_range =
|
||||
if !min
|
||||
other
|
||||
elsif !other.min
|
||||
self
|
||||
else
|
||||
case min <=> other.min
|
||||
when 0
|
||||
include_min ? other : self
|
||||
when -1
|
||||
other
|
||||
when 1
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
max_range =
|
||||
if !max
|
||||
other
|
||||
elsif !other.max
|
||||
self
|
||||
else
|
||||
case max <=> other.max
|
||||
when 0
|
||||
include_max ? other : self
|
||||
when -1
|
||||
self
|
||||
when 1
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
if !min_range.equal?(max_range) && min_range.min && max_range.max
|
||||
case min_range.min <=> max_range.max
|
||||
when -1
|
||||
when 0
|
||||
if !min_range.include_min || !max_range.include_max
|
||||
return EMPTY
|
||||
end
|
||||
when 1
|
||||
return EMPTY
|
||||
end
|
||||
end
|
||||
|
||||
VersionRange.new(
|
||||
min: min_range.min,
|
||||
include_min: min_range.include_min,
|
||||
max: max_range.max,
|
||||
include_max: max_range.include_max
|
||||
)
|
||||
end
|
||||
|
||||
# The span covered by two ranges
|
||||
#
|
||||
# If self and other are contiguous, this builds a union of the two ranges.
|
||||
# (if they aren't you are probably calling the wrong method)
|
||||
def span(other)
|
||||
return self if other.empty?
|
||||
|
||||
min_range =
|
||||
if !min
|
||||
self
|
||||
elsif !other.min
|
||||
other
|
||||
else
|
||||
case min <=> other.min
|
||||
when 0
|
||||
include_min ? self : other
|
||||
when -1
|
||||
self
|
||||
when 1
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
max_range =
|
||||
if !max
|
||||
self
|
||||
elsif !other.max
|
||||
other
|
||||
else
|
||||
case max <=> other.max
|
||||
when 0
|
||||
include_max ? self : other
|
||||
when -1
|
||||
other
|
||||
when 1
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
VersionRange.new(
|
||||
min: min_range.min,
|
||||
include_min: min_range.include_min,
|
||||
max: max_range.max,
|
||||
include_max: max_range.include_max
|
||||
)
|
||||
end
|
||||
|
||||
def union(other)
|
||||
return other.union(self) if other.is_a?(VersionUnion)
|
||||
|
||||
if contiguous_to?(other)
|
||||
span(other)
|
||||
else
|
||||
VersionUnion.union([self, other])
|
||||
end
|
||||
end
|
||||
|
||||
def contiguous_to?(other)
|
||||
return false if other.empty?
|
||||
|
||||
intersects?(other) ||
|
||||
(min == other.max && (include_min || other.include_max)) ||
|
||||
(max == other.min && (include_max || other.include_min))
|
||||
end
|
||||
|
||||
def allows_all?(other)
|
||||
return true if other.empty?
|
||||
|
||||
if other.is_a?(VersionUnion)
|
||||
return VersionUnion.new([self]).allows_all?(other)
|
||||
end
|
||||
|
||||
return false if max && !other.max
|
||||
return false if min && !other.min
|
||||
|
||||
if min
|
||||
case min <=> other.min
|
||||
when -1
|
||||
when 0
|
||||
return false if !include_min && other.include_min
|
||||
when 1
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
if max
|
||||
case max <=> other.max
|
||||
when -1
|
||||
return false
|
||||
when 0
|
||||
return false if !include_max && other.include_max
|
||||
when 1
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def any?
|
||||
!min && !max
|
||||
end
|
||||
|
||||
def empty?
|
||||
false
|
||||
end
|
||||
|
||||
def to_s
|
||||
@name ||= constraints.join(", ")
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{to_s}>"
|
||||
end
|
||||
|
||||
def upper_invert
|
||||
return self.class.empty unless max
|
||||
|
||||
VersionRange.new(min: max, include_min: !include_max)
|
||||
end
|
||||
|
||||
def invert
|
||||
return self.class.empty if any?
|
||||
|
||||
low = VersionRange.new(max: min, include_max: !include_min)
|
||||
high = VersionRange.new(min: max, include_min: !include_max)
|
||||
|
||||
if !min
|
||||
high
|
||||
elsif !max
|
||||
low
|
||||
else
|
||||
low.union(high)
|
||||
end
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
self.class == other.class &&
|
||||
min == other.min &&
|
||||
max == other.max &&
|
||||
include_min == other.include_min &&
|
||||
include_max == other.include_max
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def constraints
|
||||
return ["any"] if any?
|
||||
return ["= #{min}"] if min == max
|
||||
|
||||
c = []
|
||||
c << "#{include_min ? ">=" : ">"} #{min}" if min
|
||||
c << "#{include_max ? "<=" : "<"} #{max}" if max
|
||||
c
|
||||
end
|
||||
|
||||
end
|
||||
end
|
240
lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb
vendored
Normal file
240
lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb
vendored
Normal file
@ -0,0 +1,240 @@
|
||||
require_relative 'partial_solution'
|
||||
require_relative 'term'
|
||||
require_relative 'incompatibility'
|
||||
require_relative 'solve_failure'
|
||||
|
||||
module Bundler::PubGrub
|
||||
class VersionSolver
|
||||
attr_reader :logger
|
||||
attr_reader :source
|
||||
attr_reader :solution
|
||||
|
||||
def initialize(source:, root: Package.root, logger: Bundler::PubGrub.logger)
|
||||
@logger = logger
|
||||
|
||||
@source = source
|
||||
|
||||
# { package => [incompatibility, ...]}
|
||||
@incompatibilities = Hash.new do |h, k|
|
||||
h[k] = []
|
||||
end
|
||||
|
||||
@seen_incompatibilities = {}
|
||||
|
||||
@solution = PartialSolution.new
|
||||
|
||||
add_incompatibility Incompatibility.new([
|
||||
Term.new(VersionConstraint.any(root), false)
|
||||
], cause: :root)
|
||||
|
||||
propagate(root)
|
||||
end
|
||||
|
||||
def solved?
|
||||
solution.unsatisfied.empty?
|
||||
end
|
||||
|
||||
# Returns true if there is more work to be done, false otherwise
|
||||
def work
|
||||
return false if solved?
|
||||
|
||||
next_package = choose_package_version
|
||||
propagate(next_package)
|
||||
|
||||
if solved?
|
||||
logger.info { "Solution found after #{solution.attempted_solutions} attempts:" }
|
||||
solution.decisions.each do |package, version|
|
||||
next if Package.root?(package)
|
||||
logger.info { "* #{package} #{version}" }
|
||||
end
|
||||
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def solve
|
||||
work until solved?
|
||||
|
||||
solution.decisions
|
||||
end
|
||||
|
||||
alias_method :result, :solve
|
||||
|
||||
private
|
||||
|
||||
def propagate(initial_package)
|
||||
changed = [initial_package]
|
||||
while package = changed.shift
|
||||
@incompatibilities[package].reverse_each do |incompatibility|
|
||||
result = propagate_incompatibility(incompatibility)
|
||||
if result == :conflict
|
||||
root_cause = resolve_conflict(incompatibility)
|
||||
changed.clear
|
||||
changed << propagate_incompatibility(root_cause)
|
||||
elsif result # should be a Package
|
||||
changed << result
|
||||
end
|
||||
end
|
||||
changed.uniq!
|
||||
end
|
||||
end
|
||||
|
||||
def propagate_incompatibility(incompatibility)
|
||||
unsatisfied = nil
|
||||
incompatibility.terms.each do |term|
|
||||
relation = solution.relation(term)
|
||||
if relation == :disjoint
|
||||
return nil
|
||||
elsif relation == :overlap
|
||||
# If more than one term is inconclusive, we can't deduce anything
|
||||
return nil if unsatisfied
|
||||
unsatisfied = term
|
||||
end
|
||||
end
|
||||
|
||||
if !unsatisfied
|
||||
return :conflict
|
||||
end
|
||||
|
||||
logger.debug { "derived: #{unsatisfied.invert}" }
|
||||
|
||||
solution.derive(unsatisfied.invert, incompatibility)
|
||||
|
||||
unsatisfied.package
|
||||
end
|
||||
|
||||
def next_package_to_try
|
||||
solution.unsatisfied.min_by do |term|
|
||||
package = term.package
|
||||
range = term.constraint.range
|
||||
matching_versions = source.versions_for(package, range)
|
||||
higher_versions = source.versions_for(package, range.upper_invert)
|
||||
|
||||
[matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
|
||||
end.package
|
||||
end
|
||||
|
||||
def choose_package_version
|
||||
if solution.unsatisfied.empty?
|
||||
logger.info "No packages unsatisfied. Solving complete!"
|
||||
return nil
|
||||
end
|
||||
|
||||
package = next_package_to_try
|
||||
unsatisfied_term = solution.unsatisfied.find { |t| t.package == package }
|
||||
version = source.versions_for(package, unsatisfied_term.constraint.range).first
|
||||
|
||||
if version.nil?
|
||||
add_incompatibility source.no_versions_incompatibility_for(package, unsatisfied_term)
|
||||
return package
|
||||
end
|
||||
|
||||
conflict = false
|
||||
|
||||
source.incompatibilities_for(package, version).each do |incompatibility|
|
||||
if @seen_incompatibilities.include?(incompatibility)
|
||||
logger.debug { "knew: #{incompatibility}" }
|
||||
next
|
||||
end
|
||||
@seen_incompatibilities[incompatibility] = true
|
||||
|
||||
add_incompatibility incompatibility
|
||||
|
||||
conflict ||= incompatibility.terms.all? do |term|
|
||||
term.package == package || solution.satisfies?(term)
|
||||
end
|
||||
end
|
||||
|
||||
unless conflict
|
||||
logger.info { "selecting #{package} #{version}" }
|
||||
|
||||
solution.decide(package, version)
|
||||
end
|
||||
|
||||
package
|
||||
end
|
||||
|
||||
def resolve_conflict(incompatibility)
|
||||
logger.info { "conflict: #{incompatibility}" }
|
||||
|
||||
new_incompatibility = false
|
||||
|
||||
while !incompatibility.failure?
|
||||
most_recent_term = nil
|
||||
most_recent_satisfier = nil
|
||||
difference = nil
|
||||
|
||||
previous_level = 1
|
||||
|
||||
incompatibility.terms.each do |term|
|
||||
satisfier = solution.satisfier(term)
|
||||
|
||||
if most_recent_satisfier.nil?
|
||||
most_recent_term = term
|
||||
most_recent_satisfier = satisfier
|
||||
elsif most_recent_satisfier.index < satisfier.index
|
||||
previous_level = [previous_level, most_recent_satisfier.decision_level].max
|
||||
most_recent_term = term
|
||||
most_recent_satisfier = satisfier
|
||||
difference = nil
|
||||
else
|
||||
previous_level = [previous_level, satisfier.decision_level].max
|
||||
end
|
||||
|
||||
if most_recent_term == term
|
||||
difference = most_recent_satisfier.term.difference(most_recent_term)
|
||||
if difference.empty?
|
||||
difference = nil
|
||||
else
|
||||
difference_satisfier = solution.satisfier(difference.inverse)
|
||||
previous_level = [previous_level, difference_satisfier.decision_level].max
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if previous_level < most_recent_satisfier.decision_level ||
|
||||
most_recent_satisfier.decision?
|
||||
|
||||
logger.info { "backtracking to #{previous_level}" }
|
||||
solution.backtrack(previous_level)
|
||||
|
||||
if new_incompatibility
|
||||
add_incompatibility(incompatibility)
|
||||
end
|
||||
|
||||
return incompatibility
|
||||
end
|
||||
|
||||
new_terms = []
|
||||
new_terms += incompatibility.terms - [most_recent_term]
|
||||
new_terms += most_recent_satisfier.cause.terms.reject { |term|
|
||||
term.package == most_recent_satisfier.term.package
|
||||
}
|
||||
if difference
|
||||
new_terms << difference.invert
|
||||
end
|
||||
|
||||
incompatibility = Incompatibility.new(new_terms, cause: Incompatibility::ConflictCause.new(incompatibility, most_recent_satisfier.cause))
|
||||
|
||||
new_incompatibility = true
|
||||
|
||||
partially = difference ? " partially" : ""
|
||||
logger.info { "! #{most_recent_term} is#{partially} satisfied by #{most_recent_satisfier.term}" }
|
||||
logger.info { "! which is caused by #{most_recent_satisfier.cause}" }
|
||||
logger.info { "! thus #{incompatibility}" }
|
||||
end
|
||||
|
||||
raise SolveFailure.new(incompatibility)
|
||||
end
|
||||
|
||||
def add_incompatibility(incompatibility)
|
||||
logger.debug { "fact: #{incompatibility}" }
|
||||
incompatibility.terms.each do |term|
|
||||
package = term.package
|
||||
@incompatibilities[package] << incompatibility
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
178
lib/bundler/vendor/pub_grub/lib/pub_grub/version_union.rb
vendored
Normal file
178
lib/bundler/vendor/pub_grub/lib/pub_grub/version_union.rb
vendored
Normal file
@ -0,0 +1,178 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler::PubGrub
|
||||
class VersionUnion
|
||||
attr_reader :ranges
|
||||
|
||||
def self.normalize_ranges(ranges)
|
||||
ranges = ranges.flat_map do |range|
|
||||
range.ranges
|
||||
end
|
||||
|
||||
ranges.reject!(&:empty?)
|
||||
|
||||
return [] if ranges.empty?
|
||||
|
||||
mins, ranges = ranges.partition { |r| !r.min }
|
||||
original_ranges = mins + ranges.sort_by { |r| [r.min, r.include_min ? 0 : 1] }
|
||||
ranges = [original_ranges.shift]
|
||||
original_ranges.each do |range|
|
||||
if ranges.last.contiguous_to?(range)
|
||||
ranges << ranges.pop.span(range)
|
||||
else
|
||||
ranges << range
|
||||
end
|
||||
end
|
||||
|
||||
ranges
|
||||
end
|
||||
|
||||
def self.union(ranges, normalize: true)
|
||||
ranges = normalize_ranges(ranges) if normalize
|
||||
|
||||
if ranges.size == 0
|
||||
VersionRange.empty
|
||||
elsif ranges.size == 1
|
||||
ranges[0]
|
||||
else
|
||||
new(ranges)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(ranges)
|
||||
raise ArgumentError unless ranges.all? { |r| r.instance_of?(VersionRange) }
|
||||
@ranges = ranges
|
||||
end
|
||||
|
||||
def hash
|
||||
ranges.hash
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
ranges.eql?(other.ranges)
|
||||
end
|
||||
|
||||
def include?(version)
|
||||
!!ranges.bsearch {|r| r.compare_version(version) }
|
||||
end
|
||||
|
||||
def select_versions(all_versions)
|
||||
versions = []
|
||||
ranges.inject(all_versions) do |acc, range|
|
||||
_, matching, higher = range.partition_versions(acc)
|
||||
versions.concat matching
|
||||
higher
|
||||
end
|
||||
versions
|
||||
end
|
||||
|
||||
def intersects?(other)
|
||||
my_ranges = ranges.dup
|
||||
other_ranges = other.ranges.dup
|
||||
|
||||
my_range = my_ranges.shift
|
||||
other_range = other_ranges.shift
|
||||
while my_range && other_range
|
||||
if my_range.intersects?(other_range)
|
||||
return true
|
||||
end
|
||||
|
||||
if !my_range.max || (other_range.max && other_range.max < my_range.max)
|
||||
other_range = other_ranges.shift
|
||||
else
|
||||
my_range = my_ranges.shift
|
||||
end
|
||||
end
|
||||
end
|
||||
alias_method :allows_any?, :intersects?
|
||||
|
||||
def allows_all?(other)
|
||||
my_ranges = ranges.dup
|
||||
|
||||
my_range = my_ranges.shift
|
||||
|
||||
other.ranges.all? do |other_range|
|
||||
while my_range
|
||||
break if my_range.allows_all?(other_range)
|
||||
my_range = my_ranges.shift
|
||||
end
|
||||
|
||||
!!my_range
|
||||
end
|
||||
end
|
||||
|
||||
def empty?
|
||||
false
|
||||
end
|
||||
|
||||
def any?
|
||||
false
|
||||
end
|
||||
|
||||
def intersect(other)
|
||||
my_ranges = ranges.dup
|
||||
other_ranges = other.ranges.dup
|
||||
new_ranges = []
|
||||
|
||||
my_range = my_ranges.shift
|
||||
other_range = other_ranges.shift
|
||||
while my_range && other_range
|
||||
new_ranges << my_range.intersect(other_range)
|
||||
|
||||
if !my_range.max || (other_range.max && other_range.max < my_range.max)
|
||||
other_range = other_ranges.shift
|
||||
else
|
||||
my_range = my_ranges.shift
|
||||
end
|
||||
end
|
||||
new_ranges.reject!(&:empty?)
|
||||
VersionUnion.union(new_ranges, normalize: false)
|
||||
end
|
||||
|
||||
def upper_invert
|
||||
ranges.last.upper_invert
|
||||
end
|
||||
|
||||
def invert
|
||||
ranges.map(&:invert).inject(:intersect)
|
||||
end
|
||||
|
||||
def union(other)
|
||||
VersionUnion.union([self, other])
|
||||
end
|
||||
|
||||
def to_s
|
||||
output = []
|
||||
|
||||
ranges = self.ranges.dup
|
||||
while !ranges.empty?
|
||||
ne = []
|
||||
range = ranges.shift
|
||||
while !ranges.empty? && ranges[0].min == range.max
|
||||
ne << range.max
|
||||
range = range.span(ranges.shift)
|
||||
end
|
||||
|
||||
ne.map! {|x| "!= #{x}" }
|
||||
if ne.empty?
|
||||
output << range.to_s
|
||||
elsif range.any?
|
||||
output << ne.join(', ')
|
||||
else
|
||||
output << "#{range}, #{ne.join(', ')}"
|
||||
end
|
||||
end
|
||||
|
||||
output.join(" OR ")
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{to_s}>"
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
self.class == other.class &&
|
||||
self.ranges == other.ranges
|
||||
end
|
||||
end
|
||||
end
|
@ -1,4 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler; end
|
||||
require_relative "vendor/molinillo/lib/molinillo"
|
||||
require_relative "vendor/pub_grub/lib/pub_grub"
|
@ -6,4 +6,8 @@ module Bundler
|
||||
def self.bundler_major_version
|
||||
@bundler_major_version ||= VERSION.split(".").first.to_i
|
||||
end
|
||||
|
||||
def self.gem_version
|
||||
@gem_version ||= Gem::Version.create(VERSION)
|
||||
end
|
||||
end
|
||||
|
@ -1,122 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
module VersionRanges
|
||||
NEq = Struct.new(:version)
|
||||
ReqR = Struct.new(:left, :right)
|
||||
class ReqR
|
||||
Endpoint = Struct.new(:version, :inclusive) do
|
||||
def <=>(other)
|
||||
if version.equal?(INFINITY)
|
||||
return 0 if other.version.equal?(INFINITY)
|
||||
return 1
|
||||
elsif other.version.equal?(INFINITY)
|
||||
return -1
|
||||
end
|
||||
|
||||
comp = version <=> other.version
|
||||
return comp unless comp.zero?
|
||||
|
||||
if inclusive && !other.inclusive
|
||||
1
|
||||
elsif !inclusive && other.inclusive
|
||||
-1
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{left.inclusive ? "[" : "("}#{left.version}, #{right.version}#{right.inclusive ? "]" : ")"}"
|
||||
end
|
||||
INFINITY = begin
|
||||
inf = Object.new
|
||||
def inf.to_s
|
||||
"∞"
|
||||
end
|
||||
def inf.<=>(other)
|
||||
return 0 if other.equal?(self)
|
||||
1
|
||||
end
|
||||
inf.freeze
|
||||
end
|
||||
ZERO = Gem::Version.new("0.a")
|
||||
|
||||
def cover?(v)
|
||||
return false if left.inclusive && left.version > v
|
||||
return false if !left.inclusive && left.version >= v
|
||||
|
||||
if right.version != INFINITY
|
||||
return false if right.inclusive && right.version < v
|
||||
return false if !right.inclusive && right.version <= v
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def empty?
|
||||
left.version == right.version && !(left.inclusive && right.inclusive)
|
||||
end
|
||||
|
||||
def single?
|
||||
left.version == right.version
|
||||
end
|
||||
|
||||
def <=>(other)
|
||||
return -1 if other.equal?(INFINITY)
|
||||
|
||||
comp = left <=> other.left
|
||||
return comp unless comp.zero?
|
||||
|
||||
right <=> other.right
|
||||
end
|
||||
|
||||
UNIVERSAL = ReqR.new(ReqR::Endpoint.new(Gem::Version.new("0.a"), true), ReqR::Endpoint.new(ReqR::INFINITY, false)).freeze
|
||||
end
|
||||
|
||||
def self.for_many(requirements)
|
||||
requirements = requirements.map(&:requirements).flatten(1).map {|r| r.join(" ") }
|
||||
requirements << ">= 0.a" if requirements.empty?
|
||||
requirement = Gem::Requirement.new(requirements)
|
||||
self.for(requirement)
|
||||
end
|
||||
|
||||
def self.for(requirement)
|
||||
ranges = requirement.requirements.map do |op, v|
|
||||
case op
|
||||
when "=" then ReqR.new(ReqR::Endpoint.new(v, true), ReqR::Endpoint.new(v, true))
|
||||
when "!=" then NEq.new(v)
|
||||
when ">=" then ReqR.new(ReqR::Endpoint.new(v, true), ReqR::Endpoint.new(ReqR::INFINITY, false))
|
||||
when ">" then ReqR.new(ReqR::Endpoint.new(v, false), ReqR::Endpoint.new(ReqR::INFINITY, false))
|
||||
when "<" then ReqR.new(ReqR::Endpoint.new(ReqR::ZERO, true), ReqR::Endpoint.new(v, false))
|
||||
when "<=" then ReqR.new(ReqR::Endpoint.new(ReqR::ZERO, true), ReqR::Endpoint.new(v, true))
|
||||
when "~>" then ReqR.new(ReqR::Endpoint.new(v, true), ReqR::Endpoint.new(v.bump, false))
|
||||
else raise "unknown version op #{op} in requirement #{requirement}"
|
||||
end
|
||||
end.uniq
|
||||
ranges, neqs = ranges.partition {|r| !r.is_a?(NEq) }
|
||||
|
||||
[ranges.sort, neqs.map(&:version)]
|
||||
end
|
||||
|
||||
def self.empty?(ranges, neqs)
|
||||
!ranges.reduce(ReqR::UNIVERSAL) do |last_range, curr_range|
|
||||
next false unless last_range
|
||||
next false if curr_range.single? && neqs.include?(curr_range.left.version)
|
||||
next curr_range if last_range.right.version == ReqR::INFINITY
|
||||
case last_range.right.version <=> curr_range.left.version
|
||||
# higher
|
||||
when 1 then next ReqR.new(curr_range.left, last_range.right)
|
||||
# equal
|
||||
when 0
|
||||
if last_range.right.inclusive && curr_range.left.inclusive && !neqs.include?(curr_range.left.version)
|
||||
ReqR.new(curr_range.left, [curr_range.right, last_range.right].max)
|
||||
end
|
||||
# lower
|
||||
when -1 then next false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -193,31 +193,6 @@ RSpec.describe Bundler::Definition do
|
||||
|
||||
describe "initialize" do
|
||||
context "gem version promoter" do
|
||||
context "with lockfile" do
|
||||
before do
|
||||
install_gemfile <<-G
|
||||
source "#{file_uri_for(gem_repo1)}"
|
||||
gem "foo"
|
||||
G
|
||||
|
||||
allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
|
||||
end
|
||||
|
||||
it "should get a locked specs list when updating all" do
|
||||
definition = Bundler::Definition.new(bundled_app_lock, [], Bundler::SourceList.new, true)
|
||||
locked_specs = definition.gem_version_promoter.locked_specs
|
||||
expect(locked_specs.to_a.map(&:name)).to eq ["foo"]
|
||||
expect(definition.instance_variable_get("@locked_specs").empty?).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
context "without gemfile or lockfile" do
|
||||
it "should not attempt to parse empty lockfile contents" do
|
||||
definition = Bundler::Definition.new(nil, [], mock_source_list, true)
|
||||
expect(definition.gem_version_promoter.locked_specs.to_a).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context "eager unlock" do
|
||||
let(:source_list) do
|
||||
Bundler::SourceList.new.tap do |source_list|
|
||||
|
@ -6,35 +6,32 @@ RSpec.describe Bundler::GemVersionPromoter do
|
||||
result.flatten.map(&:version).map(&:to_s)
|
||||
end
|
||||
|
||||
def make_instance(*args)
|
||||
@gvp = Bundler::GemVersionPromoter.new(*args).tap do |gvp|
|
||||
def make_instance
|
||||
@gvp = Bundler::GemVersionPromoter.new.tap do |gvp|
|
||||
gvp.class.class_eval { public :filter_dep_specs, :sort_dep_specs }
|
||||
end
|
||||
end
|
||||
|
||||
def unlocking(options)
|
||||
make_instance(Bundler::SpecSet.new([]), ["foo"]).tap do |p|
|
||||
def with_options(options)
|
||||
make_instance.tap do |p|
|
||||
p.level = options[:level] if options[:level]
|
||||
p.strict = options[:strict] if options[:strict]
|
||||
end
|
||||
end
|
||||
|
||||
def keep_locked(options)
|
||||
make_instance(Bundler::SpecSet.new([]), ["bar"]).tap do |p|
|
||||
p.level = options[:level] if options[:level]
|
||||
p.strict = options[:strict] if options[:strict]
|
||||
end
|
||||
end
|
||||
|
||||
def build_spec_groups(name, versions)
|
||||
def build_candidates(versions)
|
||||
versions.map do |v|
|
||||
Bundler::Resolver::SpecGroup.new(build_spec(name, v), [Gem::Platform::RUBY])
|
||||
Bundler::Resolver::Candidate.new(v)
|
||||
end
|
||||
end
|
||||
|
||||
def build_spec_set(name, v)
|
||||
Bundler::SpecSet.new(build_spec(name, v))
|
||||
end
|
||||
|
||||
# Rightmost (highest array index) in result is most preferred.
|
||||
# Leftmost (lowest array index) in result is least preferred.
|
||||
# `build_spec_groups` has all versions of gem in index.
|
||||
# `build_candidates` has all versions of gem in index.
|
||||
# `build_spec` is the version currently in the .lock file.
|
||||
#
|
||||
# In default (not strict) mode, all versions in the index will
|
||||
@ -43,28 +40,28 @@ RSpec.describe Bundler::GemVersionPromoter do
|
||||
# would not consider conservative.
|
||||
context "filter specs (strict) level patch" do
|
||||
it "when keeping build_spec, keep current, next release" do
|
||||
keep_locked(:level => :patch)
|
||||
with_options(:level => :patch)
|
||||
res = @gvp.filter_dep_specs(
|
||||
build_spec_groups("foo", %w[1.7.8 1.7.9 1.8.0]),
|
||||
build_spec("foo", "1.7.8").first
|
||||
build_candidates(%w[1.7.8 1.7.9 1.8.0]),
|
||||
Bundler::Resolver::Package.new("foo", [], build_spec_set("foo", "1.7.8"), [])
|
||||
)
|
||||
expect(versions(res)).to eq %w[1.7.9 1.7.8]
|
||||
expect(versions(res)).to match_array %w[1.7.9 1.7.8]
|
||||
end
|
||||
|
||||
it "when unlocking prefer next release first" do
|
||||
unlocking(:level => :patch)
|
||||
with_options(:level => :patch)
|
||||
res = @gvp.filter_dep_specs(
|
||||
build_spec_groups("foo", %w[1.7.8 1.7.9 1.8.0]),
|
||||
build_spec("foo", "1.7.8").first
|
||||
build_candidates(%w[1.7.8 1.7.9 1.8.0]),
|
||||
Bundler::Resolver::Package.new("foo", [], build_spec_set("foo", "1.7.8"), [])
|
||||
)
|
||||
expect(versions(res)).to eq %w[1.7.8 1.7.9]
|
||||
end
|
||||
|
||||
it "when unlocking keep current when already at latest release" do
|
||||
unlocking(:level => :patch)
|
||||
with_options(:level => :patch)
|
||||
res = @gvp.filter_dep_specs(
|
||||
build_spec_groups("foo", %w[1.7.9 1.8.0 2.0.0]),
|
||||
build_spec("foo", "1.7.9").first
|
||||
build_candidates(%w[1.7.9 1.8.0 2.0.0]),
|
||||
Bundler::Resolver::Package.new("foo", [], build_spec_set("foo", "1.7.9"), [])
|
||||
)
|
||||
expect(versions(res)).to eq %w[1.7.9]
|
||||
end
|
||||
@ -72,57 +69,57 @@ RSpec.describe Bundler::GemVersionPromoter do
|
||||
|
||||
context "filter specs (strict) level minor" do
|
||||
it "when unlocking favor next releases, remove minor and major increases" do
|
||||
unlocking(:level => :minor)
|
||||
with_options(:level => :minor)
|
||||
res = @gvp.filter_dep_specs(
|
||||
build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]),
|
||||
build_spec("foo", "0.2.0").first
|
||||
build_candidates(%w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]),
|
||||
Bundler::Resolver::Package.new("foo", [], build_spec_set("foo", "0.2.0"), [])
|
||||
)
|
||||
expect(versions(res)).to eq %w[0.2.0 0.3.0 0.3.1 0.9.0]
|
||||
end
|
||||
|
||||
it "when keep locked, keep current, then favor next release, remove minor and major increases" do
|
||||
keep_locked(:level => :minor)
|
||||
with_options(:level => :minor)
|
||||
res = @gvp.filter_dep_specs(
|
||||
build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]),
|
||||
build_spec("foo", "0.2.0").first
|
||||
build_candidates(%w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]),
|
||||
Bundler::Resolver::Package.new("foo", [], build_spec_set("foo", "0.2.0"), ["bar"])
|
||||
)
|
||||
expect(versions(res)).to eq %w[0.3.0 0.3.1 0.9.0 0.2.0]
|
||||
expect(versions(res)).to match_array %w[0.3.0 0.3.1 0.9.0 0.2.0]
|
||||
end
|
||||
end
|
||||
|
||||
context "sort specs (not strict) level patch" do
|
||||
it "when not unlocking, same order but make sure build_spec version is most preferred to stay put" do
|
||||
keep_locked(:level => :patch)
|
||||
with_options(:level => :patch)
|
||||
res = @gvp.sort_dep_specs(
|
||||
build_spec_groups("foo", %w[1.5.4 1.6.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 2.0.0 2.0.1]),
|
||||
build_spec("foo", "1.7.7").first
|
||||
build_candidates(%w[1.5.4 1.6.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 2.0.0 2.0.1]),
|
||||
Bundler::Resolver::Package.new("foo", [], build_spec_set("foo", "1.7.7"), ["bar"])
|
||||
)
|
||||
expect(versions(res)).to eq %w[1.5.4 1.6.5 1.7.6 2.0.0 2.0.1 1.8.0 1.8.1 1.7.8 1.7.9 1.7.7]
|
||||
end
|
||||
|
||||
it "when unlocking favor next release, then current over minor increase" do
|
||||
unlocking(:level => :patch)
|
||||
with_options(:level => :patch)
|
||||
res = @gvp.sort_dep_specs(
|
||||
build_spec_groups("foo", %w[1.7.7 1.7.8 1.7.9 1.8.0]),
|
||||
build_spec("foo", "1.7.8").first
|
||||
build_candidates(%w[1.7.7 1.7.8 1.7.9 1.8.0]),
|
||||
Bundler::Resolver::Package.new("foo", [], build_spec_set("foo", "1.7.8"), [])
|
||||
)
|
||||
expect(versions(res)).to eq %w[1.7.7 1.8.0 1.7.8 1.7.9]
|
||||
end
|
||||
|
||||
it "when unlocking do proper integer comparison, not string" do
|
||||
unlocking(:level => :patch)
|
||||
with_options(:level => :patch)
|
||||
res = @gvp.sort_dep_specs(
|
||||
build_spec_groups("foo", %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0]),
|
||||
build_spec("foo", "1.7.8").first
|
||||
build_candidates(%w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0]),
|
||||
Bundler::Resolver::Package.new("foo", [], build_spec_set("foo", "1.7.8"), [])
|
||||
)
|
||||
expect(versions(res)).to eq %w[1.7.7 1.8.0 1.7.8 1.7.9 1.7.15]
|
||||
end
|
||||
|
||||
it "leave current when unlocking but already at latest release" do
|
||||
unlocking(:level => :patch)
|
||||
with_options(:level => :patch)
|
||||
res = @gvp.sort_dep_specs(
|
||||
build_spec_groups("foo", %w[1.7.9 1.8.0 2.0.0]),
|
||||
build_spec("foo", "1.7.9").first
|
||||
build_candidates(%w[1.7.9 1.8.0 2.0.0]),
|
||||
Bundler::Resolver::Package.new("foo", [], build_spec_set("foo", "1.7.9"), [])
|
||||
)
|
||||
expect(versions(res)).to eq %w[2.0.0 1.8.0 1.7.9]
|
||||
end
|
||||
@ -130,10 +127,10 @@ RSpec.describe Bundler::GemVersionPromoter do
|
||||
|
||||
context "sort specs (not strict) level minor" do
|
||||
it "when unlocking favor next release, then minor increase over current" do
|
||||
unlocking(:level => :minor)
|
||||
with_options(:level => :minor)
|
||||
res = @gvp.sort_dep_specs(
|
||||
build_spec_groups("foo", %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]),
|
||||
build_spec("foo", "0.2.0").first
|
||||
build_candidates(%w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.0 2.0.1]),
|
||||
Bundler::Resolver::Package.new("foo", [], build_spec_set("foo", "0.2.0"), [])
|
||||
)
|
||||
expect(versions(res)).to eq %w[2.0.0 2.0.1 1.0.0 0.2.0 0.3.0 0.3.1 0.9.0]
|
||||
end
|
||||
|
10
spec/bundler/bundler/resolver/candidate_spec.rb
Normal file
10
spec/bundler/bundler/resolver/candidate_spec.rb
Normal file
@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Bundler::Resolver::Candidate do
|
||||
it "compares fine" do
|
||||
version1 = described_class.new("1.12.5", :specs => [Gem::Specification.new("foo", "1.12.5") {|s| s.platform = Gem::Platform::RUBY }])
|
||||
version2 = described_class.new("1.12.5")
|
||||
|
||||
expect(version1 >= version2).to be true
|
||||
end
|
||||
end
|
@ -1,40 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "bundler/version_ranges"
|
||||
|
||||
RSpec.describe Bundler::VersionRanges do
|
||||
describe ".empty?" do
|
||||
shared_examples_for "empty?" do |exp, *req|
|
||||
it "returns #{exp} for #{req}" do
|
||||
r = Gem::Requirement.new(*req)
|
||||
ranges = described_class.for(r)
|
||||
expect(described_class.empty?(*ranges)).to eq(exp), "expected `#{r}` #{exp ? "" : "not "}to be empty"
|
||||
end
|
||||
end
|
||||
|
||||
include_examples "empty?", false
|
||||
include_examples "empty?", false, "!= 1"
|
||||
include_examples "empty?", false, "!= 1", "= 2"
|
||||
include_examples "empty?", false, "!= 1", "> 1"
|
||||
include_examples "empty?", false, "!= 1", ">= 1"
|
||||
include_examples "empty?", false, "= 1", ">= 0.1", "<= 1.1"
|
||||
include_examples "empty?", false, "= 1", ">= 1", "<= 1"
|
||||
include_examples "empty?", false, "= 1", "~> 1"
|
||||
include_examples "empty?", false, ">= 0.z", "= 0"
|
||||
include_examples "empty?", false, ">= 0"
|
||||
include_examples "empty?", false, ">= 1.0.0", "< 2.0.0"
|
||||
include_examples "empty?", false, "~> 1"
|
||||
include_examples "empty?", false, "~> 2.0", "~> 2.1"
|
||||
include_examples "empty?", true, ">= 4.1.0", "< 5.0", "= 5.2.1"
|
||||
include_examples "empty?", true, "< 5.0", "< 5.3", "< 6.0", "< 6", "= 5.2.0", "> 2", ">= 3.0", ">= 3.1", ">= 3.2", ">= 4.0.0", ">= 4.1.0", ">= 4.2.0", ">= 4.2", ">= 4"
|
||||
include_examples "empty?", true, "!= 1", "< 2", "> 2"
|
||||
include_examples "empty?", true, "!= 1", "<= 1", ">= 1"
|
||||
include_examples "empty?", true, "< 2", "> 2"
|
||||
include_examples "empty?", true, "< 2", "> 2", "= 2"
|
||||
include_examples "empty?", true, "= 1", "!= 1"
|
||||
include_examples "empty?", true, "= 1", "= 2"
|
||||
include_examples "empty?", true, "= 1", "~> 2"
|
||||
include_examples "empty?", true, ">= 0", "<= 0.a"
|
||||
include_examples "empty?", true, "~> 2.0", "~> 3"
|
||||
end
|
||||
end
|
@ -37,12 +37,11 @@ RSpec.describe "bundle install" do
|
||||
G
|
||||
|
||||
nice_error = <<-E.strip.gsub(/^ {8}/, "")
|
||||
Bundler could not find compatible versions for gem "bundler":
|
||||
In Gemfile:
|
||||
bundler (= 0.9.1)
|
||||
Could not find compatible versions
|
||||
|
||||
Current Bundler version:
|
||||
bundler (#{Bundler::VERSION})
|
||||
Because the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler = 0.9.1
|
||||
and Gemfile depends on bundler = 0.9.1,
|
||||
version solving has failed.
|
||||
|
||||
Your bundle requires a different version of Bundler than the one you're running.
|
||||
Install the necessary version with `gem install bundler:0.9.1` and rerun bundler using `bundle _0.9.1_ install`
|
||||
@ -58,12 +57,14 @@ RSpec.describe "bundle install" do
|
||||
G
|
||||
|
||||
nice_error = <<-E.strip.gsub(/^ {8}/, "")
|
||||
Bundler could not find compatible versions for gem "bundler":
|
||||
In Gemfile:
|
||||
bundler (~> 0.8)
|
||||
Could not find compatible versions
|
||||
|
||||
Current Bundler version:
|
||||
bundler (#{Bundler::VERSION})
|
||||
Because rails >= 3.0 depends on bundler >= 0.9.0.pre
|
||||
and the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler >= 0.9.0.pre, < 1.A,
|
||||
rails >= 3.0 requires bundler >= 1.A.
|
||||
So, because Gemfile depends on rails = 3.0
|
||||
and Gemfile depends on bundler ~> 0.8,
|
||||
version solving has failed.
|
||||
|
||||
Your bundle requires a different version of Bundler than the one you're running.
|
||||
Install the necessary version with `gem install bundler:0.9.1` and rerun bundler using `bundle _0.9.1_ install`
|
||||
@ -79,12 +80,11 @@ RSpec.describe "bundle install" do
|
||||
G
|
||||
|
||||
nice_error = <<-E.strip.gsub(/^ {8}/, "")
|
||||
Bundler could not find compatible versions for gem "bundler":
|
||||
In Gemfile:
|
||||
bundler (= 0.9.2)
|
||||
Could not find compatible versions
|
||||
|
||||
Current Bundler version:
|
||||
bundler (#{Bundler::VERSION})
|
||||
Because the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler = 0.9.2
|
||||
and Gemfile depends on bundler = 0.9.2,
|
||||
version solving has failed.
|
||||
|
||||
Your bundle requires a different version of Bundler than the one you're running, and that version could not be found.
|
||||
E
|
||||
@ -150,13 +150,14 @@ RSpec.describe "bundle install" do
|
||||
G
|
||||
|
||||
nice_error = <<-E.strip.gsub(/^ {8}/, "")
|
||||
Bundler could not find compatible versions for gem "activesupport":
|
||||
In Gemfile:
|
||||
activemerchant was resolved to 1.0, which depends on
|
||||
activesupport (>= 2.0.0)
|
||||
Could not find compatible versions
|
||||
|
||||
rails_pinned_to_old_activesupport was resolved to 1.0, which depends on
|
||||
activesupport (= 1.2.3)
|
||||
Because every version of rails_pinned_to_old_activesupport depends on activesupport = 1.2.3
|
||||
and every version of activemerchant depends on activesupport >= 2.0.0,
|
||||
every version of rails_pinned_to_old_activesupport is incompatible with activemerchant >= 0.
|
||||
So, because Gemfile depends on activemerchant >= 0
|
||||
and Gemfile depends on rails_pinned_to_old_activesupport >= 0,
|
||||
version solving has failed.
|
||||
E
|
||||
expect(err).to include(nice_error)
|
||||
end
|
||||
@ -177,12 +178,13 @@ RSpec.describe "bundle install" do
|
||||
G
|
||||
|
||||
nice_error = <<-E.strip.gsub(/^ {8}/, "")
|
||||
Bundler could not find compatible versions for gem "activesupport":
|
||||
In Gemfile:
|
||||
activesupport (= 2.3.5)
|
||||
Could not find compatible versions
|
||||
|
||||
rails_pinned_to_old_activesupport was resolved to 1.0, which depends on
|
||||
activesupport (= 1.2.3)
|
||||
Because every version of rails_pinned_to_old_activesupport depends on activesupport = 1.2.3
|
||||
and Gemfile depends on rails_pinned_to_old_activesupport >= 0,
|
||||
activesupport = 1.2.3 is required.
|
||||
So, because Gemfile depends on activesupport = 2.3.5,
|
||||
version solving has failed.
|
||||
E
|
||||
expect(err).to include(nice_error)
|
||||
end
|
||||
|
@ -881,7 +881,7 @@ RSpec.describe "bundle install with git sources" do
|
||||
gem "has_submodule"
|
||||
end
|
||||
G
|
||||
expect(err).to match(/could not find gem 'submodule/i)
|
||||
expect(err).to match(%r{submodule >= 0 could not be found in rubygems repository #{file_uri_for(gem_repo1)}/ or installed locally})
|
||||
|
||||
expect(the_bundle).not_to include_gems "has_submodule 1.0"
|
||||
end
|
||||
|
@ -371,7 +371,15 @@ RSpec.describe "bundle install with gems on multiple sources" do
|
||||
|
||||
it "fails" do
|
||||
bundle :install, :artifice => "compact_index", :raise_on_error => false
|
||||
expect(err).to include("Could not find gem 'missing', which is required by gem 'depends_on_missing', in any of the sources.")
|
||||
expect(err).to end_with <<~E.strip
|
||||
Could not find compatible versions
|
||||
|
||||
Because every version of depends_on_missing depends on missing >= 0
|
||||
and missing >= 0 could not be found in any of the sources,
|
||||
every version of depends_on_missing is forbidden.
|
||||
So, because Gemfile depends on depends_on_missing >= 0,
|
||||
version solving has failed.
|
||||
E
|
||||
end
|
||||
end
|
||||
|
||||
@ -425,9 +433,15 @@ RSpec.describe "bundle install with gems on multiple sources" do
|
||||
|
||||
it "does not find the dependency" do
|
||||
bundle :install, :artifice => "compact_index", :raise_on_error => false
|
||||
expect(err).to include(
|
||||
"Could not find gem 'rack', which is required by gem 'depends_on_rack', in rubygems repository https://gem.repo2/ or installed locally."
|
||||
)
|
||||
expect(err).to end_with <<~E.strip
|
||||
Could not find compatible versions
|
||||
|
||||
Because every version of depends_on_rack depends on rack >= 0
|
||||
and rack >= 0 could not be found in rubygems repository https://gem.repo2/ or installed locally,
|
||||
every version of depends_on_rack is forbidden.
|
||||
So, because Gemfile depends on depends_on_rack >= 0,
|
||||
version solving has failed.
|
||||
E
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -370,6 +370,16 @@ RSpec.describe "bundle install with specific platforms" do
|
||||
* sorbet-static-0.5.6433-x86_64-linux
|
||||
ERROR
|
||||
|
||||
error_message = <<~ERROR.strip
|
||||
Could not find compatible versions
|
||||
|
||||
Because every version of sorbet depends on sorbet-static = 0.5.6433
|
||||
and sorbet-static = 0.5.6433 could not be found in rubygems repository #{file_uri_for(gem_repo4)}/ or installed locally,
|
||||
every version of sorbet is forbidden.
|
||||
So, because Gemfile depends on sorbet = 0.5.6433,
|
||||
version solving has failed.
|
||||
ERROR
|
||||
|
||||
simulate_platform "arm64-darwin-21" do
|
||||
bundle "lock", :raise_on_error => false
|
||||
end
|
||||
|
@ -194,11 +194,13 @@ RSpec.describe "bundle flex_install" do
|
||||
bundle "config set force_ruby_platform true"
|
||||
|
||||
nice_error = <<-E.strip.gsub(/^ {8}/, "")
|
||||
Could not find gem 'rack (= 1.2)', which is required by gem 'rack-obama (= 2.0)', in rubygems repository #{file_uri_for(gem_repo2)}/ or installed locally.
|
||||
Could not find compatible versions
|
||||
|
||||
The source contains the following gems matching 'rack':
|
||||
* rack-0.9.1
|
||||
* rack-1.0.0
|
||||
Because rack-obama >= 2.0 depends on rack = 1.2
|
||||
and rack = 1.2 could not be found in rubygems repository #{file_uri_for(gem_repo2)}/ or installed locally,
|
||||
rack-obama >= 2.0 is forbidden.
|
||||
So, because Gemfile depends on rack-obama = 2.0,
|
||||
version solving has failed.
|
||||
E
|
||||
|
||||
bundle :install, :retry => 0, :raise_on_error => false
|
||||
|
@ -159,7 +159,7 @@ RSpec.describe "bundle install with install-time dependencies" do
|
||||
|
||||
bundle :install, :env => { "BUNDLER_DEBUG_RESOLVER" => "1", "DEBUG" => "1" }
|
||||
|
||||
expect(out).to include("BUNDLER: Starting resolution")
|
||||
expect(out).to include("Resolving dependencies...")
|
||||
end
|
||||
end
|
||||
|
||||
@ -173,7 +173,7 @@ RSpec.describe "bundle install with install-time dependencies" do
|
||||
|
||||
bundle :install, :env => { "DEBUG_RESOLVER" => "1", "DEBUG" => "1" }
|
||||
|
||||
expect(out).to include("BUNDLER: Starting resolution")
|
||||
expect(out).to include("Resolving dependencies...")
|
||||
end
|
||||
end
|
||||
|
||||
@ -187,12 +187,10 @@ RSpec.describe "bundle install with install-time dependencies" do
|
||||
|
||||
bundle :install, :env => { "DEBUG_RESOLVER_TREE" => "1", "DEBUG" => "1" }
|
||||
|
||||
activated_groups = "net_b (1.0) (ruby)"
|
||||
|
||||
expect(out).to include(" net_b").
|
||||
and include("BUNDLER: Starting resolution").
|
||||
and include("BUNDLER: Finished resolution").
|
||||
and include("Attempting to activate [#{activated_groups}]")
|
||||
and include("Resolving dependencies...").
|
||||
and include("Solution found after 1 attempts:").
|
||||
and include("selecting net_b 1.0")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -379,12 +377,12 @@ RSpec.describe "bundle install with install-time dependencies" do
|
||||
it "gives a meaningful error on ruby version mismatches between dependencies" do
|
||||
build_repo4 do
|
||||
build_gem "requires-old-ruby" do |s|
|
||||
s.required_ruby_version = "< #{RUBY_VERSION}"
|
||||
s.required_ruby_version = "< #{Gem.ruby_version}"
|
||||
end
|
||||
end
|
||||
|
||||
build_lib("foo", :path => bundled_app) do |s|
|
||||
s.required_ruby_version = ">= #{RUBY_VERSION}"
|
||||
s.required_ruby_version = ">= #{Gem.ruby_version}"
|
||||
|
||||
s.add_dependency "requires-old-ruby"
|
||||
end
|
||||
@ -394,7 +392,16 @@ RSpec.describe "bundle install with install-time dependencies" do
|
||||
gemspec
|
||||
G
|
||||
|
||||
expect(err).to include("Bundler found conflicting requirements for the Ruby\0 version:")
|
||||
expect(err).to end_with <<~E.strip
|
||||
Could not find compatible versions
|
||||
|
||||
Because every version of foo depends on requires-old-ruby >= 0
|
||||
and every version of requires-old-ruby depends on Ruby < #{Gem.ruby_version},
|
||||
every version of foo requires Ruby < #{Gem.ruby_version}.
|
||||
So, because Gemfile depends on foo >= 0
|
||||
and current Ruby version is = #{Gem.ruby_version},
|
||||
version solving has failed.
|
||||
E
|
||||
end
|
||||
|
||||
it "installs the older version under rate limiting conditions" do
|
||||
@ -464,14 +471,13 @@ RSpec.describe "bundle install with install-time dependencies" do
|
||||
expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000")
|
||||
|
||||
nice_error = strip_whitespace(<<-E).strip
|
||||
Bundler found conflicting requirements for the Ruby\0 version:
|
||||
In Gemfile:
|
||||
require_ruby was resolved to 1.0, which depends on
|
||||
Ruby\0 (> 9000)
|
||||
|
||||
Current Ruby\0 version:
|
||||
Ruby\0 (#{error_message_requirement})
|
||||
Could not find compatible versions
|
||||
|
||||
Because every version of require_ruby depends on Ruby > 9000
|
||||
and Gemfile depends on require_ruby >= 0,
|
||||
Ruby > 9000 is required.
|
||||
So, because current Ruby version is #{error_message_requirement},
|
||||
version solving has failed.
|
||||
E
|
||||
expect(err).to end_with(nice_error)
|
||||
end
|
||||
@ -487,14 +493,13 @@ RSpec.describe "bundle install with install-time dependencies" do
|
||||
expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000")
|
||||
|
||||
nice_error = strip_whitespace(<<-E).strip
|
||||
Bundler found conflicting requirements for the Ruby\0 version:
|
||||
In Gemfile:
|
||||
require_ruby was resolved to 1.0, which depends on
|
||||
Ruby\0 (> 9000)
|
||||
|
||||
Current Ruby\0 version:
|
||||
Ruby\0 (#{error_message_requirement})
|
||||
Could not find compatible versions
|
||||
|
||||
Because every version of require_ruby depends on Ruby > 9000
|
||||
and Gemfile depends on require_ruby >= 0,
|
||||
Ruby > 9000 is required.
|
||||
So, because current Ruby version is #{error_message_requirement},
|
||||
version solving has failed.
|
||||
E
|
||||
expect(err).to end_with(nice_error)
|
||||
end
|
||||
@ -532,14 +537,11 @@ RSpec.describe "bundle install with install-time dependencies" do
|
||||
|
||||
expect(err).to_not include("Gem::InstallError: require_rubygems requires RubyGems version > 9000")
|
||||
nice_error = strip_whitespace(<<-E).strip
|
||||
Bundler found conflicting requirements for the RubyGems\0 version:
|
||||
In Gemfile:
|
||||
require_rubygems was resolved to 1.0, which depends on
|
||||
RubyGems\0 (> 9000)
|
||||
|
||||
Current RubyGems\0 version:
|
||||
RubyGems\0 (= #{Gem::VERSION})
|
||||
|
||||
Because every version of require_rubygems depends on RubyGems > 9000
|
||||
and Gemfile depends on require_rubygems >= 0,
|
||||
RubyGems > 9000 is required.
|
||||
So, because current RubyGems version is = #{Gem::VERSION},
|
||||
version solving has failed.
|
||||
E
|
||||
expect(err).to end_with(nice_error)
|
||||
end
|
||||
|
@ -11,7 +11,8 @@ RSpec.describe "real world edgecases", :realworld => true do
|
||||
source = Bundler::Source::Rubygems::Remote.new(Bundler::URI("https://rubygems.org"))
|
||||
fetcher = Bundler::Fetcher.new(source)
|
||||
index = fetcher.specs([#{name.dump}], nil)
|
||||
index.search(Gem::Dependency.new(#{name.dump}, #{requirement.dump})).last
|
||||
requirement = Gem::Requirement.create(#{requirement.dump})
|
||||
index.search(#{name.dump}).select {|spec| requirement.satisfied_by?(spec.version) }.last
|
||||
end
|
||||
if rubygem.nil?
|
||||
raise "Could not find #{name} (#{requirement}) on rubygems.org!\n" \
|
||||
|
@ -104,7 +104,7 @@ RSpec.describe "Resolving" do
|
||||
dep "chef_app_error"
|
||||
expect do
|
||||
resolve
|
||||
end.to raise_error(Bundler::VersionConflict)
|
||||
end.to raise_error(Bundler::SolveFailure)
|
||||
end
|
||||
|
||||
it "raises an exception with the minimal set of conflicting dependencies" do
|
||||
@ -118,14 +118,15 @@ RSpec.describe "Resolving" do
|
||||
dep "c"
|
||||
expect do
|
||||
resolve
|
||||
end.to raise_error(Bundler::VersionConflict, <<-E.strip)
|
||||
Bundler could not find compatible versions for gem "a":
|
||||
In Gemfile:
|
||||
b was resolved to 1.0, which depends on
|
||||
a (>= 2)
|
||||
end.to raise_error(Bundler::SolveFailure, <<~E.strip)
|
||||
Could not find compatible versions
|
||||
|
||||
c was resolved to 1.0, which depends on
|
||||
a (< 1)
|
||||
Because every version of c depends on a < 1
|
||||
and every version of b depends on a >= 2,
|
||||
every version of c is incompatible with b >= 0.
|
||||
So, because Gemfile depends on b >= 0
|
||||
and Gemfile depends on c >= 0,
|
||||
version solving has failed.
|
||||
E
|
||||
end
|
||||
|
||||
@ -134,7 +135,7 @@ Bundler could not find compatible versions for gem "a":
|
||||
dep "circular_app"
|
||||
|
||||
expect do
|
||||
resolve
|
||||
Bundler::SpecSet.new(resolve).sort
|
||||
end.to raise_error(Bundler::CyclicDependencyError, /please remove either gem 'bar' or gem 'foo'/i)
|
||||
end
|
||||
|
||||
|
@ -210,39 +210,6 @@ RSpec.describe "Resolving platform craziness" do
|
||||
should_resolve_as %w[foo-1.1.0]
|
||||
end
|
||||
|
||||
it "doesn't include gems not needed for none of the platforms" do
|
||||
@index = build_index do
|
||||
gem "empyrean", "0.1.0"
|
||||
gem "coderay", "1.1.2"
|
||||
gem "method_source", "0.9.0"
|
||||
|
||||
gem "spoon", "0.0.6" do
|
||||
dep "ffi", ">= 0"
|
||||
end
|
||||
|
||||
gem "pry", "0.11.3", "java" do
|
||||
dep "coderay", "~> 1.1.0"
|
||||
dep "method_source", "~> 0.9.0"
|
||||
dep "spoon", "~> 0.0"
|
||||
end
|
||||
|
||||
gem "pry", "0.11.3" do
|
||||
dep "coderay", "~> 1.1.0"
|
||||
dep "method_source", "~> 0.9.0"
|
||||
end
|
||||
|
||||
gem "ffi", "1.9.23", "java"
|
||||
gem "ffi", "1.9.23"
|
||||
end
|
||||
|
||||
dep "empyrean", "0.1.0"
|
||||
dep "pry"
|
||||
|
||||
platforms "ruby", "java"
|
||||
|
||||
should_resolve_as %w[coderay-1.1.2 empyrean-0.1.0 ffi-1.9.23-java method_source-0.9.0 pry-0.11.3 pry-0.11.3-java spoon-0.0.6]
|
||||
end
|
||||
|
||||
it "includes gems needed for at least one platform" do
|
||||
@index = build_index do
|
||||
gem "empyrean", "0.1.0"
|
||||
|
@ -96,12 +96,14 @@ RSpec.describe "bundler/inline#gemfile" do
|
||||
it "lets me use my own ui object" do
|
||||
script <<-RUBY, :artifice => "endpoint"
|
||||
require '#{entrypoint}'
|
||||
class MyBundlerUI < Bundler::UI::Silent
|
||||
class MyBundlerUI < Bundler::UI::Shell
|
||||
def confirm(msg, newline = nil)
|
||||
puts "CONFIRMED!"
|
||||
end
|
||||
end
|
||||
gemfile(true, :ui => MyBundlerUI.new) do
|
||||
my_ui = MyBundlerUI.new
|
||||
my_ui.level = "confirm"
|
||||
gemfile(true, :ui => my_ui) do
|
||||
source "https://notaserver.com"
|
||||
gem "activesupport", :require => true
|
||||
end
|
||||
|
@ -18,15 +18,22 @@ module Spec
|
||||
@platforms ||= ["ruby"]
|
||||
default_source = instance_double("Bundler::Source::Rubygems", :specs => @index, :to_s => "locally install gems")
|
||||
source_requirements = { :default => default_source }
|
||||
@deps.each do |d|
|
||||
source_requirements[d.name] = d.source = default_source
|
||||
end
|
||||
args[0] ||= Bundler::SpecSet.new([]) # base
|
||||
args[0].each {|ls| ls.source = default_source }
|
||||
args[1] ||= Bundler::GemVersionPromoter.new # gem_version_promoter
|
||||
args[2] ||= [] # additional_base_requirements
|
||||
args[3] ||= @platforms # platforms
|
||||
Bundler::Resolver.new(source_requirements, *args).start(@deps)
|
||||
originally_locked = args[3] || Bundler::SpecSet.new([])
|
||||
unlock = args[4] || []
|
||||
packages = Hash.new do |h, k|
|
||||
h[k] = Bundler::Resolver::Package.new(k, @platforms, originally_locked, unlock)
|
||||
end
|
||||
@deps.each do |d|
|
||||
name = d.name
|
||||
platforms = d.gem_platforms(@platforms)
|
||||
source_requirements[name] = d.source = default_source
|
||||
packages[name] = Bundler::Resolver::Package.new(name, platforms, originally_locked, unlock, :dependency => d)
|
||||
end
|
||||
Bundler::Resolver.new(source_requirements, *args[0..2]).start(@deps, packages)
|
||||
end
|
||||
|
||||
def should_not_resolve
|
||||
@ -47,13 +54,6 @@ module Spec
|
||||
end
|
||||
end
|
||||
|
||||
def should_conflict_on(names)
|
||||
got = resolve
|
||||
raise "The resolve succeeded with: #{got.map(&:full_name).sort.inspect}"
|
||||
rescue Bundler::VersionConflict => e
|
||||
expect(Array(names).sort).to eq(e.conflicts.sort)
|
||||
end
|
||||
|
||||
def gem(*args, &blk)
|
||||
build_spec(*args, &blk).first
|
||||
end
|
||||
@ -67,12 +67,11 @@ module Spec
|
||||
def should_conservative_resolve_and_include(opts, unlock, specs)
|
||||
# empty unlock means unlock all
|
||||
opts = Array(opts)
|
||||
search = Bundler::GemVersionPromoter.new(@locked, unlock).tap do |s|
|
||||
search = Bundler::GemVersionPromoter.new.tap do |s|
|
||||
s.level = opts.first
|
||||
s.strict = opts.include?(:strict)
|
||||
s.prerelease_specified = Hash[@deps.map {|d| [d.name, d.requirement.prerelease?] }]
|
||||
end
|
||||
should_resolve_and_include specs, [@base, search]
|
||||
should_resolve_and_include specs, [@base, search, [], @locked, unlock]
|
||||
end
|
||||
|
||||
def an_awesome_index
|
||||
|
Loading…
x
Reference in New Issue
Block a user