# File lib/bundler/resolver.rb, line 137 def initialize(index, source_requirements, base) @errors = {} @base = base @index = index @deps_for = {} @missing_gems = Hash.new(0) @source_requirements = source_requirements @iteration_counter = 0 @started_at = Time.now end
Figures out the best possible configuration of gems that satisfies the list of passed dependencies and any child dependencies without causing any gem activation errors.
The list of dependencies to resolve
If the list of dependencies can be resolved, a
collection of gemspecs is returned. Otherwise, nil is returned.
# File lib/bundler/resolver.rb, line 125 def self.resolve(requirements, index, source_requirements = {}, base = []) Bundler.ui.info "Resolving dependencies...", false base = SpecSet.new(base) unless base.is_a?(SpecSet) resolver = new(index, source_requirements, base) result = resolver.start(requirements) Bundler.ui.info "" # new line now that dots are done SpecSet.new(result) rescue => e Bundler.ui.info "" # new line before the error raise e end
# File lib/bundler/resolver.rb, line 206 def activate_gem(reqs, activated, requirement, current) requirement.required_by.replace current.required_by requirement.required_by << current activated[requirement.name] = requirement debug { " Activating: #{requirement.name} (#{requirement.version})" } debug { requirement.required_by.map { |d| " * #{d.name} (#{d.requirement})" }.join("\n") } dependencies = requirement.activate_platform(current.__platform) debug { " Dependencies"} dependencies.each do |dep| next if dep.type == :development dep.required_by.replace(current.required_by) dep.required_by << current @gems_size[dep] ||= gems_size(dep) reqs << dep end end
# File lib/bundler/resolver.rb, line 413 def clean_req(req) if req.to_s.include?(">= 0") req.to_s.gsub(/ \(.*?\)$/, '') else req.to_s.gsub(/\, (runtime|development)\)$/, ')') end end
# File lib/bundler/resolver.rb, line 380 def clear_search_cache @deps_for = {} end
# File lib/bundler/resolver.rb, line 148 def debug if ENV['DEBUG_RESOLVER'] debug_info = yield debug_info = debug_info.inspect unless debug_info.is_a?(String) $stderr.puts debug_info end end
# File lib/bundler/resolver.rb, line 440 def dependency_tree(m, requirements) requirements.each_with_index do |i, j| m << " " << (" " * j) m << "#{clean_req(i)}" m << " depends on\n" end m << " " << (" " * requirements.size) end
# File lib/bundler/resolver.rb, line 449 def error_message errors.inject("") do |o, (conflict, (origin, requirement))| # origin is the SpecSet of specs from the Gemfile that is conflicted with if origin o << %Q{Bundler could not find compatible versions for gem "#{origin.name}":\n} o << " In Gemfile:\n" required_by = requirement.required_by o << gem_message(requirement, required_by) # If the origin is "bundler", the conflict is us if origin.name == "bundler" o << " Current Bundler version:\n" other_bundler_required = !requirement.requirement.satisfied_by?(origin.version) # If the origin is a LockfileParser, it does not respond_to :required_by elsif !origin.respond_to?(:required_by) || !(origin.required_by.first) o << " In snapshot (Gemfile.lock):\n" end required_by = origin.required_by[0..-2] o << gem_message(origin, required_by) # If the bundle wants a newer bundler than the running bundler, explain if origin.name == "bundler" && other_bundler_required o << "This Gemfile requires a different version of Bundler.\n" o << "Perhaps you need to update Bundler by running `gem install bundler`?" end # origin is nil if the required gem and version cannot be found in any of # the specified sources else # if the gem cannot be found because of a version conflict between lockfile and gemfile, # print a useful error that suggests running `bundle update`, which may fix things # # @base is a SpecSet of the gems in the lockfile # conflict is the name of the gem that could not be found if locked = @base[conflict].first o << "Bundler could not find compatible versions for gem #{conflict.inspect}:\n" o << " In snapshot (Gemfile.lock):\n" o << " #{clean_req(locked)}\n\n" o << " In Gemfile:\n" required_by = requirement.required_by o << gem_message(requirement, required_by) o << "Running `bundle update` will rebuild your snapshot from scratch, using only\n" o << "the gems in your Gemfile, which may resolve the conflict.\n" # the rest of the time, the gem cannot be found because it does not exist in the known sources else if requirement.required_by.first o << "Could not find gem '#{clean_req(requirement)}', which is required by " o << "gem '#{clean_req(requirement.required_by.first)}', in any of the sources." else o << "Could not find gem '#{clean_req(requirement)} in any of the sources\n" end end end o end end
# File lib/bundler/resolver.rb, line 198 def find_conflict_state(conflict, states) return unless conflict until states.empty? do state = states.pop return state if conflict.name == state.name end end
# File lib/bundler/resolver.rb, line 188 def find_state(current, states) states.detect { |i| current && current.name == i.name } end
For a given conflicted requirement, print out what exactly went wrong
# File lib/bundler/resolver.rb, line 426 def gem_message(requirement, required_by=[]) m = "" # A requirement that is required by itself is actually in the Gemfile, and does # not "depend on" itself if requirement.required_by.first && requirement.required_by.first.name != requirement.name dependency_tree(m, required_by) m << "#{clean_req(requirement)}\n" else m << " #{clean_req(requirement)}\n" end m << "\n" end
# File lib/bundler/resolver.rb, line 376 def gems_size(dep) search(dep).size end
# File lib/bundler/resolver.rb, line 173 def handle_conflict(current, states, existing=nil) until current.nil? && existing.nil? current_state = find_state(current, states) existing_state = find_state(existing, states) return current if state_any?(current_state) return existing if state_any?(existing_state) existing = existing.required_by.last if existing current = current.required_by.last if current end end
# File lib/bundler/resolver.rb, line 192 def other_possible?(conflict, states) return unless conflict state = states.detect { |i| i.name == conflict.name } state && state.possibles.any? end
# File lib/bundler/resolver.rb, line 254 def resolve(reqs, activated) states = [] depth = 0 conflicts = Set.new until reqs.empty? indicate_progress debug { print "\e[2J\e[f" ; "==== Iterating ====\n\n" } reqs = reqs.sort_by do |a| [ activated[a.name] ? 0 : 1, a.requirement.prerelease? ? 0 : 1, @errors[a.name] ? 0 : 1, activated[a.name] ? 0 : @gems_size[a] ] end debug { "Activated:\n" + activated.values.map {|a| " #{a}" }.join("\n") } debug { "Requirements:\n" + reqs.map {|r| " #{r}"}.join("\n") } current = reqs.shift $stderr.puts "#{' ' * depth}#{current}" if ENV['DEBUG_RESOLVER_TREE'] debug { "Attempting:\n #{current}"} existing = activated[current.name] if existing || current.name == 'bundler' # Force the current if current.name == 'bundler' && !existing existing = search(DepProxy.new(Gem::Dependency.new('bundler', VERSION), Gem::Platform::RUBY)).first raise GemNotFound, %Q{Bundler could not find gem "bundler" (#{VERSION})} unless existing existing.required_by << existing activated['bundler'] = existing end if current.requirement.satisfied_by?(existing.version) debug { " * [SUCCESS] Already activated" } @errors.delete(existing.name) dependencies = existing.activate_platform(current.__platform) reqs.concat dependencies dependencies.each do |dep| next if dep.type == :development @gems_size[dep] ||= gems_size(dep) end depth += 1 next else debug { " * [FAIL] Already activated" } @errors[existing.name] = [existing, current] conflicts << current.name parent = current.required_by.last if existing.respond_to?(:required_by) parent = handle_conflict(current, states, existing.required_by[-2]) unless other_possible?(parent, states) else parent = handle_conflict(current, states) unless other_possible?(parent, states) end if parent.nil? && !conflicts.empty? parent = states.reverse.detect { |i| conflicts.include?(i.name) && state_any?(i)} end raise version_conflict if parent.nil? || parent.name == 'bundler' reqs, activated, depth, conflicts = resolve_conflict(parent, states) end else matching_versions = search(current) # If we found no versions that match the current requirement if matching_versions.empty? # If this is a top-level Gemfile requirement if current.required_by.empty? if base = @base[current.name] and !base.empty? version = base.first.version message = "You have requested:\n" " #{current.name} #{current.requirement}\n\n" "The bundle currently has #{current.name} locked at #{version}.\n" "Try running `bundle update #{current.name}`" elsif current.source name = current.name versions = @source_requirements[name][name].map { |s| s.version } message = "Could not find gem '#{current}' in #{current.source}.\n" if versions.any? message << "Source contains '#{name}' at: #{versions.join(', ')}" else message << "Source does not contain any versions of '#{current}'" end else message = "Could not find gem '#{current}' " if @index.source_types.include?(Bundler::Source::Rubygems) message << "in any of the gem sources listed in your Gemfile." else message << "in the gems available on this machine." end end raise GemNotFound, message # This is not a top-level Gemfile requirement else @errors[current.name] = [nil, current] parent = handle_conflict(current, states) reqs, activated, depth = resolve_conflict(parent, states) next end end state = State.new(reqs.dup, activated.dup, current, matching_versions, depth, conflicts) states << state requirement = state.possibles.pop activate_gem(reqs, activated, requirement, current) end end successify(activated) end
# File lib/bundler/resolver.rb, line 237 def resolve_conflict(current, states) # Find the state where the conflict has occurred state = find_conflict_state(current, states) debug { " -> Going to: #{current.name} state" } if current # Resolve the conflicts by rewinding the state # when the conflicted gem was activated reqs, activated, depth, conflicts = resolve_for_conflict(state) # Keep the state around if it still has other possibilities states << state unless state.possibles.empty? clear_search_cache return reqs, activated, depth, conflicts end
# File lib/bundler/resolver.rb, line 226 def resolve_for_conflict(state) raise version_conflict if state.nil? || state.possibles.empty? reqs, activated, depth, conflicts = state.reqs.dup, state.activated.dup, state.depth, state.conflicts.dup requirement = state.requirement possible = state.possibles.pop activate_gem(reqs, activated, possible, requirement) return reqs, activated, depth, conflicts end
# File lib/bundler/resolver.rb, line 384 def search(dep) if base = @base[dep.name] and base.any? reqs = [dep.requirement.as_list, base.first.version.to_s].flatten.compact d = Gem::Dependency.new(base.first.name, *reqs) else d = dep.dep end @deps_for[d.hash] ||= begin index = @source_requirements[d.name] || @index results = index.search(d, @base[d.name]) if results.any? version = results.first.version nested = [[]] results.each do |spec| if spec.version != version nested << [] version = spec.version end nested.last << spec end deps = nested.map{|a| SpecGroup.new(a) }.select{|sg| sg.for?(dep.__platform) } else deps = [] end end end
# File lib/bundler/resolver.rb, line 160 def start(reqs) activated = {} @gems_size = Hash[reqs.map { |r| [r, gems_size(r)] }] resolve(reqs, activated) end
# File lib/bundler/resolver.rb, line 184 def state_any?(state) state && state.possibles.any? end
# File lib/bundler/resolver.rb, line 156 def successify(activated) activated.values.map { |s| s.to_specs }.flatten.compact end
# File lib/bundler/resolver.rb, line 421 def version_conflict VersionConflict.new(errors.keys, error_message) end
Indicates progress by writing a '.' every #iteration_rate time which is approximately every second. #iteration_rate is calculated in the first second of resolve running.
# File lib/bundler/resolver.rb, line 520 def indicate_progress @iteration_counter += 1 if iteration_rate.nil? if ((Time.now - started_at) % 3600).round >= 1 @iteration_rate = iteration_counter end else if ((iteration_counter % iteration_rate) == 0) Bundler.ui.info ".", false end end end