# This is the latest iteration of the gem dependency resolving algorithm. As of now,# it can resolve (as a success of failure) any set of gem dependencies we throw at it# in a reasonable amount of time. The most iterations I've seen it take is about 150.# The actual implementation of the algorithm is not as good as it could be yet, but that# can come later.# Extending Gem classes to add necessary tracking informationmoduleGemclassDependencydefrequired_by@required_by||=[]endendclassSpecificationdefrequired_by@required_by||=[]endendendmoduleBundlerclassGemNotFound<StandardError;endclassVersionConflict<StandardError;endclassResolverattr_reader:errors# 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.## ==== Parameters# *dependencies<Gem::Dependency>:: The list of dependencies to resolve## ==== Returns# <GemBundle>,nil:: If the list of dependencies can be resolved, a# collection of gemspecs is returned. Otherwise, nil is returned.defself.resolve(requirements,sources,source_requirements={})resolver=new(sources,source_requirements)result=catch(:success)doresolver.resolve(requirements,{})output=resolver.errors.inject("")do|o,(conflict,(origin,requirement))|o<<" Conflict on: #{conflict.inspect}:\n"o<<" * #{conflict} (#{origin.version}) activated by #{origin.required_by.first}\n"o<<" * #{requirement} required by #{requirement.required_by.first}\n"o<<" All possible versions of origin requirements conflict."endraiseVersionConflict,"No compatible versions could be found for required dependencies:\n#{output}"nilendifresult# Order gems in order of dependencies. Every gem's dependency is at# a smaller index in the array.ordered=[]result.values.eachdo|spec1|index=nilplace=ordered.detectdo|spec2|spec1.dependencies.any?{|d|d.name==spec2.name}endplace?ordered.insert(ordered.index(place),spec1):ordered<<spec1endordered.reverseendenddefinitialize(sources,source_requirements)@errors={}@stack=[]@specs=Hash.new{|h,k|h[k]=[]}@by_gem=source_requirements@cache={}@index={}sources.eachdo|source|source.gems.eachdo|name,specs|# Hack to work with a regular Gem::SourceIndex[specs].flatten.compact.eachdo|spec|nextif@specs[spec.name].any?{|s|s.version==spec.version}@specs[spec.name]<<specendendendenddefresolve(reqs,activated)# If the requirements are empty, then we are in a success state. Aka, all# gem dependencies have been resolved.throw:success,activatedifreqs.empty?# Sort dependencies so that the ones that are easiest to resolve are first.# Easiest to resolve is defined by:# 1) Is this gem already activated?# 2) Do the version requirements include prereleased gems?# 3) Sort by number of gems available in the source.reqs=reqs.sort_bydo|a|[activated[a.name]?0:1,a.version_requirements.prerelease??0:1,activated[a.name]?0:search(a).size]endactivated=activated.dup# Pull off the first requirement so that we can resolve itcurrent=reqs.shift# Check if the gem has already been activated, if it has, we will make sure# that the currently activated gem satisfies the requirement.ifexisting=activated[current.name]ifcurrent.version_requirements.satisfied_by?(existing.version)@errors.delete(existing.name)# Since the current requirement is satisfied, we can continue resolving# the remaining requirements.resolve(reqs,activated)else@errors[existing.name]=[existing,current]# Since the current requirement conflicts with an activated gem, we need# to backtrack to the current requirement's parent and try another version# of it (maybe the current requirement won't be present anymore). If the# current requirement is a root level requirement, we need to jump back to# where the conflicting gem was activated.parent=current.required_by.last||existing.required_by.last# We track the spot where the current gem was activated because we need# to keep a list of every spot a failure happened.throwparent.name,existing.required_by.last.nameendelse# There are no activated gems for the current requirement, so we are going# to find all gems that match the current requirement and try them in decending# order. We also need to keep a set of all conflicts that happen while trying# this gem. This is so that if no versions work, we can figure out the best# place to backtrack to.conflicts=Set.new# Fetch all gem versions matching the requirement## TODO: Warn / error when no matching versions are found.matching_versions=search(current)ifmatching_versions.empty?ifcurrent.required_by.empty?raiseGemNotFound,"Could not find gem '#{current}' in any of the sources"endBundler.logger.warn"Could not find gem '#{current}' (required by '#{current.required_by.last}') in any of the sources"endmatching_versions.reverse_eachdo|spec|conflict=resolve_requirement(spec,current,reqs.dup,activated.dup)conflicts<<conflictifconflictend# If the current requirement is a root level gem and we have conflicts, we# can figure out the best spot to backtrack to.ifcurrent.required_by.empty?&&!conflicts.empty?# Check the current "catch" stack for the first one that is included in the# conflicts set. That is where the parent of the conflicting gem was required.# By jumping back to this spot, we can try other version of the parent of# the conflicting gem, hopefully finding a combination that activates correctly.@stack.reverse_eachdo|savepoint|ifconflicts.include?(savepoint)throwsavepointendendendendenddefresolve_requirement(spec,requirement,reqs,activated)# We are going to try activating the spec. We need to keep track of stack of# requirements that got us to the point of activating this gem.spec.required_by.replacerequirement.required_byspec.required_by<<requirementactivated[spec.name]=spec# Now, we have to loop through all child dependencies and add them to our# array of requirements.spec.dependencies.eachdo|dep|nextifdep.type==:developmentdep.required_by<<requirementreqs<<depend# We create a savepoint and mark it by the name of the requirement that caused# the gem to be activated. If the activated gem ever conflicts, we are able to# jump back to this point and try another version of the gem.length=@stack.length@stack<<requirement.nameretval=catch(requirement.name)doresolve(reqs,activated)end# Since we're doing a lot of throw / catches. A push does not necessarily match# up to a pop. So, we simply slice the stack back to what it was before the catch# block.@stack.slice!(length..-1)retvalenddefsearch(dependency)@cache[dependency.hash]||=begincollection=@by_gem[dependency.name].gemsif@by_gem[dependency.name]collection||=@specscollection[dependency.name].selectdo|spec|match=dependency=~specmatch&=dependency.version_requirements.prerelease?ifspec.version.prerelease?matchend.sort_by{|s|s.version}endendendend