1#--
2# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
3# All rights reserved.
4# See LICENSE.txt for permissions.
5#++
6
7require 'tsort'
8require 'rubygems/deprecate'
9
10##
11# Gem::DependencyList is used for installing and uninstalling gems in the
12# correct order to avoid conflicts.
13#--
14# TODO: It appears that all but topo-sort functionality is being duplicated
15# (or is planned to be duplicated) elsewhere in rubygems.  Is the majority of
16# this class necessary anymore?  Especially #ok?, #why_not_ok?
17
18class Gem::DependencyList
19  attr_reader :specs
20
21  include Enumerable
22  include TSort
23
24  ##
25  # Allows enabling/disabling use of development dependencies
26
27  attr_accessor :development
28
29  ##
30  # Creates a DependencyList from the current specs.
31
32  def self.from_specs
33    list = new
34    list.add(*Gem::Specification.to_a)
35    list
36  end
37
38  ##
39  # Creates a new DependencyList.  If +development+ is true, development
40  # dependencies will be included.
41
42  def initialize development = false
43    @specs = []
44
45    @development = development
46  end
47
48  ##
49  # Adds +gemspecs+ to the dependency list.
50
51  def add(*gemspecs)
52    @specs.concat gemspecs
53  end
54
55  def clear
56    @specs.clear
57  end
58
59  ##
60  # Return a list of the gem specifications in the dependency list, sorted in
61  # order so that no gemspec in the list depends on a gemspec earlier in the
62  # list.
63  #
64  # This is useful when removing gems from a set of installed gems.  By
65  # removing them in the returned order, you don't get into as many dependency
66  # issues.
67  #
68  # If there are circular dependencies (yuck!), then gems will be returned in
69  # order until only the circular dependents and anything they reference are
70  # left.  Then arbitrary gemspecs will be returned until the circular
71  # dependency is broken, after which gems will be returned in dependency
72  # order again.
73
74  def dependency_order
75    sorted = strongly_connected_components.flatten
76
77    result = []
78    seen = {}
79
80    sorted.each do |spec|
81      if index = seen[spec.name] then
82        if result[index].version < spec.version then
83          result[index] = spec
84        end
85      else
86        seen[spec.name] = result.length
87        result << spec
88      end
89    end
90
91    result.reverse
92  end
93
94  ##
95  # Iterator over dependency_order
96
97  def each(&block)
98    dependency_order.each(&block)
99  end
100
101  def find_name(full_name)
102    @specs.find { |spec| spec.full_name == full_name }
103  end
104
105  def inspect # :nodoc:
106    "#<%s:0x%x %p>" % [self.class, object_id, map { |s| s.full_name }]
107  end
108
109  ##
110  # Are all the dependencies in the list satisfied?
111
112  def ok?
113    why_not_ok?(:quick).empty?
114  end
115
116  def why_not_ok? quick = false
117    unsatisfied = Hash.new { |h,k| h[k] = [] }
118    each do |spec|
119      spec.runtime_dependencies.each do |dep|
120        inst = Gem::Specification.any? { |installed_spec|
121          dep.name == installed_spec.name and
122            dep.requirement.satisfied_by? installed_spec.version
123        }
124
125        unless inst or @specs.find { |s| s.satisfies_requirement? dep } then
126          unsatisfied[spec.name] << dep
127          return unsatisfied if quick
128        end
129      end
130    end
131
132    unsatisfied
133  end
134
135  ##
136  # Is is ok to remove a gemspec from the dependency list?
137  #
138  # If removing the gemspec creates breaks a currently ok dependency, then it
139  # is NOT ok to remove the gemspec.
140
141  def ok_to_remove?(full_name, check_dev=true)
142    gem_to_remove = find_name full_name
143
144    siblings = @specs.find_all { |s|
145      s.name == gem_to_remove.name &&
146        s.full_name != gem_to_remove.full_name
147    }
148
149    deps = []
150
151    @specs.each do |spec|
152      check = check_dev ? spec.dependencies : spec.runtime_dependencies
153
154      check.each do |dep|
155        deps << dep if gem_to_remove.satisfies_requirement?(dep)
156      end
157    end
158
159    deps.all? { |dep|
160      siblings.any? { |s|
161        s.satisfies_requirement? dep
162      }
163    }
164  end
165
166  ##
167  # Remove everything in the DependencyList that matches but doesn't
168  # satisfy items in +dependencies+ (a hash of gem names to arrays of
169  # dependencies).
170
171  def remove_specs_unsatisfied_by dependencies
172    specs.reject! { |spec|
173      dep = dependencies[spec.name]
174      dep and not dep.requirement.satisfied_by? spec.version
175    }
176  end
177
178  ##
179  # Removes the gemspec matching +full_name+ from the dependency list
180
181  def remove_by_name(full_name)
182    @specs.delete_if { |spec| spec.full_name == full_name }
183  end
184
185  ##
186  # Return a hash of predecessors.  <tt>result[spec]</tt> is an Array of
187  # gemspecs that have a dependency satisfied by the named gemspec.
188
189  def spec_predecessors
190    result = Hash.new { |h,k| h[k] = [] }
191
192    specs = @specs.sort.reverse
193
194    specs.each do |spec|
195      specs.each do |other|
196        next if spec == other
197
198        other.dependencies.each do |dep|
199          if spec.satisfies_requirement? dep then
200            result[spec] << other
201          end
202        end
203      end
204    end
205
206    result
207  end
208
209  def tsort_each_node(&block)
210    @specs.each(&block)
211  end
212
213  def tsort_each_child(node)
214    specs = @specs.sort.reverse
215
216    dependencies = node.runtime_dependencies
217    dependencies.push(*node.development_dependencies) if @development
218
219    dependencies.each do |dep|
220      specs.each do |spec|
221        if spec.satisfies_requirement? dep then
222          begin
223            yield spec
224          rescue TSort::Cyclic
225            # do nothing
226          end
227          break
228        end
229      end
230    end
231  end
232
233  private
234
235  ##
236  # Count the number of gemspecs in the list +specs+ that are not in
237  # +ignored+.
238
239  def active_count(specs, ignored)
240    specs.count { |spec| ignored[spec.full_name].nil? }
241  end
242
243end
244
245