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
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
Source
# File lib/rubygems/resolver.rb, line 53 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
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.
Source
# File lib/rubygems/resolver.rb, line 81 def self.for_current_gems(needed) new needed, Gem::Resolver::CurrentSet.new end
Creates a Resolver that queries only against the already installed gems for the needed dependencies.
Source
# File lib/rubygems/resolver.rb, line 93 def initialize(needed, set = nil) @set = set || Gem::Resolver::IndexSet.new @needed = needed @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
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.
Public Instance Methods
Source
# File lib/rubygems/resolver.rb, line 180 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
PubGrub source interface methods
Source
# File lib/rubygems/resolver.rb, line 254 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
Source
# File lib/rubygems/resolver.rb, line 235 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
Source
# File lib/rubygems/resolver.rb, line 129 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) 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 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
Proceed with resolution! Returns an array of ActivationRequest objects.
Source
# File lib/rubygems/resolver.rb, line 217 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
Private Instance Methods
Source
# File lib/rubygems/resolver.rb, line 439 def build_extended_explanation(name, constraint) unfiltered = @unfiltered_specs[name] return if unfiltered.empty? filtered = @all_specs[name] pkg = package_for(name) # A prerelease hint applies when the source would strip prereleases for # this constraint (global prerelease flag off and the constraint's range # doesn't itself reach into prerelease territory) AND a prerelease of # the gem exists somewhere. prerelease_gated = !(@set.respond_to?(:prerelease) && @set.prerelease) && !range_admits_prerelease?(constraint.range) has_prerelease_candidate = prerelease_gated && @all_versions[pkg].any?(&:prerelease?) return if filtered.length == unfiltered.length && !has_prerelease_candidate hints = [] # Check for specs that exist for other platforms platform_specs = unfiltered.select do |s| !Gem::Platform.installable?(s) && constraint.range.include?(s.version) end if platform_specs.any? label = "#{name} (#{constraint.constraint_string})" hints << "The source contains the following gems matching '#{label}':" platform_specs.each do |s| actual = s.respond_to?(:spec) ? s.spec : s hints << " * #{actual.full_name}" end end # Check for specs filtered by Ruby version installable = select_local_platforms(unfiltered) ruby_specs = installable.select do |s| actual = s.respond_to?(:spec) ? s.spec : s constraint.range.include?(s.version) && !actual.required_ruby_version.satisfied_by?(Gem.ruby_version) rescue StandardError false end if ruby_specs.any? versions = ruby_specs.map(&:version).uniq.sort.reverse.first(3) sample = ruby_specs.find {|s| s.version == versions.first } actual = sample.respond_to?(:spec) ? sample.spec : sample ruby_req = actual.required_ruby_version hints << "#{name} #{versions.join(", ")} requires Ruby #{ruby_req} (you have #{Gem.ruby_version})" end # Check for specs filtered by prerelease status if prerelease_gated prerelease_versions = @all_versions[pkg].select(&:prerelease?) if prerelease_versions.any? versions = prerelease_versions.sort.reverse.first(3) # limit to avoid cluttering error output hints << "#{name} #{versions.join(", ")} are pre-release versions. Use --prerelease to allow pre-release gems." end end hints.empty? ? nil : hints.join("\n") end
Source
# File lib/rubygems/resolver.rb, line 383 def build_spec_for_cache(name) # Rank sources by the order they were first supplied so that, when multiple # sources offer the same version and platform, the earlier source wins. source_rank = {} @all_specs[name].each do |s| source_rank[s.source] ||= source_rank.size end @all_specs[name].group_by(&:version).transform_values do |candidates| next candidates.first if candidates.length == 1 # Prefer already-installed specs to avoid unnecessary downloads installed = candidates.select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) } next installed.first if installed.length == 1 candidates = installed if installed.any? # Among remaining candidates, prefer the most specific platform, then the # earlier-supplied source. candidates.min_by do |s| [Gem::Platform.platform_specificity_match(s.platform, Gem::Platform.local), source_rank[s.source]] end end end
Source
# File lib/rubygems/resolver.rb, line 408 def compute_dependencies(package, version) spec = spec_for(package.to_s, version) return {} unless spec return {} if @ignore_dependencies spec.fetch_development_dependencies if @development && spec.respond_to?(:fetch_development_dependencies) deps = {} root_names = @needed.map(&:name) spec.dependencies.each do |d| next if d.name == package.to_s next if d.type == :development && !@development next if d.type == :development && @development_shallow && !root_names.include?(package.to_s) dep_package = package_for(d.name) # In force mode, skip deps that can't be satisfied - either no # specs at all, or no specs matching the version requirement. if @soft_missing dep_specs = @all_specs[d.name] matching = dep_specs.select {|s| d.requirement.satisfied_by?(s.version) } next if matching.empty? end deps[d.name] = Gem::PubGrub::RubyGems.requirement_to_constraint(dep_package, d.requirement) end deps end
Source
# File lib/rubygems/resolver.rb, line 501 def extract_extended_explanation(incompatibility) while incompatibility.cause.is_a?(Gem::PubGrub::Incompatibility::ConflictCause) cause = incompatibility.cause [cause.conflict, cause.other].each do |incompat| if incompat.cause.is_a?(Gem::PubGrub::Incompatibility::NoVersions) && incompat.respond_to?(:extended_explanation) && incompat.extended_explanation return incompat.extended_explanation end end incompatibility = cause.conflict end nil end
Source
# File lib/rubygems/resolver.rb, line 364 def filter_specs(specs) filtered = select_local_platforms(specs) unless @soft_missing filtered = filtered.select do |s| s.required_ruby_version.satisfied_by?(Gem.ruby_version) && s.required_rubygems_version.satisfied_by?(Gem.rubygems_version) rescue StandardError true end end filtered end
Source
# File lib/rubygems/resolver.rb, line 358 def find_unfiltered_specs_for(name) dep = Gem::Dependency.new(name, ">= 0.a") dep_request = DependencyRequest.new(dep, nil) @set.find_all(dep_request) end
Source
# File lib/rubygems/resolver.rb, line 519 def make_logger DEBUG_RESOLVER ? Gem::PubGrub::StderrLogger.new : Gem::PubGrub::NullLogger.new end
Source
# File lib/rubygems/resolver.rb, line 335 def package_for(name) @packages[name] ||= Gem::PubGrub::Package.new(name) end
Source
# File lib/rubygems/resolver.rb, line 351 def range_admits_prerelease?(range) range.ranges.any? do |r| next false if r.empty? r.min&.prerelease? end end
Only the min bound is inspected: β~>` synthesises a max like X.A whose suffix looks prerelease to Gem::Version but is not the userβs intent, so checking max would mis-admit prereleases for every β~>`.
Source
# File lib/rubygems/resolver.rb, line 339 def root_dependencies deps = {} @needed.each do |dep| constraint = Gem::PubGrub::RubyGems.requirement_to_constraint(package_for(dep.name), dep.requirement) deps[dep.name] = deps.key?(dep.name) ? deps[dep.name].intersect(constraint) : constraint end deps end
Source
# File lib/rubygems/resolver.rb, line 379 def spec_for(name, version) @spec_for_cache[name][version] end