diff options
Diffstat (limited to 'lib/bundler/resolver')
-rw-r--r-- | lib/bundler/resolver/base.rb | 107 | ||||
-rw-r--r-- | lib/bundler/resolver/candidate.rb | 94 | ||||
-rw-r--r-- | lib/bundler/resolver/incompatibility.rb | 15 | ||||
-rw-r--r-- | lib/bundler/resolver/package.rb | 77 | ||||
-rw-r--r-- | lib/bundler/resolver/root.rb | 25 | ||||
-rw-r--r-- | lib/bundler/resolver/spec_group.rb | 111 |
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 |