class Gem::Resolver

Given a set of Gem::Dependency objects as needed and a way to query the set of available specs via set, calculates a set of ActivationRequest objects which indicate all the specs that should be activated to meet the all the requirements.

Constants

DEBUG_RESOLVER

If the DEBUG_RESOLVER environment variable is set then debugging mode is enabled for the resolver. This will display information about the state of the resolver while a set of dependencies is being resolved.

Attributes

The Gem::Cooldown applied to release candidates, if any.

Set to true if all development dependencies should be considered.

Set to true if immediate development dependencies should be considered.

When true, no dependencies are looked up for requested gems.

Hash of gems to skip resolution. Keyed by gem name, with arrays of gem specifications as values.

Public Class Methods

Combines sets into a ComposedSet that allows specification lookup in a uniform manner. If one of the sets is itself a ComposedSet its sets are flattened into the result ComposedSet.

# File lib/rubygems/resolver.rb, line 66
def self.compose_sets(*sets)
  sets.compact!

  sets = sets.flat_map do |set|
    case set
    when Gem::Resolver::BestSet then
      set
    when Gem::Resolver::ComposedSet then
      set.sets
    else
      set
    end
  end

  case sets.length
  when 0 then
    raise ArgumentError, "one set in the composition must be non-nil"
  when 1 then
    sets.first
  else
    Gem::Resolver::ComposedSet.new(*sets)
  end
end

Creates a Resolver that queries only against the already installed gems for the needed dependencies.

# File lib/rubygems/resolver.rb, line 94
def self.for_current_gems(needed)
  new needed, Gem::Resolver::CurrentSet.new
end

Create Resolver object which will resolve the tree starting with needed Dependency objects.

set is an object that provides where to look for specifications to satisfy the Dependencies. This defaults to IndexSet, which will query rubygems.org.

# File lib/rubygems/resolver.rb, line 106
def initialize(needed, set = nil)
  @set = set || Gem::Resolver::IndexSet.new
  @needed = needed

  @cooldown            = nil
  @development         = false
  @development_shallow = false
  @ignore_dependencies = false
  @skip_gems           = {}
  @soft_missing        = false

  @root_package = RootPackage.new
  @root_version = Gem::PubGrub::Package.root_version

  @packages = {}

  @unfiltered_specs = Hash.new {|h, name| h[name] = find_unfiltered_specs_for(name) }
  @all_specs = Hash.new {|h, name| h[name] = filter_specs(@unfiltered_specs[name]) }
  @all_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort }
  @sorted_versions = Hash.new do |h, pkg|
    h[pkg] = Gem::PubGrub::Package.root?(pkg) ? [@root_version] : @all_versions[pkg]
  end
  @cached_dependencies = Hash.new do |h, pkg|
    h[pkg] = if Gem::PubGrub::Package.root?(pkg)
      { @root_version => root_dependencies }
    else
      Hash.new {|v, ver| v[ver] = compute_dependencies(pkg, ver) }
    end
  end
  @version_to_index = Hash.new {|h, pkg| h[pkg] = @sorted_versions[pkg].each_with_index.to_h }
  @versions_for_cache = Hash.new {|h, pkg| h[pkg] = {} }
  @spec_for_cache = Hash.new {|h, name| h[name] = build_spec_for_cache(name) }
end

Public Instance Methods

PubGrub source interface methods

# File lib/rubygems/resolver.rb, line 198
def all_versions_for(package)
  versions = @sorted_versions[package].reverse # highest first
  name = package.to_s

  if (skip_dep_gems = skip_gems[name]) && !skip_dep_gems.empty?
    # Conservative mode: float the already-installed (skip) versions to the
    # front so the solver prefers them. This sets *preference* only (it feeds
    # the strategy's version-index map); it does not restrict availability, so
    # every version stays selectable via versions_for. When an installed
    # version is made impossible by a downstream conflict, the solver
    # backtracks to a newer version instead of failing. Molinillo instead
    # hard-restricted the candidate set to skip versions and raised.
    #
    # This reaches the same outcome as Bundler (upgrade-over-raise) for the
    # common single-blocked-gem case, though the mechanism differs: Bundler
    # hard-pins locked gems and selectively unlocks + re-solves on conflict,
    # whereas we float as a preference and let PubGrub backtrack in one solve.
    # The float can therefore over-upgrade when several installed gems are
    # jointly involved in a conflict; that outcome-level divergence is
    # accepted (see test_conservative_upgrades_when_installed_blocked).
    skip_versions = skip_dep_gems.map(&:version)
    preferred, rest = versions.partition {|v| skip_versions.include?(v) }
    preferred + rest
  else
    # Prefer already-installed versions to avoid unnecessary upgrades
    installed_versions = @all_specs[name].
      select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) }.
      map(&:version)
    if installed_versions.any?
      preferred, rest = versions.partition {|v| installed_versions.include?(v) }
      preferred + rest
    else
      versions
    end
  end
end

Per-gem summary entries for the newest versions the cooldown kept out of a successful resolution. See Gem::Cooldown.output_skipped_summary.

# File lib/rubygems/resolver.rb, line 46
def cooldown_skipped
  @cooldown_skipped || []
end
# File lib/rubygems/resolver.rb, line 272
def incompatibilities_for(package, version)
  package_deps = @cached_dependencies[package]
  sorted_versions = @sorted_versions[package]
  package_deps[version].filter_map do |dep_package_name, dep_constraint|
    dep_package = dep_constraint.package

    low = high = @version_to_index[package][version]

    # find version low such that all >= low share the same dep
    while low > 0 &&
          package_deps[sorted_versions[low - 1]][dep_package_name] == dep_constraint
      low -= 1
    end
    low =
      if low == 0
        nil
      else
        sorted_versions[low]
      end

    # find version high such that all < high share the same dep
    while high < sorted_versions.length &&
          package_deps[sorted_versions[high]][dep_package_name] == dep_constraint
      high += 1
    end
    high =
      if high == sorted_versions.length
        nil
      else
        sorted_versions[high]
      end

    range = Gem::PubGrub::VersionRange.new(min: low, max: high, include_min: !low.nil?)
    self_constraint = Gem::PubGrub::VersionConstraint.new(package, range: range)

    # No specs anywhere means an unknown package. Check @unfiltered_specs, not
    # the filtered set, so a dep filtered out by platform/Ruby/prerelease falls
    # through to NoVersions for proper hints instead. The band-scoped
    # self_constraint lets clean sibling versions still resolve via backtracking.
    if @unfiltered_specs[dep_package_name].empty?
      cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint)
      self_term = Gem::PubGrub::Term.new(self_constraint, true)
      # PubGrub's default InvalidDependency rendering drops the version
      # requirement ("depends on unknown package bar"). Supply a custom
      # explanation so the missing dependency's constraint is preserved
      # ("depends on bar = 0.5 which could not be found in any repository"),
      # matching Molinillo's diagnostics.
      return [Gem::PubGrub::Incompatibility.new(
        [self_term],
        cause: cause,
        custom_explanation: "#{self_term.to_s(allow_every: true)} depends on #{dep_constraint} which could not be found in any repository"
      )]
    end

    # An empty range means the requirement is self-contradictory (e.g. `> 2, < 1`).
    if dep_constraint.range.empty?
      return [Gem::Resolver::Incompatibility.new(
        [Gem::PubGrub::Term.new(self_constraint, true)],
        cause: Gem::PubGrub::Incompatibility::NoVersions.new(dep_constraint),
        custom_explanation: "#{dep_package_name} cannot satisfy contradictory requirements #{dep_constraint.constraint_string}"
      )]
    end

    Gem::PubGrub::Incompatibility.new(
      [Gem::PubGrub::Term.new(self_constraint, true), Gem::PubGrub::Term.new(dep_constraint, false)],
      cause: :dependency
    )
  end
end
# File lib/rubygems/resolver.rb, line 253
def no_versions_incompatibility_for(_package, unsatisfied_term)
  cause = Gem::PubGrub::Incompatibility::NoVersions.new(unsatisfied_term)

  name = unsatisfied_term.package.to_s
  constraint = unsatisfied_term.constraint
  extended_explanation = build_extended_explanation(name, constraint)

  custom_explanation = if extended_explanation
    "#{constraint} could not be found in any repository"
  end

  Gem::Resolver::Incompatibility.new(
    [unsatisfied_term],
    cause: cause,
    custom_explanation: custom_explanation,
    extended_explanation: extended_explanation
  )
end

Proceed with resolution! Returns an array of ActivationRequest objects.

# File lib/rubygems/resolver.rb, line 143
def resolve
  # Pre-check: raise UnsatisfiableDependencyError for root deps with no
  # platform match. We filter by platform ONLY here (not required_ruby_version
  # / required_rubygems_version): a foreign-platform gem is genuinely "not
  # found", but a gem that exists yet is incompatible with the running Ruby
  # should flow through the solver to a DependencyResolutionError that names
  # the Ruby requirement. That matches Bundler (which models Ruby as a
  # synthetic dependency, so this surfaces as a solve failure) and gives a
  # clearer message than the platform-oriented UnsatisfiableDependencyError.
  @needed.each do |dep|
    next if @soft_missing
    dep_request = DependencyRequest.new(dep, nil)
    all = @set.find_all(dep_request)
    matching = select_local_platforms(all)

    next unless matching.empty?

    exc = Gem::UnsatisfiableDependencyError.new(dep_request, all)
    exc.errors = @set.errors
    raise exc
  end

  solver = Gem::PubGrub::VersionSolver.new(
    source: self,
    root: @root_package,
    strategy: Gem::Resolver::Strategy.new(self),
    logger: make_logger
  )
  result = solver.solve

  # Convert to Array<ActivationRequest>
  needed_by_name = @needed.group_by(&:name)
  requests = result.filter_map do |package, version|
    next if Gem::PubGrub::Package.root?(package)
    spec = spec_for(package.to_s, version)
    dep = needed_by_name[package.to_s]&.first || Gem::Dependency.new(package.to_s)
    dep_request = DependencyRequest.new(dep, nil)
    ActivationRequest.new(spec, dep_request)
  end

  @cooldown_skipped = cooldown_skipped_summary(requests) if @cooldown&.active?

  requests
rescue Gem::PubGrub::SolveFailure => e
  extended = extract_extended_explanation(e.incompatibility)
  if extended
    message = "#{e.explanation}\n\n#{extended}"
    raise Gem::DependencyResolutionError, Struct.new(:explanation).new(message)
  else
    raise Gem::DependencyResolutionError, e
  end
end
# File lib/rubygems/resolver.rb, line 235
def versions_for(package, range = Gem::PubGrub::VersionRange.any)
  @versions_for_cache[package][range] ||= begin
    candidates = range.select_versions(@sorted_versions[package])

    if Gem::PubGrub::Package.root?(package) ||
       (@set.respond_to?(:prerelease) && @set.prerelease) ||
       range_admits_prerelease?(range)
      candidates
    elsif @all_versions[package].any? {|v| !v.prerelease? }
      candidates.reject(&:prerelease?)
    else
      # Only prereleases exist for this gem; fall back to them so
      # dependencies like `>= 1.0` can still be satisfied.
      candidates
    end
  end
end