123456789_123456789_123456789_123456789_123456789_

Class: Bundler::PubGrub::VersionSolver

Relationships & Source Files
Inherits: Object
Defined in: lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(source:, root: Package.root, logger: Bundler::PubGrub.logger) ⇒ VersionSolver

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 12

def initialize(source:, root: Package.root, logger: Bundler::PubGrub.logger)
  @logger = logger

  @source = source

  # { package => [incompatibility, ...]}
  @incompatibilities = Hash.new do |h, k|
    h[k] = []
  end

  @seen_incompatibilities = {}

  @solution = PartialSolution.new

  add_incompatibility Incompatibility.new([
    Term.new(VersionConstraint.any(root), false)
  ], cause: :root)

  propagate(root)
end

Instance Attribute Details

#logger (readonly)

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 8

attr_reader :logger

#solution (readonly)

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 10

attr_reader :solution

#solved?Boolean (readonly)

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 33

def solved?
  solution.unsatisfied.empty?
end

#source (readonly)

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 9

attr_reader :source

Instance Method Details

#add_incompatibility(incompatibility) (private)

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 240

def add_incompatibility(incompatibility)
  logger.debug { "fact: #{incompatibility}" }
  incompatibility.terms.each do |term|
    package = term.package
    @incompatibilities[package] << incompatibility
  end
end

#choose_package_version (private)

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 119

def choose_package_version
  if solution.unsatisfied.empty?
    logger.info "No packages unsatisfied. Solving complete!"
    return nil
  end

  package = next_package_to_try
  unsatisfied_term = solution.unsatisfied.find { |t| t.package == package }
  version = source.versions_for(package, unsatisfied_term.constraint.range).first
  logger.debug { "attempting #{package} #{version}" }

  if version.nil?
    add_incompatibility source.no_versions_incompatibility_for(package, unsatisfied_term)
    return package
  end

  conflict = false

  source.incompatibilities_for(package, version).each do |incompatibility|
    if @seen_incompatibilities.include?(incompatibility)
      logger.debug { "knew: #{incompatibility}" }
      next
    end
    @seen_incompatibilities[incompatibility] = true

    add_incompatibility incompatibility

    conflict ||= incompatibility.terms.all? do |term|
      term.package == package || solution.satisfies?(term)
    end
  end

  unless conflict
    logger.info { "selected #{package} #{version}" }

    solution.decide(package, version)
  else
    logger.info { "conflict: #{conflict.inspect}" }
  end

  package
end

#next_package_to_try (private)

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 108

def next_package_to_try
  solution.unsatisfied.min_by do |term|
    package = term.package
    range = term.constraint.range
    matching_versions = source.versions_for(package, range)
    higher_versions = source.versions_for(package, range.upper_invert)

    [matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
  end.package
end

#propagate(initial_package) (private)

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 67

def propagate(initial_package)
  changed = [initial_package]
  while package = changed.shift
    @incompatibilities[package].reverse_each do |incompatibility|
      result = propagate_incompatibility(incompatibility)
      if result == :conflict
        root_cause = resolve_conflict(incompatibility)
        changed.clear
        changed << propagate_incompatibility(root_cause)
      elsif result # should be a Package
        changed << result
      end
    end
    changed.uniq!
  end
end

#propagate_incompatibility(incompatibility) (private)

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 84

def propagate_incompatibility(incompatibility)
  unsatisfied = nil
  incompatibility.terms.each do |term|
    relation = solution.relation(term)
    if relation == :disjoint
      return nil
    elsif relation == :overlap
      # If more than one term is inconclusive, we can't deduce anything
      return nil if unsatisfied
      unsatisfied = term
    end
  end

  if !unsatisfied
    return :conflict
  end

  logger.debug { "derived: #{unsatisfied.invert}" }

  solution.derive(unsatisfied.invert, incompatibility)

  unsatisfied.package
end

#resolve_conflict(incompatibility) (private)

Raises:

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 162

def resolve_conflict(incompatibility)
  logger.info { "conflict: #{incompatibility}" }

  new_incompatibility = nil

  while !incompatibility.failure?
    most_recent_term = nil
    most_recent_satisfier = nil
    difference = nil

    previous_level = 1

    incompatibility.terms.each do |term|
      satisfier = solution.satisfier(term)

      if most_recent_satisfier.nil?
        most_recent_term = term
        most_recent_satisfier = satisfier
      elsif most_recent_satisfier.index < satisfier.index
        previous_level = [previous_level, most_recent_satisfier.decision_level].max
        most_recent_term = term
        most_recent_satisfier = satisfier
        difference = nil
      else
        previous_level = [previous_level, satisfier.decision_level].max
      end

      if most_recent_term == term
        difference = most_recent_satisfier.term.difference(most_recent_term)
        if difference.empty?
          difference = nil
        else
          difference_satisfier = solution.satisfier(difference.inverse)
          previous_level = [previous_level, difference_satisfier.decision_level].max
        end
      end
    end

    if previous_level < most_recent_satisfier.decision_level ||
        most_recent_satisfier.decision?

      logger.info { "backtracking to #{previous_level}" }
      solution.backtrack(previous_level)

      if new_incompatibility
        add_incompatibility(new_incompatibility)
      end

      return incompatibility
    end

    new_terms = []
    new_terms += incompatibility.terms - [most_recent_term]
    new_terms += most_recent_satisfier.cause.terms.reject { |term|
      term.package == most_recent_satisfier.term.package
    }
    if difference
      new_terms << difference.invert
    end

    new_incompatibility = Incompatibility.new(new_terms, cause: Incompatibility::ConflictCause.new(incompatibility, most_recent_satisfier.cause))

    if incompatibility.to_s == new_incompatibility.to_s
      logger.info { "!! failed to resolve conflicts, this shouldn't have happened" }
      break
    end

    incompatibility = new_incompatibility

    partially = difference ? " partially" : ""
    logger.info { "! #{most_recent_term} is#{partially} satisfied by #{most_recent_satisfier.term}" }
    logger.info { "! which is caused by #{most_recent_satisfier.cause}" }
    logger.info { "! thus #{incompatibility}" }
  end

  raise SolveFailure.new(incompatibility)
end

#result

Alias for #solve.

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 63

alias_method :result, :solve

#solve Also known as: #result

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 57

def solve
  work until solved?

  solution.decisions
end

#work

Returns true if there is more work to be done, false otherwise

[ GitHub ]

  
# File 'lib/bundler/vendor/pub_grub/lib/pub_grub/version_solver.rb', line 38

def work
  return false if solved?

  next_package = choose_package_version
  propagate(next_package)

  if solved?
    logger.info { "Solution found after #{solution.attempted_solutions} attempts:" }
    solution.decisions.each do |package, version|
      next if Package.root?(package)
      logger.info { "* #{package} #{version}" }
    end

    false
  else
    true
  end
end