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_RESOLVERenvironment 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