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
|