summaryrefslogtreecommitdiff
path: root/lib/rubygems/commands/update_command.rb
blob: 9e9bd088f2d7e01e3791a2e3f09773f5a4d815e5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
require 'rubygems/command'
require 'rubygems/command_manager'
require 'rubygems/dependency_installer'
require 'rubygems/install_update_options'
require 'rubygems/local_remote_options'
require 'rubygems/spec_fetcher'
require 'rubygems/version_option'
require 'rubygems/install_message' # must come before rdoc for messaging
require 'rubygems/rdoc'

class Gem::Commands::UpdateCommand < Gem::Command

  include Gem::InstallUpdateOptions
  include Gem::LocalRemoteOptions
  include Gem::VersionOption

  attr_reader :installer # :nodoc:

  attr_reader :updated # :nodoc:

  def initialize
    super 'update', 'Update installed gems to the latest version',
      :document => %w[rdoc ri],
      :force    => false

    add_install_update_options

    OptionParser.accept Gem::Version do |value|
      Gem::Version.new value

      value
    end

    add_option('--system [VERSION]', Gem::Version,
               'Update the RubyGems system software') do |value, options|
      value = true unless value

      options[:system] = value
    end

    add_local_remote_options
    add_platform_option
    add_prerelease_option "as update targets"

    @updated   = []
    @installer = nil
  end

  def arguments # :nodoc:
    "REGEXP        regexp to search for in gem name"
  end

  def defaults_str # :nodoc:
    "--document --no-force --install-dir #{Gem.dir}"
  end

  def description # :nodoc:
    <<-EOF
The update command will update your gems to the latest version.

The update command does not remove the previous version. Use the cleanup
command to remove old versions.
    EOF
  end

  def usage # :nodoc:
    "#{program_name} REGEXP [REGEXP ...]"
  end

  def check_latest_rubygems version # :nodoc:
    if Gem.rubygems_version == version then
      say "Latest version currently installed. Aborting."
      terminate_interaction
    end

    options[:user_install] = false
  end

  def check_update_arguments # :nodoc:
    unless options[:args].empty? then
      alert_error "Gem names are not allowed with the --system option"
      terminate_interaction 1
    end
  end

  def execute
    hig = {}

    if options[:system] then
      update_rubygems
      return
    end

    say "Updating installed gems"

    hig = highest_installed_gems

    gems_to_update = which_to_update hig, options[:args].uniq

    updated = update_gems gems_to_update

    updated_names = updated.map { |spec| spec.name }
    not_updated_names = options[:args].uniq - updated_names

    if updated.empty? then
      say "Nothing to update"
    else
      say "Gems updated: #{updated_names.join(' ')}"
      say "Gems already up-to-date: #{not_updated_names.join(' ')}" unless not_updated_names.empty?
    end
  end

  def fetch_remote_gems spec # :nodoc:
    dependency = Gem::Dependency.new spec.name, "> #{spec.version}"
    dependency.prerelease = options[:prerelease]

    fetcher = Gem::SpecFetcher.fetcher

    spec_tuples, errors = fetcher.search_for_dependency dependency

    error = errors.find { |e| e.respond_to? :exception }

    raise error if error

    spec_tuples
  end

  def highest_installed_gems # :nodoc:
    hig = {} # highest installed gems

    Gem::Specification.each do |spec|
      if hig[spec.name].nil? or hig[spec.name].version < spec.version then
        hig[spec.name] = spec
      end
    end

    hig
  end

  def highest_remote_version spec # :nodoc:
    spec_tuples = fetch_remote_gems spec

    matching_gems = spec_tuples.select do |g,_|
      g.name == spec.name and g.match_platform?
    end

    highest_remote_gem = matching_gems.max_by { |g,_| g.version }

    highest_remote_gem ||= [Gem::NameTuple.null]

    highest_remote_gem.first.version
  end

  def install_rubygems version # :nodoc:
    args = update_rubygems_arguments

    update_dir = File.join Gem.dir, 'gems', "rubygems-update-#{version}"

    Dir.chdir update_dir do
      say "Installing RubyGems #{version}"

      # Make sure old rubygems isn't loaded
      old = ENV["RUBYOPT"]
      ENV.delete("RUBYOPT") if old
      installed = system Gem.ruby, 'setup.rb', *args
      say "RubyGems system software updated" if installed
      ENV["RUBYOPT"] = old if old
    end
  end

  def rubygems_target_version
    version = options[:system]
    update_latest = version == true

    if update_latest then
      version     = Gem::Version.new     Gem::VERSION
      requirement = Gem::Requirement.new ">= #{Gem::VERSION}"
    else
      version     = Gem::Version.new     version
      requirement = Gem::Requirement.new version
    end

    rubygems_update         = Gem::Specification.new
    rubygems_update.name    = 'rubygems-update'
    rubygems_update.version = version

    hig = {
      'rubygems-update' => rubygems_update
    }

    gems_to_update = which_to_update hig, options[:args], :system
    _, up_ver   = gems_to_update.first

    target = if update_latest then
               up_ver
             else
               version
             end

    return target, requirement
  end

  def update_gem name, version = Gem::Requirement.default
    return if @updated.any? { |spec| spec.name == name }

    @installer ||= Gem::DependencyInstaller.new options

    say "Updating #{name}"
    begin
      @installer.install name, Gem::Requirement.new(version)
    rescue Gem::InstallError, Gem::DependencyError => e
      alert_error "Error installing #{name}:\n\t#{e.message}"
    end

    @installer.installed_gems.each do |spec|
      @updated << spec
    end
  end

  def update_gems gems_to_update
    gems_to_update.uniq.sort.each do |(name, version)|
      update_gem name, version
    end

    @updated
  end

  ##
  # Update RubyGems software to the latest version.

  def update_rubygems
    check_update_arguments

    version, requirement = rubygems_target_version

    check_latest_rubygems version

    update_gem 'rubygems-update', version

    installed_gems = Gem::Specification.find_all_by_name 'rubygems-update', requirement
    version        = installed_gems.last.version

    install_rubygems version
  end

  def update_rubygems_arguments # :nodoc:
    args = []
    args << '--prefix' << Gem.prefix if Gem.prefix
    # TODO use --document for >= 1.9 , --no-rdoc --no-ri < 1.9
    args << '--no-rdoc' unless options[:document].include? 'rdoc'
    args << '--no-ri'   unless options[:document].include? 'ri'
    args << '--no-format-executable' if options[:no_format_executable]
    args << '--previous-version' << Gem::VERSION if
      options[:system] == true or
        Gem::Version.new(options[:system]) >= Gem::Version.new(2)
    args
  end

  def which_to_update highest_installed_gems, gem_names, system = false
    result = []

    highest_installed_gems.each do |l_name, l_spec|
      next if not gem_names.empty? and
              gem_names.none? { |name| name == l_spec.name }

      highest_remote_ver = highest_remote_version l_spec

      if system or (l_spec.version < highest_remote_ver) then
        result << [l_spec.name, [l_spec.version, highest_remote_ver].max]
      end
    end

    result
  end

end