diff options
Diffstat (limited to 'lib/bundler/resolver')
| -rw-r--r-- | lib/bundler/resolver/base.rb | 119 | ||||
| -rw-r--r-- | lib/bundler/resolver/candidate.rb | 85 | ||||
| -rw-r--r-- | lib/bundler/resolver/incompatibility.rb | 15 | ||||
| -rw-r--r-- | lib/bundler/resolver/package.rb | 95 | ||||
| -rw-r--r-- | lib/bundler/resolver/root.rb | 25 | ||||
| -rw-r--r-- | lib/bundler/resolver/spec_group.rb | 74 | ||||
| -rw-r--r-- | lib/bundler/resolver/strategy.rb | 43 |
7 files changed, 456 insertions, 0 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 new file mode 100644 index 0000000000..ac6ba86c4c --- /dev/null +++ b/lib/bundler/resolver/spec_group.rb @@ -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 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 |
