summaryrefslogtreecommitdiff
path: root/lib/bundler/resolver
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bundler/resolver')
-rw-r--r--lib/bundler/resolver/base.rb119
-rw-r--r--lib/bundler/resolver/candidate.rb85
-rw-r--r--lib/bundler/resolver/incompatibility.rb15
-rw-r--r--lib/bundler/resolver/package.rb95
-rw-r--r--lib/bundler/resolver/root.rb25
-rw-r--r--lib/bundler/resolver/spec_group.rb112
-rw-r--r--lib/bundler/resolver/strategy.rb43
7 files changed, 422 insertions, 72 deletions
diff --git a/lib/bundler/resolver/base.rb b/lib/bundler/resolver/base.rb
new file mode 100644
index 0000000000..00bdd08303
--- /dev/null
+++ b/lib/bundler/resolver/base.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require_relative "package"
+
+module Bundler
+ class Resolver
+ class Base
+ attr_reader :packages, :requirements, :source_requirements, :locked_specs, :overrides
+
+ def initialize(source_requirements, dependencies, base, platforms, options)
+ @overrides = options.delete(:overrides) || []
+ @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
+
+ 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
diff --git a/lib/bundler/resolver/candidate.rb b/lib/bundler/resolver/candidate.rb
new file mode 100644
index 0000000000..5298b2530f
--- /dev/null
+++ b/lib/bundler/resolver/candidate.rb
@@ -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
diff --git a/lib/bundler/resolver/incompatibility.rb b/lib/bundler/resolver/incompatibility.rb
new file mode 100644
index 0000000000..4ac1b2e1ea
--- /dev/null
+++ b/lib/bundler/resolver/incompatibility.rb
@@ -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
diff --git a/lib/bundler/resolver/package.rb b/lib/bundler/resolver/package.rb
new file mode 100644
index 0000000000..3906be3f57
--- /dev/null
+++ b/lib/bundler/resolver/package.rb
@@ -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
diff --git a/lib/bundler/resolver/root.rb b/lib/bundler/resolver/root.rb
new file mode 100644
index 0000000000..e5eb634fb8
--- /dev/null
+++ b/lib/bundler/resolver/root.rb
@@ -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
diff --git a/lib/bundler/resolver/spec_group.rb b/lib/bundler/resolver/spec_group.rb
index 34d043aed7..ac6ba86c4c 100644
--- a/lib/bundler/resolver/spec_group.rb
+++ b/lib/bundler/resolver/spec_group.rb
@@ -3,103 +3,71 @@
module Bundler
class Resolver
class SpecGroup
- include GemHelpers
+ attr_reader :specs
- attr_accessor :name, :version, :source
- attr_accessor :ignores_bundler_dependencies
+ def initialize(specs)
+ @specs = specs
+ end
- def initialize(all_specs)
- raise ArgumentError, "cannot initialize with an empty value" unless exemplary_spec = all_specs.first
- @name = exemplary_spec.name
- @version = exemplary_spec.version
- @source = exemplary_spec.source
+ def empty?
+ @specs.empty?
+ end
- @activated_platforms = []
- @dependencies = nil
- @specs = Hash.new do |specs, platform|
- specs[platform] = select_best_platform_match(all_specs, platform)
- end
- @ignores_bundler_dependencies = true
+ def name
+ @name ||= exemplary_spec.name
end
- def to_specs
- @activated_platforms.map do |p|
- next unless s = @specs[p]
- lazy_spec = LazySpecification.new(name, version, s.platform, source)
- lazy_spec.dependencies.replace s.dependencies
- lazy_spec
- end.compact
+ def version
+ @version ||= exemplary_spec.version
end
- def activate_platform!(platform)
- return unless for?(platform)
- return if @activated_platforms.include?(platform)
- @activated_platforms << platform
+ def source
+ @source ||= exemplary_spec.source
end
- def for?(platform)
- spec = @specs[platform]
- !spec.nil?
+ 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
- @to_s ||= "#{name} (#{version})"
+ sorted_spec_names.join(", ")
end
- def dependencies_for_activated_platforms
- dependencies = @activated_platforms.map {|p| __dependencies[p] }
- metadata_dependencies = @activated_platforms.map do |platform|
- metadata_dependencies(@specs[platform], platform)
- end
- dependencies.concat(metadata_dependencies).flatten
+ def dependencies
+ @dependencies ||= @specs.flat_map(&:expanded_dependencies).uniq.sort
end
def ==(other)
- return unless other.is_a?(SpecGroup)
- name == other.name &&
- version == other.version &&
- source == other.source
+ sorted_spec_names == other.sorted_spec_names
end
- def eql?(other)
- name.eql?(other.name) &&
- version.eql?(other.version) &&
- source.eql?(other.source)
+ def merge(other)
+ return false unless equivalent?(other)
+
+ @specs |= other.specs
+
+ true
end
- def hash
- to_s.hash ^ source.hash
+ protected
+
+ def sorted_spec_names
+ @specs.map(&:full_name).sort
end
- private
-
- def __dependencies
- @dependencies = Hash.new do |dependencies, platform|
- dependencies[platform] = []
- if spec = @specs[platform]
- spec.dependencies.each do |dep|
- next if dep.type == :development
- next if @ignores_bundler_dependencies && dep.name == "bundler".freeze
- dependencies[platform] << DepProxy.new(dep, platform)
- end
- end
- dependencies[platform]
- end
+ private
+
+ def equivalent?(other)
+ name == other.name && version == other.version && source == other.source && dependencies == other.dependencies
end
- def metadata_dependencies(spec, platform)
- return [] unless spec
- # Only allow endpoint specifications since they won't hit the network to
- # fetch the full gemspec when calling required_ruby_version
- return [] if !spec.is_a?(EndpointSpecification) && !spec.is_a?(Gem::Specification)
- dependencies = []
- if !spec.required_ruby_version.nil? && !spec.required_ruby_version.none?
- dependencies << DepProxy.new(Gem::Dependency.new("ruby\0", spec.required_ruby_version), platform)
- end
- if !spec.required_rubygems_version.nil? && !spec.required_rubygems_version.none?
- dependencies << DepProxy.new(Gem::Dependency.new("rubygems\0", spec.required_rubygems_version), platform)
- end
- dependencies
+ def exemplary_spec
+ @specs.first
end
end
end
diff --git a/lib/bundler/resolver/strategy.rb b/lib/bundler/resolver/strategy.rb
new file mode 100644
index 0000000000..7519d38968
--- /dev/null
+++ b/lib/bundler/resolver/strategy.rb
@@ -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