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 'fileutils'
8require 'rubygems'
9require 'rubygems/dependency_list'
10require 'rubygems/rdoc'
11require 'rubygems/user_interaction'
12
13##
14# An Uninstaller.
15#
16# The uninstaller fires pre and post uninstall hooks.  Hooks can be added
17# either through a rubygems_plugin.rb file in an installed gem or via a
18# rubygems/defaults/#{RUBY_ENGINE}.rb or rubygems/defaults/operating_system.rb
19# file.  See Gem.pre_uninstall and Gem.post_uninstall for details.
20
21class Gem::Uninstaller
22
23  include Gem::UserInteraction
24
25  ##
26  # The directory a gem's executables will be installed into
27
28  attr_reader :bin_dir
29
30  ##
31  # The gem repository the gem will be installed into
32
33  attr_reader :gem_home
34
35  ##
36  # The Gem::Specification for the gem being uninstalled, only set during
37  # #uninstall_gem
38
39  attr_reader :spec
40
41  ##
42  # Constructs an uninstaller that will uninstall +gem+
43
44  def initialize(gem, options = {})
45    # TODO document the valid options
46    @gem               = gem
47    @version           = options[:version] || Gem::Requirement.default
48    @gem_home          = File.expand_path(options[:install_dir] || Gem.dir)
49    @force_executables = options[:executables]
50    @force_all         = options[:all]
51    @force_ignore      = options[:ignore]
52    @bin_dir           = options[:bin_dir]
53    @format_executable = options[:format_executable]
54
55    # Indicate if development dependencies should be checked when
56    # uninstalling. (default: false)
57    #
58    @check_dev         = options[:check_dev]
59
60    if options[:force]
61      @force_all = true
62      @force_ignore = true
63    end
64
65    # only add user directory if install_dir is not set
66    @user_install = false
67    @user_install = options[:user_install] unless options[:install_dir]
68  end
69
70  ##
71  # Performs the uninstall of the gem.  This removes the spec, the Gem
72  # directory, and the cached .gem file.
73
74  def uninstall
75    dependency = Gem::Dependency.new @gem, @version
76
77    list = []
78
79    dirs =
80      Gem::Specification.dirs +
81      [Gem::Specification.default_specifications_dir]
82
83    Gem::Specification.each_spec dirs do |spec|
84      next unless dependency.matches_spec? spec
85
86      list << spec
87    end
88
89    default_specs, list = list.partition do |spec|
90      spec.default_gem?
91    end
92
93    list, other_repo_specs = list.partition do |spec|
94      @gem_home == spec.base_dir or
95        (@user_install and spec.base_dir == Gem.user_dir)
96    end
97
98    if list.empty? then
99      if other_repo_specs.empty?
100        if default_specs.empty?
101          raise Gem::InstallError, "gem #{@gem.inspect} is not installed"
102        else
103          message =
104            "gem #{@gem.inspect} cannot be uninstalled " +
105            "because it is a default gem"
106          raise Gem::InstallError, message
107        end
108      end
109
110      other_repos = other_repo_specs.map { |spec| spec.base_dir }.uniq
111
112      message = ["#{@gem} is not installed in GEM_HOME, try:"]
113      message.concat other_repos.map { |repo|
114        "\tgem uninstall -i #{repo} #{@gem}"
115      }
116
117      raise Gem::InstallError, message.join("\n")
118    elsif @force_all then
119      remove_all list
120
121    elsif list.size > 1 then
122      gem_names = list.collect {|gem| gem.full_name} + ["All versions"]
123
124      say
125      _, index = choose_from_list "Select gem to uninstall:", gem_names
126
127      if index == list.size then
128        remove_all list
129      elsif index >= 0 && index < list.size then
130        uninstall_gem list[index]
131      else
132        say "Error: must enter a number [1-#{list.size+1}]"
133      end
134    else
135      uninstall_gem list.first
136    end
137  end
138
139  ##
140  # Uninstalls gem +spec+
141
142  def uninstall_gem(spec)
143    @spec = spec
144
145    unless dependencies_ok? spec
146      unless ask_if_ok(spec)
147        raise Gem::DependencyRemovalException,
148          "Uninstallation aborted due to dependent gem(s)"
149      end
150    end
151
152    Gem.pre_uninstall_hooks.each do |hook|
153      hook.call self
154    end
155
156    remove_executables @spec
157    remove @spec
158
159    Gem.post_uninstall_hooks.each do |hook|
160      hook.call self
161    end
162
163    @spec = nil
164  end
165
166  ##
167  # Removes installed executables and batch files (windows only) for
168  # +gemspec+.
169
170  def remove_executables(spec)
171    return if spec.nil? or spec.executables.empty?
172
173    executables = spec.executables.clone
174
175    # Leave any executables created by other installed versions
176    # of this gem installed.
177
178    list = Gem::Specification.find_all { |s|
179      s.name == spec.name && s.version != spec.version
180    }
181
182    list.each do |s|
183      s.executables.each do |exe_name|
184        executables.delete exe_name
185      end
186    end
187
188    return if executables.empty?
189
190    executables = executables.map { |exec| formatted_program_filename exec }
191
192    remove = if @force_executables.nil? then
193               ask_yes_no("Remove executables:\n" +
194                          "\t#{executables.join ', '}\n\n" +
195                          "in addition to the gem?",
196                          true)
197             else
198               @force_executables
199             end
200
201    if remove then
202      bin_dir = @bin_dir || Gem.bindir(spec.base_dir)
203
204      raise Gem::FilePermissionError, bin_dir unless File.writable? bin_dir
205
206      executables.each do |exe_name|
207        say "Removing #{exe_name}"
208
209        exe_file = File.join bin_dir, exe_name
210
211        FileUtils.rm_f exe_file
212        FileUtils.rm_f "#{exe_file}.bat"
213      end
214    else
215      say "Executables and scripts will remain installed."
216    end
217  end
218
219  ##
220  # Removes all gems in +list+.
221  #
222  # NOTE: removes uninstalled gems from +list+.
223
224  def remove_all(list)
225    list.each { |spec| uninstall_gem spec }
226  end
227
228  ##
229  # spec:: the spec of the gem to be uninstalled
230  # list:: the list of all such gems
231  #
232  # Warning: this method modifies the +list+ parameter.  Once it has
233  # uninstalled a gem, it is removed from that list.
234
235  def remove(spec)
236    unless path_ok?(@gem_home, spec) or
237           (@user_install and path_ok?(Gem.user_dir, spec)) then
238      e = Gem::GemNotInHomeException.new \
239            "Gem is not installed in directory #{@gem_home}"
240      e.spec = spec
241
242      raise e
243    end
244
245    raise Gem::FilePermissionError, spec.base_dir unless
246      File.writable?(spec.base_dir)
247
248    FileUtils.rm_rf spec.full_gem_path
249
250    # TODO: should this be moved to spec?... I vote eww (also exists in docmgr)
251    old_platform_name = [spec.name,
252                         spec.version,
253                         spec.original_platform].join '-'
254
255    gemspec = spec.spec_file
256
257    unless File.exist? gemspec then
258      gemspec = File.join(File.dirname(gemspec), "#{old_platform_name}.gemspec")
259    end
260
261    FileUtils.rm_rf gemspec
262
263    gem = spec.cache_file
264    gem = File.join(spec.cache_dir, "#{old_platform_name}.gem") unless
265      File.exist? gem
266
267    FileUtils.rm_rf gem
268
269    Gem::RDoc.new(spec).remove
270
271    say "Successfully uninstalled #{spec.full_name}"
272
273    Gem::Specification.remove_spec spec
274  end
275
276  ##
277  # Is +spec+ in +gem_dir+?
278
279  def path_ok?(gem_dir, spec)
280    full_path     = File.join gem_dir, 'gems', spec.full_name
281    original_path = File.join gem_dir, 'gems', spec.original_name
282
283    full_path == spec.full_gem_path || original_path == spec.full_gem_path
284  end
285
286  def dependencies_ok?(spec)
287    return true if @force_ignore
288
289    deplist = Gem::DependencyList.from_specs
290    deplist.ok_to_remove?(spec.full_name, @check_dev)
291  end
292
293  def ask_if_ok(spec)
294    msg = ['']
295    msg << 'You have requested to uninstall the gem:'
296    msg << "\t#{spec.full_name}"
297    msg << ''
298
299    siblings = Gem::Specification.select do |s|
300                 s.name == spec.name && s.full_name != spec.full_name
301               end
302
303    spec.dependent_gems.each do |dep_spec, dep, satlist|
304      unless siblings.any? { |s| s.satisfies_requirement? dep }
305        msg << "#{dep_spec.name}-#{dep_spec.version} depends on #{dep}"
306      end
307    end
308
309    msg << 'If you remove this gem, these dependencies will not be met.'
310    msg << 'Continue with Uninstall?'
311    return ask_yes_no(msg.join("\n"), false)
312  end
313
314  def formatted_program_filename(filename)
315    # TODO perhaps the installer should leave a small manifest
316    # of what it did for us to find rather than trying to recreate
317    # it again.
318    if @format_executable then
319      require 'rubygems/installer'
320      Gem::Installer.exec_format % File.basename(filename)
321    else
322      filename
323    end
324  end
325end
326