summaryrefslogtreecommitdiff
path: root/lib/bundler/resolver
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bundler/resolver')
-rw-r--r--lib/bundler/resolver/base.rb107
-rw-r--r--lib/bundler/resolver/candidate.rb94
-rw-r--r--lib/bundler/resolver/incompatibility.rb15
-rw-r--r--lib/bundler/resolver/package.rb77
-rw-r--r--lib/bundler/resolver/root.rb25
-rw-r--r--lib/bundler/resolver/spec_group.rb111
6 files changed, 358 insertions, 71 deletions
diff --git a/lib/bundler/resolver/base.rb b/lib/bundler/resolver/base.rb
new file mode 100644
index 0000000000..ad19eeb3f4
--- /dev/null
+++ b/lib/bundler/resolver/base.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+require_relative "package"
+
+module Bundler
+ class Resolver
+ class Base
+ attr_reader :packages, :requirements, :source_requirements
+
+ def initialize(source_requirements, dependencies, base, platforms, options)
+ @source_requirements = source_requirements
+
+ @base = base
+
+ @packages = Hash.new do |hash, name|
+ hash[name] = Package.new(name, platforms, **options)
+ end
+
+ @requirements = dependencies.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.compact
+ 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
+
+ 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|
+ 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..9e8b913335
--- /dev/null
+++ b/lib/bundler/resolver/candidate.rb
@@ -0,0 +1,94 @@
+# 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 lock files, and so on.
+ #
+ class Candidate
+ include Comparable
+
+ attr_reader :version
+
+ def initialize(version, specs: [])
+ @spec_group = Resolver::SpecGroup.new(specs)
+ @version = Gem::Version.new(version)
+ @ruby_only = specs.map(&:platform).uniq == [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 generic!
+ @ruby_only = true
+
+ self
+ end
+
+ def platform_specific!
+ @ruby_only = false
+
+ self
+ end
+
+ def prerelease?
+ @version.prerelease?
+ end
+
+ def segments
+ @version.segments
+ end
+
+ def sort_obj
+ [@version, @ruby_only ? -1 : 1]
+ end
+
+ def <=>(other)
+ return unless other.is_a?(self.class)
+
+ sort_obj <=> other.sort_obj
+ end
+
+ def ==(other)
+ return unless other.is_a?(self.class)
+
+ sort_obj == other.sort_obj
+ end
+
+ def eql?(other)
+ return unless other.is_a?(self.class)
+
+ sort_obj.eql?(other.sort_obj)
+ end
+
+ def hash
+ sort_obj.hash
+ end
+
+ def to_s
+ @version.to_s
+ end
+ 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..0461328683
--- /dev/null
+++ b/lib/bundler/resolver/package.rb
@@ -0,0 +1,77 @@
+# 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, dependency: nil)
+ @name = name
+ @platforms = platforms
+ @locked_version = locked_specs[name].first&.version
+ @unlock = unlock
+ @dependency = dependency || Dependency.new(name, @locked_version)
+ @top_level = !dependency.nil?
+ @prerelease = @dependency.prerelease? || @locked_version&.prerelease? || prerelease ? :consider_first : :ignore
+ 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.empty? || @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 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 232520de77..5cee444e5e 100644
--- a/lib/bundler/resolver/spec_group.rb
+++ b/lib/bundler/resolver/spec_group.rb
@@ -3,107 +3,76 @@
module Bundler
class Resolver
class SpecGroup
- attr_accessor :name, :version, :source
- attr_accessor :activated_platforms
-
- def self.create_for(specs, all_platforms, specific_platform)
- specific_platform_specs = specs[specific_platform]
- return unless specific_platform_specs.any?
-
- platforms = all_platforms.select {|p| specs[p].any? }
-
- new(specific_platform_specs.first, specs, platforms)
+ def initialize(specs)
+ @specs = specs
end
- def initialize(exemplary_spec, specs, relevant_platforms)
- @exemplary_spec = exemplary_spec
- @name = exemplary_spec.name
- @version = exemplary_spec.version
- @source = exemplary_spec.source
-
- @activated_platforms = relevant_platforms
- @dependencies = Hash.new do |dependencies, platforms|
- dependencies[platforms] = dependencies_for(platforms)
- end
- @specs = specs
+ def empty?
+ @specs.empty?
end
- def to_specs
- activated_platforms.map do |p|
- specs = @specs[p]
- next unless specs.any?
-
- specs.map do |s|
- lazy_spec = LazySpecification.new(name, version, s.platform, source)
- lazy_spec.dependencies.replace s.dependencies
- lazy_spec
- end
- end.flatten.compact.uniq
+ def name
+ @name ||= exemplary_spec.name
end
- def to_s
- activated_platforms_string = sorted_activated_platforms.join(", ")
- "#{name} (#{version}) (#{activated_platforms_string})"
+ def version
+ @version ||= exemplary_spec.version
end
- def dependencies_for_activated_platforms
- @dependencies[activated_platforms]
+ def source
+ @source ||= exemplary_spec.source
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
+ def to_specs(force_ruby_platform)
+ @specs.map do |s|
+ lazy_spec = LazySpecification.from_spec(s)
+ lazy_spec.force_ruby_platform = force_ruby_platform
+ lazy_spec
+ end
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)
+ def to_s
+ sorted_spec_names.join(", ")
end
- def hash
- name.hash ^ version.hash ^ sorted_activated_platforms.hash ^ source.hash
+ def dependencies
+ @dependencies ||= @specs.map do |spec|
+ __dependencies(spec) + metadata_dependencies(spec)
+ end.flatten.uniq
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 dependencies_for(platforms)
- platforms.map do |platform|
- __dependencies(platform) + metadata_dependencies(platform)
- end.flatten
+ def exemplary_spec
+ @specs.first
end
- def __dependencies(platform)
+ def __dependencies(spec)
dependencies = []
- @specs[platform].first.dependencies.each do |dep|
+ spec.dependencies.each do |dep|
next if dep.type == :development
- dependencies << DepProxy.get_proxy(dep, platform)
+ dependencies << Dependency.new(dep.name, dep.requirement)
end
dependencies
end
- def metadata_dependencies(platform)
- spec = @specs[platform].first
- return [] if spec.is_a?(LazySpecification)
- dependencies = []
- if !spec.required_ruby_version.nil? && !spec.required_ruby_version.none?
- dependencies << DepProxy.get_proxy(Gem::Dependency.new("Ruby\0", spec.required_ruby_version), platform)
- end
- if !spec.required_rubygems_version.nil? && !spec.required_rubygems_version.none?
- dependencies << DepProxy.get_proxy(Gem::Dependency.new("RubyGems\0", spec.required_rubygems_version), platform)
- end
- dependencies
+ def metadata_dependencies(spec)
+ [
+ metadata_dependency("Ruby", spec.required_ruby_version),
+ metadata_dependency("RubyGems", spec.required_rubygems_version),
+ ].compact
+ end
+
+ def metadata_dependency(name, requirement)
+ return if requirement.nil? || requirement.none?
+
+ Dependency.new("#{name}\0", requirement)
end
end
end