mirror of
				https://github.com/ruby/ruby.git
				synced 2022-11-09 12:17:21 -05:00 
			
		
		
		
	 9694bb8cac
			
		
	
	
		9694bb8cac
		
	
	
	
	
		
			
			* test/rubygems*: ditto. * common.mk (prelude): Updated for RubyGems 2.0 source rearrangement. * tool/change_maker.rb: Allow invalid UTF-8 characters in source files. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@37976 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
		
			
				
	
	
		
			562 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			562 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| require 'rubygems'
 | |
| require 'rubygems/dependency'
 | |
| require 'rubygems/exceptions'
 | |
| 
 | |
| require 'uri'
 | |
| require 'net/http'
 | |
| 
 | |
| module Gem
 | |
| 
 | |
|   # Raised when a DependencyConflict reaches the toplevel.
 | |
|   # Indicates which dependencies were incompatible.
 | |
|   #
 | |
|   class DependencyResolutionError < Gem::Exception
 | |
|     def initialize(conflict)
 | |
|       @conflict = conflict
 | |
|       a, b = conflicting_dependencies
 | |
| 
 | |
|       super "unable to resolve conflicting dependencies '#{a}' and '#{b}'"
 | |
|     end
 | |
| 
 | |
|     attr_reader :conflict
 | |
| 
 | |
|     def conflicting_dependencies
 | |
|       @conflict.conflicting_dependencies
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   # Raised when a dependency requests a gem for which there is
 | |
|   # no spec.
 | |
|   #
 | |
|   class UnsatisfiableDepedencyError < Gem::Exception
 | |
|     def initialize(dep)
 | |
|       super "unable to find any gem matching dependency '#{dep}'"
 | |
| 
 | |
|       @dependency = dep
 | |
|     end
 | |
| 
 | |
|     attr_reader :dependency
 | |
|   end
 | |
| 
 | |
|   # Raised when dependencies conflict and create the inability to
 | |
|   # find a valid possible spec for a request.
 | |
|   #
 | |
|   class ImpossibleDependenciesError < Gem::Exception
 | |
|     def initialize(request, conflicts)
 | |
|       s = conflicts.size == 1 ? "" : "s"
 | |
|       super "detected #{conflicts.size} conflict#{s} with dependency '#{request.dependency}'"
 | |
|       @request = request
 | |
|       @conflicts = conflicts
 | |
|     end
 | |
| 
 | |
|     def dependency
 | |
|       @request.dependency
 | |
|     end
 | |
| 
 | |
|     attr_reader :conflicts
 | |
|   end
 | |
| 
 | |
|   # 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.
 | |
|   #
 | |
|   class DependencyResolver
 | |
| 
 | |
|     # Represents a specification retrieved via the rubygems.org
 | |
|     # API. This is used to avoid having to load the full
 | |
|     # Specification object when all we need is the name, version,
 | |
|     # and dependencies.
 | |
|     #
 | |
|     class APISpecification
 | |
|       def initialize(set, api_data)
 | |
|         @set = set
 | |
|         @name = api_data[:name]
 | |
|         @version = Gem::Version.new api_data[:number]
 | |
|         @dependencies = api_data[:dependencies].map do |name, ver|
 | |
|           Gem::Dependency.new name, ver.split(/\s*,\s*/)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       attr_reader :name, :version, :dependencies
 | |
| 
 | |
|       def full_name
 | |
|         "#{@name}-#{@version}"
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     # The global rubygems pool, available via the rubygems.org API.
 | |
|     # Returns instances of APISpecification.
 | |
|     #
 | |
|     class APISet
 | |
|       def initialize
 | |
|         @data = Hash.new { |h,k| h[k] = [] }
 | |
|       end
 | |
| 
 | |
|       # Return data for all versions of the gem +name+.
 | |
|       #
 | |
|       def versions(name)
 | |
|         if @data.key?(name)
 | |
|           return @data[name]
 | |
|         end
 | |
| 
 | |
|         u = URI.parse "http://rubygems.org/api/v1/dependencies?gems=#{name}"
 | |
|         str = Net::HTTP.get(u)
 | |
| 
 | |
|         Marshal.load(str).each do |ver|
 | |
|           @data[ver[:name]] << ver
 | |
|         end
 | |
| 
 | |
|         @data[name]
 | |
|       end
 | |
| 
 | |
|       # Return an array of APISpecification objects matching
 | |
|       # DependencyRequest +req+.
 | |
|       #
 | |
|       def find_all(req)
 | |
|         res = []
 | |
| 
 | |
|         versions(req.name).each do |ver|
 | |
|           if req.dependency.match? req.name, ver[:number]
 | |
|             res << APISpecification.new(self, ver)
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         res
 | |
|       end
 | |
| 
 | |
|       # A hint run by the resolver to allow the Set to fetch
 | |
|       # data for DependencyRequests +reqs+.
 | |
|       #
 | |
|       def prefetch(reqs)
 | |
|         names = reqs.map { |r| r.dependency.name }
 | |
|         needed = names.find_all { |d| !@data.key?(d) }
 | |
| 
 | |
|         return if needed.empty?
 | |
| 
 | |
|         u = URI.parse "http://rubygems.org/api/v1/dependencies?gems=#{needed.join ','}"
 | |
|         str = Net::HTTP.get(u)
 | |
| 
 | |
|         Marshal.load(str).each do |ver|
 | |
|           @data[ver[:name]] << ver
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     # Represents a possible Specification object returned
 | |
|     # from IndexSet. Used to delay needed to download full
 | |
|     # Specification objects when only the +name+ and +version+
 | |
|     # are needed.
 | |
|     #
 | |
|     class IndexSpecification
 | |
|       def initialize(set, name, version, source, plat)
 | |
|         @set = set
 | |
|         @name = name
 | |
|         @version = version
 | |
|         @source = source
 | |
|         @platform = plat
 | |
| 
 | |
|         @spec = nil
 | |
|       end
 | |
| 
 | |
|       attr_reader :name, :version, :source
 | |
| 
 | |
|       def full_name
 | |
|         "#{@name}-#{@version}"
 | |
|       end
 | |
| 
 | |
|       def spec
 | |
|         @spec ||= @set.load_spec(@name, @version, @source)
 | |
|       end
 | |
| 
 | |
|       def dependencies
 | |
|         spec.dependencies
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     # The global rubygems pool represented via the traditional
 | |
|     # source index.
 | |
|     #
 | |
|     class IndexSet
 | |
|       def initialize
 | |
|         @f = Gem::SpecFetcher.fetcher
 | |
| 
 | |
|         @all = Hash.new { |h,k| h[k] = [] }
 | |
| 
 | |
|         list, _ = @f.available_specs(:released)
 | |
|         list.each do |uri, specs|
 | |
|           specs.each do |n|
 | |
|             @all[n.name] << [uri, n]
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         @specs = {}
 | |
|       end
 | |
| 
 | |
|       # Return an array of IndexSpecification objects matching
 | |
|       # DependencyRequest +req+.
 | |
|       #
 | |
|       def find_all(req)
 | |
|         res = []
 | |
| 
 | |
|         name = req.dependency.name
 | |
| 
 | |
|         @all[name].each do |uri, n|
 | |
|           if req.dependency.match? n
 | |
|             res << IndexSpecification.new(self, n.name, n.version,
 | |
|                                           uri, n.platform)
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         res
 | |
|       end
 | |
| 
 | |
|       # No prefetching needed since we load the whole index in
 | |
|       # initially.
 | |
|       #
 | |
|       def prefetch(gems)
 | |
|       end
 | |
| 
 | |
|       # Called from IndexSpecification to get a true Specification
 | |
|       # object.
 | |
|       #
 | |
|       def load_spec(name, ver, source)
 | |
|         key = "#{name}-#{ver}"
 | |
|         @specs[key] ||= source.fetch_spec(Gem::NameTuple.new(name, ver))
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     # A set which represents the installed gems. Respects
 | |
|     # all the normal settings that control where to look
 | |
|     # for installed gems.
 | |
|     #
 | |
|     class CurrentSet
 | |
|       def find_all(req)
 | |
|         req.dependency.matching_specs
 | |
|       end
 | |
| 
 | |
|       def prefetch(gems)
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     # Create DependencyResolver object which will resolve
 | |
|     # the tree starting with +needed+ Depedency objects.
 | |
|     #
 | |
|     # +set+ is an object that provides where to look for
 | |
|     # specifications to satisify the Dependencies. This
 | |
|     # defaults to IndexSet, which will query rubygems.org.
 | |
|     #
 | |
|     def initialize(needed, set=IndexSet.new)
 | |
|       @set = set || IndexSet.new # Allow nil to mean IndexSet
 | |
|       @needed = needed
 | |
| 
 | |
|       @conflicts = nil
 | |
|     end
 | |
| 
 | |
|     # Provide a DependencyResolver that queries only against
 | |
|     # the already installed gems.
 | |
|     #
 | |
|     def self.for_current_gems(needed)
 | |
|       new needed, CurrentSet.new
 | |
|     end
 | |
| 
 | |
|     # Contains all the conflicts encountered while doing resolution
 | |
|     #
 | |
|     attr_reader :conflicts
 | |
| 
 | |
|     # Proceed with resolution! Returns an array of ActivationRequest
 | |
|     # objects.
 | |
|     #
 | |
|     def resolve
 | |
|       @conflicts = []
 | |
| 
 | |
|       needed = @needed.map { |n| DependencyRequest.new(n, nil) }
 | |
| 
 | |
|       res = resolve_for needed, []
 | |
| 
 | |
|       if res.kind_of? DependencyConflict
 | |
|         raise DependencyResolutionError.new(res)
 | |
|       end
 | |
| 
 | |
|       res
 | |
|     end
 | |
| 
 | |
|     # Used internally to indicate that a dependency conflicted
 | |
|     # with a spec that would be activated.
 | |
|     #
 | |
|     class DependencyConflict
 | |
|       def initialize(dependency, activated, failed_dep=dependency)
 | |
|         @dependency = dependency
 | |
|         @activated = activated
 | |
|         @failed_dep = failed_dep
 | |
|       end
 | |
| 
 | |
|       attr_reader :dependency, :activated
 | |
| 
 | |
|       # Return the Specification that listed the dependency
 | |
|       #
 | |
|       def requester
 | |
|         @failed_dep.requester
 | |
|       end
 | |
| 
 | |
|       def for_spec?(spec)
 | |
|         @dependency.name == spec.name
 | |
|       end
 | |
| 
 | |
|       # Return the 2 dependency objects that conflicted
 | |
|       #
 | |
|       def conflicting_dependencies
 | |
|         [@failed_dep.dependency, @activated.request.dependency]
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     # Used Internally. Wraps a Depedency object to also track
 | |
|     # which spec contained the Dependency.
 | |
|     #
 | |
|     class DependencyRequest
 | |
|       def initialize(dep, act)
 | |
|         @dependency = dep
 | |
|         @requester = act
 | |
|       end
 | |
| 
 | |
|       attr_reader :dependency, :requester
 | |
| 
 | |
|       def name
 | |
|         @dependency.name
 | |
|       end
 | |
| 
 | |
|       def matches_spec?(spec)
 | |
|         @dependency.matches_spec? spec
 | |
|       end
 | |
| 
 | |
|       def to_s
 | |
|         @dependency.to_s
 | |
|       end
 | |
| 
 | |
|       def ==(other)
 | |
|         case other
 | |
|         when Dependency
 | |
|           @dependency == other
 | |
|         when DependencyRequest
 | |
|           @dependency == other.dep && @requester == other.requester
 | |
|         else
 | |
|           false
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     # Specifies a Specification object that should be activated.
 | |
|     # Also contains a dependency that was used to introduce this
 | |
|     # activation.
 | |
|     #
 | |
|     class ActivationRequest
 | |
|       def initialize(spec, req, others_possible=true)
 | |
|         @spec = spec
 | |
|         @request = req
 | |
|         @others_possible = others_possible
 | |
|       end
 | |
| 
 | |
|       attr_reader :spec, :request
 | |
| 
 | |
|       # Indicate if this activation is one of a set of possible
 | |
|       # requests for the same Dependency request.
 | |
|       #
 | |
|       def others_possible?
 | |
|         @others_possible
 | |
|       end
 | |
| 
 | |
|       # Return the ActivationRequest that contained the dependency
 | |
|       # that we were activated for.
 | |
|       #
 | |
|       def parent
 | |
|         @request.requester
 | |
|       end
 | |
| 
 | |
|       def name
 | |
|         @spec.name
 | |
|       end
 | |
| 
 | |
|       def full_name
 | |
|         @spec.full_name
 | |
|       end
 | |
| 
 | |
|       def version
 | |
|         @spec.version
 | |
|       end
 | |
| 
 | |
|       def full_spec
 | |
|         Gem::Specification === @spec ? @spec : @spec.spec
 | |
|       end
 | |
| 
 | |
|       def download(path)
 | |
|         if @spec.respond_to? :source
 | |
|           source = @spec.source
 | |
|         else
 | |
|           source = Gem.sources.first
 | |
|         end
 | |
| 
 | |
|         source.download full_spec, path
 | |
|       end
 | |
| 
 | |
|       def ==(other)
 | |
|         case other
 | |
|         when Gem::Specification
 | |
|           @spec == other
 | |
|         when ActivationRequest
 | |
|           @spec == other.spec && @request == other.request
 | |
|         else
 | |
|           false
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       ##
 | |
|       # Indicates if the requested gem has already been installed.
 | |
| 
 | |
|       def installed?
 | |
|         this_spec = full_spec
 | |
| 
 | |
|         Gem::Specification.any? do |s|
 | |
|           s == this_spec
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     def requests(s, act)
 | |
|       reqs = []
 | |
|       s.dependencies.each do |d|
 | |
|         next unless d.type == :runtime
 | |
|         reqs << DependencyRequest.new(d, act)
 | |
|       end
 | |
| 
 | |
|       @set.prefetch(reqs)
 | |
| 
 | |
|       reqs
 | |
|     end
 | |
| 
 | |
|     # The meat of the algorithm. Given +needed+ DependencyRequest objects
 | |
|     # and +specs+ being a list to ActivationRequest, calculate a new list
 | |
|     # of ActivationRequest objects.
 | |
|     #
 | |
|     def resolve_for(needed, specs)
 | |
|       until needed.empty?
 | |
|         dep = needed.shift
 | |
| 
 | |
|         # If there is already a spec activated for the requested name...
 | |
|         if existing = specs.find { |s| dep.name == s.name }
 | |
| 
 | |
|           # then we're done since this new dep matches the
 | |
|           # existing spec.
 | |
|           next if dep.matches_spec? existing
 | |
| 
 | |
|           # There is a conflict! We return the conflict
 | |
|           # object which will be seen by the caller and be
 | |
|           # handled at the right level.
 | |
| 
 | |
|           # If the existing activation indicates that there
 | |
|           # are other possibles for it, then issue the conflict
 | |
|           # on the dep for the activation itself. Otherwise, issue
 | |
|           # it on the requester's request itself.
 | |
|           #
 | |
|           if existing.others_possible?
 | |
|             conflict = DependencyConflict.new(dep, existing)
 | |
|           else
 | |
|             depreq = existing.request.requester.request
 | |
|             conflict = DependencyConflict.new(depreq, existing, dep)
 | |
|           end
 | |
|           @conflicts << conflict
 | |
| 
 | |
|           return conflict
 | |
|         end
 | |
| 
 | |
|         # Get a list of all specs that satisfy dep
 | |
|         possible = @set.find_all(dep)
 | |
| 
 | |
|         case possible.size
 | |
|         when 0
 | |
|           # If there are none, then our work here is done.
 | |
|           raise UnsatisfiableDepedencyError.new(dep)
 | |
|         when 1
 | |
|           # If there is one, then we just add it to specs
 | |
|           # and process the specs dependencies by adding
 | |
|           # them to needed.
 | |
| 
 | |
|           spec = possible.first
 | |
|           act =  ActivationRequest.new(spec, dep, false)
 | |
| 
 | |
|           specs << act
 | |
| 
 | |
|           # Put the deps for at the beginning of needed
 | |
|           # rather than the end to match the depth first
 | |
|           # searching done by the multiple case code below.
 | |
|           #
 | |
|           # This keeps the error messages consistent.
 | |
|           needed = requests(spec, act) + needed
 | |
|         else
 | |
|           # There are multiple specs for this dep. This is
 | |
|           # the case that this class is built to handle.
 | |
| 
 | |
|           # Sort them so that we try the highest versions
 | |
|           # first.
 | |
|           possible = possible.sort_by { |s| s.version }
 | |
| 
 | |
|           # We track the conflicts seen so that we can report them
 | |
|           # to help the user figure out how to fix the situation.
 | |
|           conflicts = []
 | |
| 
 | |
|           # To figure out which to pick, we keep resolving
 | |
|           # given each one being activated and if there isn't
 | |
|           # a conflict, we know we've found a full set.
 | |
|           #
 | |
|           # We use an until loop rather than #reverse_each
 | |
|           # to keep the stack short since we're using a recursive
 | |
|           # algorithm.
 | |
|           #
 | |
|           until possible.empty?
 | |
|             s = possible.pop
 | |
| 
 | |
|             # Recursively call #resolve_for with this spec
 | |
|             # and add it's dependencies into the picture...
 | |
| 
 | |
|             act = ActivationRequest.new(s, dep)
 | |
| 
 | |
|             try = requests(s, act) + needed
 | |
| 
 | |
|             res = resolve_for(try, specs + [act])
 | |
| 
 | |
|             # While trying to resolve these dependencies, there may
 | |
|             # be a conflict!
 | |
| 
 | |
|             if res.kind_of? DependencyConflict
 | |
|               # The conflict might be created not by this invocation
 | |
|               # but rather one up the stack, so if we can't attempt
 | |
|               # to resolve this conflict (conflict isn't with the spec +s+)
 | |
|               # then just return it so the caller can try to sort it out.
 | |
|               return res unless res.for_spec? s
 | |
| 
 | |
|               # Otherwise, this is a conflict that we can attempt to fix
 | |
|               conflicts << [s, res]
 | |
| 
 | |
|               # Optimization:
 | |
|               #
 | |
|               # Because the conflict indicates the dependency that trigger
 | |
|               # it, we can prune possible based on this new information.
 | |
|               #
 | |
|               # This cuts down on the number of iterations needed.
 | |
|               possible.delete_if { |x| !res.dependency.matches_spec? x }
 | |
|             else
 | |
|               # No conflict, return the specs
 | |
|               return res
 | |
|             end
 | |
|           end
 | |
| 
 | |
|           # We tried all possibles and nothing worked, so we let the user
 | |
|           # know and include as much information about the problem since
 | |
|           # the user is going to have to take action to fix this.
 | |
|           raise ImpossibleDependenciesError.new(dep, conflicts)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       specs
 | |
|     end
 | |
|   end
 | |
| end
 |