1##
2# The Dependency class holds a Gem name and a Gem::Requirement.
3
4require "rubygems/requirement"
5
6class Gem::Dependency
7
8  ##
9  # Valid dependency types.
10  #--
11  # When this list is updated, be sure to change
12  # Gem::Specification::CURRENT_SPECIFICATION_VERSION as well.
13  #
14  # REFACTOR: This type of constant, TYPES, indicates we might want
15  # two classes, used via inheritance or duck typing.
16
17  TYPES = [
18    :development,
19    :runtime,
20  ]
21
22  ##
23  # Dependency name or regular expression.
24
25  attr_accessor :name
26
27  ##
28  # Allows you to force this dependency to be a prerelease.
29
30  attr_writer :prerelease
31
32  ##
33  # Constructs a dependency with +name+ and +requirements+. The last
34  # argument can optionally be the dependency type, which defaults to
35  # <tt>:runtime</tt>.
36
37  def initialize name, *requirements
38    case name
39    when String then # ok
40    when Regexp then
41      msg = ["NOTE: Dependency.new w/ a regexp is deprecated.",
42             "Dependency.new called from #{Gem.location_of_caller.join(":")}"]
43      warn msg.join("\n") unless Gem::Deprecate.skip
44    else
45      raise ArgumentError,
46            "dependency name must be a String, was #{name.inspect}"
47    end
48
49    type         = Symbol === requirements.last ? requirements.pop : :runtime
50    requirements = requirements.first if 1 == requirements.length # unpack
51
52    unless TYPES.include? type
53      raise ArgumentError, "Valid types are #{TYPES.inspect}, " +
54                           "not #{type.inspect}"
55    end
56
57    @name        = name
58    @requirement = Gem::Requirement.create requirements
59    @type        = type
60    @prerelease  = false
61
62    # This is for Marshal backwards compatibility. See the comments in
63    # +requirement+ for the dirty details.
64
65    @version_requirements = @requirement
66  end
67
68  ##
69  # A dependency's hash is the XOR of the hashes of +name+, +type+,
70  # and +requirement+.
71
72  def hash # :nodoc:
73    name.hash ^ type.hash ^ requirement.hash
74  end
75
76  def inspect # :nodoc:
77    if @prerelease
78      "<%s type=%p name=%p requirements=%p prerelease=ok>" %
79        [self.class, self.type, self.name, requirement.to_s]
80    else
81      "<%s type=%p name=%p requirements=%p>" %
82        [self.class, self.type, self.name, requirement.to_s]
83    end
84  end
85
86  ##
87  # Does this dependency require a prerelease?
88
89  def prerelease?
90    @prerelease || requirement.prerelease?
91  end
92
93  ##
94  # Is this dependency simply asking for the latest version
95  # of a gem?
96
97  def latest_version?
98    @requirement.none?
99  end
100
101  def pretty_print q # :nodoc:
102    q.group 1, 'Gem::Dependency.new(', ')' do
103      q.pp name
104      q.text ','
105      q.breakable
106
107      q.pp requirement
108
109      q.text ','
110      q.breakable
111
112      q.pp type
113    end
114  end
115
116  ##
117  # What does this dependency require?
118
119  def requirement
120    return @requirement if defined?(@requirement) and @requirement
121
122    # @version_requirements and @version_requirement are legacy ivar
123    # names, and supported here because older gems need to keep
124    # working and Dependency doesn't implement marshal_dump and
125    # marshal_load. In a happier world, this would be an
126    # attr_accessor. The horrifying instance_variable_get you see
127    # below is also the legacy of some old restructurings.
128    #
129    # Note also that because of backwards compatibility (loading new
130    # gems in an old RubyGems installation), we can't add explicit
131    # marshaling to this class until we want to make a big
132    # break. Maybe 2.0.
133    #
134    # Children, define explicit marshal and unmarshal behavior for
135    # public classes. Marshal formats are part of your public API.
136
137    # REFACTOR: See above
138
139    if defined?(@version_requirement) && @version_requirement
140      version = @version_requirement.instance_variable_get :@version
141      @version_requirement  = nil
142      @version_requirements = Gem::Requirement.new version
143    end
144
145    @requirement = @version_requirements if defined?(@version_requirements)
146  end
147
148  # DOC: this method needs documentation or :nodoc''d
149  def requirements_list
150    requirement.as_list
151  end
152
153  def to_s # :nodoc:
154    if type != :runtime then
155      "#{name} (#{requirement}, #{type})"
156    else
157      "#{name} (#{requirement})"
158    end
159  end
160
161  ##
162  # Dependency type.
163
164  def type
165    @type ||= :runtime
166  end
167
168  def == other # :nodoc:
169    Gem::Dependency === other &&
170      self.name        == other.name &&
171      self.type        == other.type &&
172      self.requirement == other.requirement
173  end
174
175  ##
176  # Dependencies are ordered by name.
177
178  def <=> other
179    self.name <=> other.name
180  end
181
182  ##
183  # Uses this dependency as a pattern to compare to +other+. This
184  # dependency will match if the name matches the other's name, and
185  # other has only an equal version requirement that satisfies this
186  # dependency.
187
188  def =~ other
189    unless Gem::Dependency === other
190      return unless other.respond_to?(:name) && other.respond_to?(:version)
191      other = Gem::Dependency.new other.name, other.version
192    end
193
194    return false unless name === other.name
195
196    reqs = other.requirement.requirements
197
198    return false unless reqs.length == 1
199    return false unless reqs.first.first == '='
200
201    version = reqs.first.last
202
203    requirement.satisfied_by? version
204  end
205
206  # DOC: this method needs either documented or :nodoc'd
207
208  def match? obj, version=nil
209    if !version
210      name = obj.name
211      version = obj.version
212    else
213      name = obj
214    end
215
216    return false unless self.name === name
217    return true if requirement.none?
218
219    requirement.satisfied_by? Gem::Version.new(version)
220  end
221
222  # DOC: this method needs either documented or :nodoc'd
223
224  def matches_spec? spec
225    return false unless name === spec.name
226    return true  if requirement.none?
227
228    requirement.satisfied_by?(spec.version)
229  end
230
231  ##
232  # Merges the requirements of +other+ into this dependency
233
234  def merge other
235    unless name == other.name then
236      raise ArgumentError,
237            "#{self} and #{other} have different names"
238    end
239
240    default = Gem::Requirement.default
241    self_req  = self.requirement
242    other_req = other.requirement
243
244    return self.class.new name, self_req  if other_req == default
245    return self.class.new name, other_req if self_req  == default
246
247    self.class.new name, self_req.as_list.concat(other_req.as_list)
248  end
249
250  # DOC: this method needs either documented or :nodoc'd
251
252  def matching_specs platform_only = false
253    matches = Gem::Specification.find_all { |spec|
254      self.name === spec.name and # TODO: == instead of ===
255        requirement.satisfied_by? spec.version
256    }
257
258    if platform_only
259      matches.reject! { |spec|
260        not Gem::Platform.match spec.platform
261      }
262    end
263
264    matches = matches.sort_by { |s| s.sort_obj } # HACK: shouldn't be needed
265  end
266
267  ##
268  # True if the dependency will not always match the latest version.
269
270  def specific?
271    @requirement.specific?
272  end
273
274  # DOC: this method needs either documented or :nodoc'd
275
276  def to_specs
277    matches = matching_specs true
278
279    # TODO: check Gem.activated_spec[self.name] in case matches falls outside
280
281    if matches.empty? then
282      specs = Gem::Specification.find_all { |s|
283                s.name == name
284              }.map { |x| x.full_name }
285
286      if specs.empty?
287        total = Gem::Specification.to_a.size
288        error = Gem::LoadError.new \
289          "Could not find '#{name}' (#{requirement}) among #{total} total gem(s)"
290      else
291        error = Gem::LoadError.new \
292          "Could not find '#{name}' (#{requirement}) - did find: [#{specs.join ','}]"
293      end
294      error.name        = self.name
295      error.requirement = self.requirement
296      raise error
297    end
298
299    # TODO: any other resolver validations should go here
300
301    matches
302  end
303
304  # DOC: this method needs either documented or :nodoc'd
305
306  def to_spec
307    matches = self.to_specs
308
309    matches.find { |spec| spec.activated? } or matches.last
310  end
311end
312