summaryrefslogtreecommitdiffstats
path: root/amarok/src/mediadevice/daap/mongrel/lib/rubygems/remote_installer.rb
blob: 4027d65db01d438cf20d0b963196192edc9150ab (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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++

require 'rubygems'
require 'socket'
require 'fileutils'

module Gem
  class DependencyError < Gem::Exception; end
  class RemoteSourceException < Gem::Exception; end
  class GemNotFoundException < Gem::Exception; end
  class RemoteInstallationCancelled < Gem::Exception; end

  ####################################################################
  # RemoteSourceFetcher handles the details of fetching gems and gem
  # information from a remote source.  
  class RemoteSourceFetcher
    include UserInteraction

    # Initialize a remote fetcher using the source URI (and possible
    # proxy information).  
    # +proxy+
    # * [String]: explicit specification of proxy; overrides any
    #   environment variable setting
    # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER, HTTP_PROXY_PASS)
    # * <tt>:no_proxy</tt>: ignore environment variables and _don't_
    #   use a proxy
    def initialize(source_uri, proxy)
      @uri = normalize_uri(source_uri)
      @proxy_uri =
      case proxy
      when :no_proxy
        nil
      when nil
        env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
        uri = env_proxy ? URI.parse(env_proxy) : nil
        if uri and uri.user.nil? and uri.password.nil?
          #Probably we have http_proxy_* variables?
          uri.user = ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER']
          uri.password = ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS']
        end
        uri
      else
        URI.parse(proxy.to_str)
      end
    end

    # The uncompressed +size+ of the source's directory (e.g. source
    # info).
    def size
      @size ||= get_size("/yaml")
    end

    # Fetch the data from the source at the given path.
    def fetch_path(path="")
      read_data(@uri + path)
    end

    # Get the source index from the gem source.  The source index is a
    # directory of the gems available on the source, formatted as a
    # Gem::Cache object.  The cache object allows easy searching for
    # gems by name and version requirement.
    #
    # Notice that the gem specs in the cache are adequate for searches
    # and queries, but may have some information elided (hence
    # "abbreviated").
    def source_index
      say "Bulk updating Gem source index for: #{@uri}"
      begin
        require 'zlib'
        yaml_spec = fetch_path("/yaml.Z")
        yaml_spec = Zlib::Inflate.inflate(yaml_spec)
      rescue
        yaml_spec = nil
      end
      begin
	yaml_spec = fetch_path("/yaml") unless yaml_spec
	convert_spec(yaml_spec)
      rescue SocketError => e
	raise RemoteSourceException.new("Error fetching remote gem cache: #{e.to_s}")
      end
    end

    private

    # Normalize the URI by adding "http://" if it is missing.
    def normalize_uri(uri)
      (uri =~ /^(https?|ftp|file):/) ? uri : "http://#{uri}"
    end

    # Connect to the source host/port, using a proxy if needed.
    def connect_to(host, port)
      if @proxy_uri
        Net::HTTP::Proxy(@proxy_uri.host, @proxy_uri.port, @proxy_uri.user, @proxy_uri.password).new(host, port)
      else
	Net::HTTP.new(host, port)
      end
    end
    
    # Get the size of the (non-compressed) data from the source at the
    # given path.
    def get_size(path)
      read_size(@uri + path)
    end

    # Read the size of the (source based) URI using an HTTP HEAD
    # command.
    def read_size(uri)
      return File.size(get_file_uri_path(uri)) if is_file_uri(uri)
      require 'net/http'
      require 'uri'
      u = URI.parse(uri)
      http = connect_to(u.host, u.port)
      path = (u.path == "") ? "/" : u.path
      resp = http.head(path)
      fail RemoteSourceException, "HTTP Response #{resp.code}" if resp.code !~ /^2/
      resp['content-length'].to_i
    end

    # Read the data from the (source based) URI.
    def read_data(uri)
      begin
    	open_uri_or_path(uri) do |input|
    	  input.read
    	end
      rescue
    	old_uri = uri
    	uri = uri.downcase
    	retry if old_uri != uri
    	raise
      end
    end
    
    # Read the data from the (source based) URI, but if it is a
    # file:// URI, read from the filesystem instead.
    def open_uri_or_path(uri, &block)
      require 'rubygems/open-uri'
      if is_file_uri(uri)
        open(get_file_uri_path(uri), &block)
      else
        connection_options = {"User-Agent" => "RubyGems/#{Gem::RubyGemsVersion}"}
        if @proxy_uri
          http_proxy_url = "#{@proxy_uri.scheme}://#{@proxy_uri.host}:#{@proxy_uri.port}"  
          connection_options[:proxy_http_basic_authentication] = [http_proxy_url, @proxy_uri.user||'', @proxy_uri.password||'']
        end
        
        open(uri, connection_options, &block)
      end
    end
    
    # Checks if the provided string is a file:// URI.
    def is_file_uri(uri)
      uri =~ %r{\Afile://}
    end
    
    # Given a file:// URI, returns its local path.
    def get_file_uri_path(uri)
      uri.sub(%r{\Afile://}, '')
    end
    
    # Convert the yamlized string spec into a real spec (actually,
    # these are hashes of specs.).
    def convert_spec(yaml_spec)
      YAML.load(reduce_spec(yaml_spec)) or
	fail "Didn't get a valid YAML document"
    end

    # This reduces the source spec in size so that YAML bugs with
    # large data sets will be dodged.  Obviously this is a workaround,
    # but it allows Gems to continue to work until the YAML bug is
    # fixed.  
    def reduce_spec(yaml_spec)
      result = ""
      state = :copy
      yaml_spec.each do |line|
	if state == :copy && line =~ /^\s+files:\s*$/
	  state = :skip
	  result << line.sub(/$/, " []")
	elsif state == :skip
	  if line !~ /^\s+-/
	    state = :copy
	  end
	end
	result << line if state == :copy
      end
      result
    end

    class << self
      # Sent by the client when it is done with all the sources,
      # allowing any cleanup activity to take place.
      def finish
	# Nothing to do
      end
    end
  end

  ####################################################################
  # Entrys held by a SourceInfoCache.
  class SourceInfoCacheEntry
    # The source index for this cache entry.
    attr_reader :source_index

    # The size of the of the source entry.  Used to determine if the
    # source index has changed.
    attr_reader :size

    # Create a cache entry.
    def initialize(si, size)
      replace_source_index(si, size)
    end

    # Replace the source index and the index size with given values.
    def replace_source_index(si, size)
      @source_index = si || SourceIndex.new({})
      @size = size
    end
  end

  ####################################################################
  # SourceInfoCache implements the cache management policy on where
  # the source info is stored on local file system.  There are two
  # possible cache locations: (1) the system wide cache, and (2) the
  # user specific cache.
  #
  # * The system cache is prefered if it is writable (or can be
  #   created).
  # * The user cache is used if the system cache is not writable (or
  #   can not be created).
  #
  # Once a cache is selected, it will be used for all operations.  It
  # will not switch between cache files dynamically.
  #
  # Cache data is a simple hash indexed by the source URI.  Retrieving
  # and entry from the cache data will return a SourceInfoCacheEntry.
  #
  class SourceInfoCache

    # The most recent cache data.
    def cache_data
      @dirty = false
      @cache_data ||= read_cache 
    end

    # Write data to the proper cache.
    def write_cache
      data = cache_data
      open(writable_file, "wb") do |f|
        f.write Marshal.dump(data)
      end
    end

    # The name of the system cache file.
    def system_cache_file
      @sysetm_cache ||= File.join(Gem.dir, "source_cache")
    end

    # The name of the user cache file.
    def user_cache_file
      @user_cache ||=
	ENV['GEMCACHE'] || File.join(Gem.user_home, ".gem/source_cache")
    end

    # Mark the cache as updated (i.e. dirty).
    def update
      @dirty = true
    end

    # Write the cache to a local file (if it is dirty).
    def flush
      write_cache if @dirty
      @dirty = false
    end

    private 

    # Find a writable cache file.
    def writable_file
      @cache_file
    end

    # Read the most current cache data.
    def read_cache
      @cache_file = select_cache_file
      begin
	open(@cache_file, "rb") { |f| load_local_cache(f) } || {}
      rescue StandardError => ex
	{}
      end
    end

    def load_local_cache(f)
      Marshal.load(f)
    end

    # Select a writable cache file
    def select_cache_file
      try_file(system_cache_file) or
	try_file(user_cache_file) or
	fail "Unable to locate a writable cache file."
    end

    # Determine if +fn+ is a candidate for a cache file.  Return fn if
    # it is.  Return nil if it is not.
    def try_file(fn)
      return fn if File.writable?(fn)
      return nil if File.exist?(fn)
      dir = File.dirname(fn)
      if ! File.exist? dir
	begin
	  FileUtils.mkdir_p(dir)
	rescue RuntimeError
	  return nil
	end
      end
      if File.writable?(dir)
	FileUtils.touch fn
	return fn
      end
      nil
    end
  end

  ####################################################################
  # CachedFetcher is a decorator that adds local file caching to
  # RemoteSourceFetcher objects.
  class CachedFetcher

    # Create a cached fetcher (based on a RemoteSourceFetcher) for the
    # source at +source_uri+ (through the proxy +proxy+).
    def initialize(source_uri, proxy)
      require 'rubygems/incremental_fetcher'
      @source_uri = source_uri
      rsf = RemoteSourceFetcher.new(source_uri, proxy)
      @fetcher = IncrementalFetcher.new(source_uri, rsf, manager)
    end

    # The uncompressed +size+ of the source's directory (e.g. source
    # info).
    def size
      @fetcher.size
    end

    # Fetch the data from the source at the given path.
    def fetch_path(path="")
      @fetcher.fetch_path(path)
    end

    # Get the source index from the gem source.  The source index is a
    # directory of the gems available on the source, formatted as a
    # Gem::Cache object.  The cache object allows easy searching for
    # gems by name and version requirement.
    #
    # Notice that the gem specs in the cache are adequate for searches
    # and queries, but may have some information elided (hence
    # "abbreviated").
    def source_index
      cache = manager.cache_data[@source_uri]
      if cache && cache.size == @fetcher.size
	cache.source_index
      else
	result = @fetcher.source_index
	manager.cache_data[@source_uri] = SourceInfoCacheEntry.new(result, @fetcher.size)
	manager.update
	result
      end
    end

    # Flush the cache to a local file, if needed.
    def flush
      manager.flush
    end

    private

    # The cache manager for this cached source.
    def manager
      self.class.manager
    end

    # The cache is shared between all caching fetchers, so the cache
    # is put in the class object.
    class << self

      # The Cache manager for all instances of this class.
      def manager
	@manager ||= SourceInfoCache.new
      end

      # Sent by the client when it is done with all the sources,
      # allowing any cleanup activity to take place.
      def finish
	manager.flush
      end
    end
    
  end

  class RemoteInstaller
    include UserInteraction

    # <tt>options[:http_proxy]</tt>::
    # * [String]: explicit specification of proxy; overrides any
    #   environment variable setting
    # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER, HTTP_PROXY_PASS)
    # * <tt>:no_proxy</tt>: ignore environment variables and _don't_
    #   use a proxy
    #
    def initialize(options={})
      require 'uri'

      # Ensure http_proxy env vars are used if no proxy explicitly supplied.
      @options = options
      @fetcher_class = CachedFetcher
    end

    # This method will install package_name onto the local system.  
    #
    # gem_name::
    #   [String] Name of the Gem to install
    #
    # version_requirement::
    #   [default = "> 0.0.0"] Gem version requirement to install
    #
    # Returns::
    #   an array of Gem::Specification objects, one for each gem installed. 
    #
    def install(gem_name, version_requirement = "> 0.0.0", force=false, install_dir=Gem.dir, install_stub=true)
      unless version_requirement.respond_to?(:satisfied_by?)
        version_requirement = Version::Requirement.new(version_requirement)
      end
      installed_gems = []
      caches = source_index_hash
      spec, source = find_gem_to_install(gem_name, version_requirement, caches)
      dependencies = find_dependencies_not_installed(spec.dependencies)
      installed_gems << install_dependencies(dependencies, force, install_dir)
      cache_dir = File.join(install_dir, "cache")
      destination_file = File.join(cache_dir, spec.full_name + ".gem")
      download_gem(destination_file, source, spec)
      installer = new_installer(destination_file)
      installed_gems.unshift installer.install(force, install_dir, install_stub)
      installed_gems.flatten
    end

    # Search Gem repository for a gem by specifying all or part of
    # the Gem's name   
    def search(pattern_to_match)
      results = []
      caches = source_index_hash
      caches.each do |cache|
        results << cache[1].search(pattern_to_match)
      end
      results
    end

    # Return a list of the sources that we can download gems from
    def sources
      unless @sources
	require 'sources'
	@sources = Gem.sources
      end
      @sources
    end
    
    # Return a hash mapping the available source names to the source
    # index of that source.
    def source_index_hash
      result = {}
      sources.each do |source|
	result[source] = fetch_source(source)
      end
      @fetcher_class.finish
      result
    end
    
    # Return the source info for the given source.  The 
    def fetch_source(source)
      rsf = @fetcher_class.new(source, @options[:http_proxy])
      rsf.source_index
    end

    # Find a gem to be installed by interacting with the user.
    def find_gem_to_install(gem_name, version_requirement, caches)
      specs_n_sources = []

      caches.each do |source, cache|
        cache.each do |name, spec|
          if /^#{gem_name}$/i === spec.name &&
             version_requirement.satisfied_by?(spec.version) then
            specs_n_sources << [spec, source]
          end
        end
      end

      if specs_n_sources.empty? then
        raise GemNotFoundException.new("Could not find #{gem_name} (#{version_requirement}) in the repository")
      end

      specs_n_sources = specs_n_sources.sort_by { |gs,| gs.version }.reverse

      non_binary_gems = specs_n_sources.reject { |item|
        item[0].platform.nil? || item[0].platform==Platform::RUBY
      }

      # only non-binary gems...return latest
      return specs_n_sources.first if non_binary_gems.empty?

      list = specs_n_sources.collect { |item|
	"#{item[0].name} #{item[0].version} (#{item[0].platform.to_s})"
      }

      list << "Cancel installation"

      string, index = choose_from_list(
	"Select which gem to install for your platform (#{RUBY_PLATFORM})",
	list)

      if index == (list.size - 1) then
        raise RemoteInstallationCancelled, "Installation of #{gem_name} cancelled."
      end

      specs_n_sources[index]
    end

    def find_dependencies_not_installed(dependencies)
      to_install = []
      dependencies.each do |dependency|
	srcindex = Gem::SourceIndex.from_installed_gems
	matches = srcindex.find_name(dependency.name, dependency.requirement_list)
	to_install.push dependency if matches.empty?
      end
      to_install
    end

    # Install all the given dependencies.  Returns an array of
    # Gem::Specification objects, one for each dependency installed.
    # 
    # TODO: For now, we recursively install, but this is not the right
    # way to do things (e.g.  if a package fails to download, we
    # shouldn't install anything).
    def install_dependencies(dependencies, force, install_dir)
      return if @options[:ignore_dependencies]
      installed_gems = []
      dependencies.each do |dependency|
        if @options[:include_dependencies] ||
	    ask_yes_no("Install required dependency #{dependency.name}?", true)
          remote_installer =  RemoteInstaller.new(@options)
          installed_gems << remote_installer.install(
	    dependency.name,
	    dependency.version_requirements,
	    force,
	    install_dir)
        else
          raise DependencyError.new("Required dependency #{dependency.name} not installed")
        end
      end
      installed_gems
    end

    def download_gem(destination_file, source, spec)
      rsf = @fetcher_class.new(source, @proxy_uri)
      path = "/gems/#{spec.full_name}.gem"
      response = rsf.fetch_path(path)
      write_gem_to_file(response, destination_file)
    end

    def write_gem_to_file(body, destination_file)
      FileUtils.mkdir_p(File.dirname(destination_file)) unless File.exist?(destination_file)
      File.open(destination_file, 'wb') do |out|
        out.write(body)
      end
    end
    
    def new_installer(gem)
      return Installer.new(gem, @options)
    end
  end

end