Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate our resolver engine to PubGrub #6718

Merged
merged 1 commit into from Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion lib/bundler.rb
Expand Up @@ -75,7 +75,6 @@ module Bundler
autoload :StubSpecification, File.expand_path("bundler/stub_specification", __dir__)
autoload :UI, File.expand_path("bundler/ui", __dir__)
autoload :URICredentialsFilter, File.expand_path("bundler/uri_credentials_filter", __dir__)
autoload :VersionRanges, File.expand_path("bundler/version_ranges", __dir__)

class << self
def configure
Expand Down
2 changes: 1 addition & 1 deletion lib/bundler/cli/check.rb
Expand Up @@ -17,7 +17,7 @@ def run
begin
definition.resolve_only_locally!
not_installed = definition.missing_specs
rescue GemNotFound, VersionConflict
rescue GemNotFound, SolveFailure
Bundler.ui.error "Bundler can't satisfy your Gemfile's dependencies."
Bundler.ui.warn "Install missing gems with `bundle install`."
exit 1
Expand Down
6 changes: 3 additions & 3 deletions lib/bundler/cli/lock.rb
Expand Up @@ -15,8 +15,8 @@ def run
end

print = options[:print]
ui = Bundler.ui
Bundler.ui = UI::Silent.new if print
previous_ui_level = Bundler.ui.level
Bundler.ui.level = "silent" if print

Bundler::Fetcher.disable_endpoint = options["full-index"]

Expand Down Expand Up @@ -61,7 +61,7 @@ def run
definition.lock(file)
end

Bundler.ui = ui
Bundler.ui.level = previous_ui_level
end
end
end
35 changes: 29 additions & 6 deletions lib/bundler/definition.rb
Expand Up @@ -150,7 +150,7 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti
end

def gem_version_promoter
@gem_version_promoter ||= GemVersionPromoter.new(@originally_locked_specs, @unlock[:gems])
@gem_version_promoter ||= GemVersionPromoter.new
end

def resolve_only_locally!
Expand Down Expand Up @@ -276,7 +276,7 @@ def resolve
end
else
Bundler.ui.debug("Found changes from the lockfile, re-resolving dependencies because #{change_reason}")
resolver.start(expanded_dependencies)
start_resolution
end
end

Expand All @@ -299,7 +299,7 @@ def lock(file, preserve_unknown_sections = false)

if @locked_bundler_version
locked_major = @locked_bundler_version.segments.first
current_major = Gem::Version.create(Bundler::VERSION).segments.first
current_major = Bundler.gem_version.segments.first

updating_major = locked_major < current_major
end
Expand Down Expand Up @@ -474,14 +474,31 @@ def resolver
@resolver ||= begin
last_resolve = converge_locked_specs
remove_ruby_from_platforms_if_necessary!(current_dependencies)
Resolver.new(source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve(last_resolve), platforms)
Resolver.new(source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve(last_resolve))
end
end

def expanded_dependencies
@expanded_dependencies ||= dependencies + metadata_dependencies
end

def resolution_packages
@resolution_packages ||= begin
packages = Hash.new do |h, k|
h[k] = Resolver::Package.new(k, @platforms, @originally_locked_specs, @unlock[:gems])
end

expanded_dependencies.each do |dep|
name = dep.name
platforms = dep.gem_platforms(@platforms)

packages[name] = Resolver::Package.new(name, platforms, @originally_locked_specs, @unlock[:gems], :dependency => dep)
end

packages
end
end

def filter_specs(specs, deps)
SpecSet.new(specs).for(deps, false, platforms)
end
Expand Down Expand Up @@ -512,16 +529,22 @@ def materialize(dependencies)
break if incomplete_specs.empty?

Bundler.ui.debug("The lockfile does not have all gems needed for the current platform though, Bundler will still re-resolve dependencies")
@resolve = resolver.start(expanded_dependencies, :exclude_specs => incomplete_specs)
@resolve = start_resolution(:exclude_specs => incomplete_specs)
specs = resolve.materialize(dependencies)
end

bundler = sources.metadata_source.specs.search(Gem::Dependency.new("bundler", VERSION)).last
bundler = sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last
specs["bundler"] = bundler

specs
end

def start_resolution(exclude_specs: [])
result = resolver.start(expanded_dependencies, resolution_packages, :exclude_specs => exclude_specs)

SpecSet.new(SpecSet.new(result).for(dependencies, false, @platforms))
end

def precompute_source_requirements_for_indirect_dependencies?
@remote && sources.non_global_rubygems_sources.all?(&:dependency_api_available?) && !sources.aggregate_global_source?
end
Expand Down
1 change: 1 addition & 0 deletions lib/bundler/dependency.rb
Expand Up @@ -50,6 +50,7 @@ def initialize(name, version, options = {}, &blk)
# Returns the platforms this dependency is valid for, in the same order as
# passed in the `valid_platforms` parameter
def gem_platforms(valid_platforms)
return [Gem::Platform::RUBY] if @force_ruby_platform
return valid_platforms if @platforms.empty?

valid_platforms.select {|p| expanded_platforms.include?(GemHelpers.generic(p)) }
Expand Down
11 changes: 1 addition & 10 deletions lib/bundler/errors.rb
Expand Up @@ -21,16 +21,7 @@ class GemfileError < BundlerError; status_code(4); end
class InstallError < BundlerError; status_code(5); end

# Internal error, should be rescued
class VersionConflict < BundlerError
attr_reader :conflicts

def initialize(conflicts, msg = nil)
super(msg)
@conflicts = conflicts
end

status_code(6)
end
class SolveFailure < BundlerError; status_code(6); end

class GemNotFound < BundlerError; status_code(7); end
class InstallHookError < BundlerError; status_code(8); end
Expand Down
121 changes: 42 additions & 79 deletions lib/bundler/gem_version_promoter.rb
Expand Up @@ -7,9 +7,7 @@ module Bundler
# available dependency versions as found in its index, before returning it to
# to the resolution engine to select the best version.
class GemVersionPromoter
DEBUG = ENV["BUNDLER_DEBUG_RESOLVER"] || ENV["DEBUG_RESOLVER"]

attr_reader :level, :locked_specs, :unlock_gems
attr_reader :level

# By default, strict is false, meaning every available version of a gem
# is returned from sort_versions. The order gives preference to the
Expand All @@ -24,24 +22,12 @@ class GemVersionPromoter
# existing in the referenced source.
attr_accessor :strict

attr_accessor :prerelease_specified

# Given a list of locked_specs and a list of gems to unlock creates a
# GemVersionPromoter instance.
# Creates a GemVersionPromoter instance.
#
# @param locked_specs [SpecSet] All current locked specs. Unlike Definition
# where this list is empty if all gems are being updated, this should
# always be populated for all gems so this class can properly function.
# @param unlock_gems [String] List of gem names being unlocked. If empty,
# all gems will be considered unlocked.
# @return [GemVersionPromoter]
def initialize(locked_specs = SpecSet.new([]), unlock_gems = [])
def initialize
@level = :major
@strict = false
@locked_specs = locked_specs
@unlock_gems = unlock_gems
@sort_versions = {}
@prerelease_specified = {}
end

# @param value [Symbol] One of three Symbols: :major, :minor or :patch.
Expand All @@ -55,34 +41,19 @@ def level=(value)
@level = v
end

# Given a Dependency and an Array of Specifications of available versions for a
# gem, this method will return the Array of Specifications sorted (and possibly
# truncated if strict is true) in an order to give preference to the current
# level (:major, :minor or :patch) when resolution is deciding what versions
# best resolve all dependencies in the bundle.
# @param dep [Dependency] The Dependency of the gem.
# @param spec_groups [Specification] An array of Specifications for the same gem
# named in the @dep param.
# Given a Resolver::Package and an Array of Specifications of available
# versions for a gem, this method will return the Array of Specifications
# sorted (and possibly truncated if strict is true) in an order to give
# preference to the current level (:major, :minor or :patch) when resolution
# is deciding what versions best resolve all dependencies in the bundle.
# @param package [Resolver::Package] The package being resolved.
# @param specs [Specification] An array of Specifications for the package.
# @return [Specification] A new instance of the Specification Array sorted and
# possibly filtered.
def sort_versions(dep, spec_groups)
@sort_versions[dep] ||= begin
gem_name = dep.name
def sort_versions(package, specs)
specs = filter_dep_specs(specs, package) if strict

# An Array per version returned, different entries for different platforms.
# We only need the version here so it's ok to hard code this to the first instance.
locked_spec = locked_specs[gem_name].first

if strict
filter_dep_specs(spec_groups, locked_spec)
else
sort_dep_specs(spec_groups, locked_spec)
end
end
end

def reset
@sort_versions = {}
sort_dep_specs(specs, package)
end

# @return [bool] Convenience method for testing value of level variable.
Expand All @@ -97,11 +68,13 @@ def minor?

private

def filter_dep_specs(spec_groups, locked_spec)
res = spec_groups.select do |spec_group|
if locked_spec && !major?
gsv = spec_group.version
lsv = locked_spec.version
def filter_dep_specs(specs, package)
locked_version = package.locked_version

specs.select do |spec|
if locked_version && !major?
gsv = spec.version
lsv = locked_version

must_match = minor? ? [0] : [0, 1]

Expand All @@ -111,63 +84,53 @@ def filter_dep_specs(spec_groups, locked_spec)
true
end
end

sort_dep_specs(res, locked_spec)
end

def sort_dep_specs(spec_groups, locked_spec)
@locked_version = locked_spec&.version
@gem_name = locked_spec&.name
def sort_dep_specs(specs, package)
locked_version = package.locked_version

result = spec_groups.sort do |a, b|
@a_ver = a.version
@b_ver = b.version

unless @gem_name && @prerelease_specified[@gem_name]
a_pre = @a_ver.prerelease?
b_pre = @b_ver.prerelease?
result = specs.sort do |a, b|
unless locked_version && package.prerelease_specified?
a_pre = a.prerelease?
b_pre = b.prerelease?

next -1 if a_pre && !b_pre
next 1 if b_pre && !a_pre
end

if major?
@a_ver <=> @b_ver
elsif either_version_older_than_locked
@a_ver <=> @b_ver
elsif segments_do_not_match(:major)
@b_ver <=> @a_ver
elsif !minor? && segments_do_not_match(:minor)
@b_ver <=> @a_ver
a <=> b
elsif either_version_older_than_locked(a, b, locked_version)
a <=> b
elsif segments_do_not_match(a, b, :major)
b <=> a
elsif !minor? && segments_do_not_match(a, b, :minor)
b <=> a
else
@a_ver <=> @b_ver
a <=> b
end
end
post_sort(result)
post_sort(result, package.unlock?, locked_version)
end

def either_version_older_than_locked
@locked_version && (@a_ver < @locked_version || @b_ver < @locked_version)
def either_version_older_than_locked(a, b, locked_version)
locked_version && (a.version < locked_version || b.version < locked_version)
end

def segments_do_not_match(level)
def segments_do_not_match(a, b, level)
index = [:major, :minor].index(level)
@a_ver.segments[index] != @b_ver.segments[index]
end

def unlocking_gem?
unlock_gems.empty? || (@gem_name && unlock_gems.include?(@gem_name))
a.segments[index] != b.segments[index]
end

# Specific version moves can't always reliably be done during sorting
# as not all elements are compared against each other.
def post_sort(result)
def post_sort(result, unlock, locked_version)
# default :major behavior in Bundler does not do this
return result if major?
if unlocking_gem? || @locked_version.nil?
if unlock || locked_version.nil?
result
else
move_version_to_end(result, @locked_version)
move_version_to_end(result, locked_version)
end
end

Expand Down
18 changes: 5 additions & 13 deletions lib/bundler/index.rb
Expand Up @@ -70,7 +70,7 @@ def local_search(query)
case query
when Gem::Specification, RemoteSpecification, LazySpecification, EndpointSpecification then search_by_spec(query)
when String then specs_by_name(query)
when Gem::Dependency then search_by_dependency(query)
when Array then specs_by_name_and_version(*query)
else
raise "You can't search for a #{query.inspect}."
end
Expand Down Expand Up @@ -157,20 +157,12 @@ def add_source(index)

private

def specs_by_name(name)
@specs[name].values
def specs_by_name_and_version(name, version)
specs_by_name(name).select {|spec| spec.version == version }
end

def search_by_dependency(dependency)
@cache[dependency] ||= begin
specs = specs_by_name(dependency.name)
found = specs.select do |spec|
next true if spec.source.is_a?(Source::Gemspec)
dependency.matches_spec?(spec)
end

found
end
def specs_by_name(name)
@specs[name].values
end

EMPTY_SEARCH = [].freeze
Expand Down
4 changes: 2 additions & 2 deletions lib/bundler/inline.rb
Expand Up @@ -34,7 +34,8 @@ def gemfile(install = false, options = {}, &gemfile)

opts = options.dup
ui = opts.delete(:ui) { Bundler::UI::Shell.new }
ui.level = "silent" if opts.delete(:quiet)
ui.level = "silent" if opts.delete(:quiet) || !install
Bundler.ui = ui
raise ArgumentError, "Unknown options: #{opts.keys.join(", ")}" unless opts.empty?

begin
Expand All @@ -52,7 +53,6 @@ def gemfile(install = false, options = {}, &gemfile)
def definition.lock(*); end
definition.validate_runtime!

Bundler.ui = install ? ui : Bundler::UI::Silent.new
if install || definition.missing_specs?
Bundler.settings.temporary(:inline => true, :no_install => false) do
installer = Bundler::Installer.install(Bundler.root, definition, :system => true)
Expand Down
2 changes: 1 addition & 1 deletion lib/bundler/lazy_specification.rb
Expand Up @@ -79,7 +79,7 @@ def materialize_for_installation
candidates = if source.is_a?(Source::Path) || !ruby_platform_materializes_to_ruby_platform?
target_platform = ruby_platform_materializes_to_ruby_platform? ? platform : local_platform

GemHelpers.select_best_platform_match(source.specs.search(Dependency.new(name, version)), target_platform)
GemHelpers.select_best_platform_match(source.specs.search([name, version]), target_platform)
else
source.specs.search(self)
end
Expand Down