This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "package"
|
||||
|
||||
module Bundler
|
||||
class Resolver
|
||||
class Base
|
||||
attr_reader :packages, :requirements, :source_requirements, :locked_specs
|
||||
|
||||
def initialize(source_requirements, dependencies, base, platforms, options)
|
||||
@explicit_unlocks = options.delete(:explicit_unlocks) || []
|
||||
@source_requirements = source_requirements
|
||||
@locked_specs = options[:locked_specs]
|
||||
|
||||
@base = base
|
||||
|
||||
@packages = Hash.new do |hash, name|
|
||||
hash[name] = Package.new(name, platforms, **options)
|
||||
end
|
||||
|
||||
@requirements = dependencies.filter_map do |dep|
|
||||
dep_platforms = dep.gem_platforms(platforms)
|
||||
|
||||
# Dependencies scoped to external platforms are ignored
|
||||
next if dep_platforms.empty?
|
||||
|
||||
name = dep.name
|
||||
|
||||
@packages[name] = Package.new(name, dep_platforms, **options.merge(dependency: dep))
|
||||
|
||||
dep
|
||||
end
|
||||
end
|
||||
|
||||
def [](name)
|
||||
@base[name]
|
||||
end
|
||||
|
||||
def delete(specs)
|
||||
@base.delete(specs)
|
||||
end
|
||||
|
||||
def get_package(name)
|
||||
@packages[name]
|
||||
end
|
||||
|
||||
# Gems the user named on a `bundle update GEM` / `bundle lock --update GEM`
|
||||
# command line. These are the only ones meant to move off their locked
|
||||
# version, so cooldown keeps applying to them while every other locked gem
|
||||
# stays exempt.
|
||||
def explicitly_unlocked?(name)
|
||||
@explicit_unlocks.include?(name)
|
||||
end
|
||||
|
||||
def base_requirements
|
||||
@base_requirements ||= build_base_requirements
|
||||
end
|
||||
|
||||
def unlock_names(names)
|
||||
indirect_pins = indirect_pins(names)
|
||||
|
||||
if indirect_pins.any?
|
||||
loosen_names(indirect_pins)
|
||||
else
|
||||
pins = pins(names)
|
||||
|
||||
if pins.any?
|
||||
loosen_names(pins)
|
||||
else
|
||||
unrestrict_names(names)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def include_prereleases(names)
|
||||
names.each do |name|
|
||||
get_package(name).consider_prereleases!
|
||||
end
|
||||
end
|
||||
|
||||
def include_remote_specs(names)
|
||||
names.each do |name|
|
||||
get_package(name).consider_remote_versions!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def indirect_pins(names)
|
||||
names.select {|name| @base_requirements[name].exact? && @requirements.none? {|dep| dep.name == name } }
|
||||
end
|
||||
|
||||
def pins(names)
|
||||
names.select {|name| @base_requirements[name].exact? }
|
||||
end
|
||||
|
||||
def loosen_names(names)
|
||||
names.each do |name|
|
||||
version = @base_requirements[name].requirements.first[1]
|
||||
|
||||
@base_requirements[name] = Gem::Requirement.new(">= #{version}")
|
||||
|
||||
@base.delete_by_name(name)
|
||||
end
|
||||
end
|
||||
|
||||
def unrestrict_names(names)
|
||||
names.each do |name|
|
||||
@base_requirements.delete(name)
|
||||
end
|
||||
end
|
||||
|
||||
def build_base_requirements
|
||||
base_requirements = {}
|
||||
@base.each do |ls|
|
||||
if ls.source_changed? && ls.source.specs.search(ls.name).empty?
|
||||
raise GemNotFound, "Could not find gem '#{ls.name}' in #{ls.source}"
|
||||
end
|
||||
|
||||
req = Gem::Requirement.new(ls.version)
|
||||
base_requirements[ls.name] = req
|
||||
end
|
||||
base_requirements
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,85 @@
|
||||
# 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 they 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 lockfiles, and so on.
|
||||
#
|
||||
class Candidate
|
||||
include Comparable
|
||||
|
||||
attr_reader :version
|
||||
|
||||
def initialize(version, group: nil, priority: -1)
|
||||
@spec_group = group || SpecGroup.new([])
|
||||
@version = Gem::Version.new(version)
|
||||
@priority = priority
|
||||
end
|
||||
|
||||
def dependencies
|
||||
@spec_group.dependencies
|
||||
end
|
||||
|
||||
def to_specs(package, most_specific_locked_platform)
|
||||
return [] if package.meta?
|
||||
|
||||
@spec_group.to_specs(package.force_ruby_platform?, most_specific_locked_platform)
|
||||
end
|
||||
|
||||
def prerelease?
|
||||
@version.prerelease?
|
||||
end
|
||||
|
||||
def segments
|
||||
@version.segments
|
||||
end
|
||||
|
||||
def <=>(other)
|
||||
return unless other.is_a?(self.class)
|
||||
|
||||
version_comparison = version <=> other.version
|
||||
return version_comparison unless version_comparison.zero?
|
||||
|
||||
priority <=> other.priority
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
return unless other.is_a?(self.class)
|
||||
|
||||
version == other.version && priority == other.priority
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
return unless other.is_a?(self.class)
|
||||
|
||||
version.eql?(other.version) && priority.eql?(other.priority)
|
||||
end
|
||||
|
||||
def hash
|
||||
[@version, @priority].hash
|
||||
end
|
||||
|
||||
def to_s
|
||||
@version.to_s
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :priority
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Resolver
|
||||
class Incompatibility < PubGrub::Incompatibility
|
||||
attr_reader :extended_explanation
|
||||
|
||||
def initialize(terms, cause:, custom_explanation: nil, extended_explanation: nil)
|
||||
@extended_explanation = extended_explanation
|
||||
|
||||
super(terms, cause: cause, custom_explanation: custom_explanation)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,95 @@
|
||||
# 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, :locked_version
|
||||
|
||||
def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, prefer_local: false, dependency: nil, new_platforms: [])
|
||||
@name = name
|
||||
@platforms = platforms
|
||||
@locked_version = locked_specs.version_for(name)
|
||||
@unlock = unlock
|
||||
@dependency = dependency || Dependency.new(name, @locked_version)
|
||||
@platforms |= [Gem::Platform::RUBY] if @dependency.default_force_ruby_platform
|
||||
@top_level = !dependency.nil?
|
||||
@prerelease = @dependency.prerelease? || @locked_version&.prerelease? || prerelease ? :consider_first : :ignore
|
||||
@prefer_local = prefer_local
|
||||
@new_platforms = new_platforms
|
||||
end
|
||||
|
||||
def platform_specs(specs)
|
||||
platforms.map do |platform|
|
||||
prefer_locked = @new_platforms.include?(platform) ? false : !unlock?
|
||||
MatchPlatform.select_best_platform_match(specs, platform, prefer_locked: prefer_locked)
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
@name.delete("\0")
|
||||
end
|
||||
|
||||
def root?
|
||||
false
|
||||
end
|
||||
|
||||
def top_level?
|
||||
@top_level
|
||||
end
|
||||
|
||||
def meta?
|
||||
@name.end_with?("\0")
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
self.class == other.class && @name == other.name
|
||||
end
|
||||
|
||||
def hash
|
||||
@name.hash
|
||||
end
|
||||
|
||||
def unlock?
|
||||
@unlock == true || @unlock.include?(name)
|
||||
end
|
||||
|
||||
def ignores_prereleases?
|
||||
@prerelease == :ignore
|
||||
end
|
||||
|
||||
def prerelease_specified?
|
||||
@prerelease == :consider_first
|
||||
end
|
||||
|
||||
def consider_prereleases!
|
||||
@prerelease = :consider_last
|
||||
end
|
||||
|
||||
def prefer_local?
|
||||
@prefer_local
|
||||
end
|
||||
|
||||
def consider_remote_versions!
|
||||
@prefer_local = false
|
||||
end
|
||||
|
||||
def force_ruby_platform?
|
||||
@dependency.force_ruby_platform
|
||||
end
|
||||
|
||||
def current_platform?
|
||||
@dependency.current_platform?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Resolver
|
||||
class SpecGroup
|
||||
attr_reader :specs
|
||||
|
||||
def initialize(specs)
|
||||
@specs = specs
|
||||
end
|
||||
|
||||
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, most_specific_locked_platform)
|
||||
@specs.map do |s|
|
||||
lazy_spec = LazySpecification.from_spec(s)
|
||||
lazy_spec.force_ruby_platform = force_ruby_platform
|
||||
lazy_spec.most_specific_locked_platform = most_specific_locked_platform
|
||||
lazy_spec
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
sorted_spec_names.join(", ")
|
||||
end
|
||||
|
||||
def dependencies
|
||||
@dependencies ||= @specs.flat_map(&:expanded_dependencies).uniq.sort
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
sorted_spec_names == other.sorted_spec_names
|
||||
end
|
||||
|
||||
def merge(other)
|
||||
return false unless equivalent?(other)
|
||||
|
||||
@specs |= other.specs
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def sorted_spec_names
|
||||
@specs.map(&:full_name).sort
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def equivalent?(other)
|
||||
name == other.name && version == other.version && source == other.source && dependencies == other.dependencies
|
||||
end
|
||||
|
||||
def exemplary_spec
|
||||
@specs.first
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Resolver
|
||||
class Strategy
|
||||
def initialize(source)
|
||||
@source = source
|
||||
@package_priority_cache = {}
|
||||
end
|
||||
|
||||
def next_package_and_version(unsatisfied)
|
||||
package, range = next_term_to_try_from(unsatisfied)
|
||||
|
||||
[package, most_preferred_version_of(package, range).first]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def next_term_to_try_from(unsatisfied)
|
||||
unsatisfied.min_by do |package, range|
|
||||
@package_priority_cache[[package, range]] ||= begin
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
def most_preferred_version_of(package, range)
|
||||
versions = @source.versions_for(package, range)
|
||||
|
||||
# Conditional avoids (among other things) calling
|
||||
# sort_versions_by_preferred with the root package
|
||||
if versions.size > 1
|
||||
@source.sort_versions_by_preferred(package, versions)
|
||||
else
|
||||
versions
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user