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
|
require 'zlib'
require 'fileutils'
require 'rubygems/remote_fetcher'
require 'rubygems/user_interaction'
##
# SpecFetcher handles metadata updates from remote gem repositories.
class Gem::SpecFetcher
include Gem::UserInteraction
##
# The SpecFetcher cache dir.
attr_reader :dir # :nodoc:
##
# Cache of latest specs
attr_reader :latest_specs # :nodoc:
##
# Cache of all released specs
attr_reader :specs # :nodoc:
##
# Cache of prerelease specs
attr_reader :prerelease_specs # :nodoc:
@fetcher = nil
def self.fetcher
@fetcher ||= new
end
def self.fetcher=(fetcher) # :nodoc:
@fetcher = fetcher
end
def initialize
@dir = File.join Gem.user_home, '.gem', 'specs'
@update_cache = File.stat(Gem.user_home).uid == Process.uid
@specs = {}
@latest_specs = {}
@prerelease_specs = {}
@fetcher = Gem::RemoteFetcher.fetcher
end
##
# Retuns the local directory to write +uri+ to.
def cache_dir(uri)
File.join @dir, "#{uri.host}%#{uri.port}", File.dirname(uri.path)
end
##
# Fetch specs matching +dependency+. If +all+ is true, all matching
# (released) versions are returned. If +matching_platform+ is
# false, all platforms are returned. If +prerelease+ is true,
# prerelease versions are included.
def fetch(dependency, all = false, matching_platform = true, prerelease = false)
specs_and_sources = find_matching dependency, all, matching_platform, prerelease
specs_and_sources.map do |spec_tuple, source_uri|
[fetch_spec(spec_tuple, URI.parse(source_uri)), source_uri]
end
rescue Gem::RemoteFetcher::FetchError => e
raise unless warn_legacy e do
require 'rubygems/source_info_cache'
return Gem::SourceInfoCache.search_with_source(dependency,
matching_platform, all)
end
end
def fetch_spec(spec, source_uri)
spec = spec - [nil, 'ruby', '']
spec_file_name = "#{spec.join '-'}.gemspec"
uri = source_uri + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}"
cache_dir = cache_dir uri
local_spec = File.join cache_dir, spec_file_name
if File.exist? local_spec then
spec = Gem.read_binary local_spec
else
uri.path << '.rz'
spec = @fetcher.fetch_path uri
spec = Gem.inflate spec
if @update_cache then
FileUtils.mkdir_p cache_dir
open local_spec, 'wb' do |io|
io.write spec
end
end
end
# TODO: Investigate setting Gem::Specification#loaded_from to a URI
Marshal.load spec
end
##
# Find spec names that match +dependency+. If +all+ is true, all
# matching released versions are returned. If +matching_platform+
# is false, gems for all platforms are returned.
def find_matching(dependency, all = false, matching_platform = true, prerelease = false)
found = {}
list(all, prerelease).each do |source_uri, specs|
found[source_uri] = specs.select do |spec_name, version, spec_platform|
dependency =~ Gem::Dependency.new(spec_name, version) and
(not matching_platform or Gem::Platform.match(spec_platform))
end
end
specs_and_sources = []
found.each do |source_uri, specs|
uri_str = source_uri.to_s
specs_and_sources.push(*specs.map { |spec| [spec, uri_str] })
end
specs_and_sources
end
##
# Returns Array of gem repositories that were generated with RubyGems less
# than 1.2.
def legacy_repos
Gem.sources.reject do |source_uri|
source_uri = URI.parse source_uri
spec_path = source_uri + "specs.#{Gem.marshal_version}.gz"
begin
@fetcher.fetch_size spec_path
rescue Gem::RemoteFetcher::FetchError
begin
@fetcher.fetch_size(source_uri + 'yaml') # re-raise if non-repo
rescue Gem::RemoteFetcher::FetchError
alert_error "#{source_uri} does not appear to be a repository"
raise
end
false
end
end
end
##
# Returns a list of gems available for each source in Gem::sources. If
# +all+ is true, all released versions are returned instead of only latest
# versions. If +prerelease+ is true, include prerelease versions.
def list(all = false, prerelease = false)
# TODO: make type the only argument
type = if all
:all
elsif prerelease
:prerelease
else
:latest
end
list = {}
file = { :latest => 'latest_specs',
:prerelease => 'prerelease_specs',
:all => 'specs' }[type]
cache = { :latest => @latest_specs,
:prerelease => @prerelease_specs,
:all => @specs }[type]
Gem.sources.each do |source_uri|
source_uri = URI.parse source_uri
unless cache.include? source_uri
cache[source_uri] = load_specs source_uri, file
end
list[source_uri] = cache[source_uri]
end
if type == :all
list.values.map do |gems|
gems.reject! { |g| !g[1] || g[1].prerelease? }
end
end
list
end
##
# Loads specs in +file+, fetching from +source_uri+ if the on-disk cache is
# out of date.
def load_specs(source_uri, file)
file_name = "#{file}.#{Gem.marshal_version}"
spec_path = source_uri + "#{file_name}.gz"
cache_dir = cache_dir spec_path
local_file = File.join(cache_dir, file_name)
loaded = false
if File.exist? local_file then
spec_dump = @fetcher.fetch_path spec_path, File.mtime(local_file)
if spec_dump.nil? then
spec_dump = Gem.read_binary local_file
else
loaded = true
end
else
spec_dump = @fetcher.fetch_path spec_path
loaded = true
end
specs = begin
Marshal.load spec_dump
rescue ArgumentError
spec_dump = @fetcher.fetch_path spec_path
loaded = true
Marshal.load spec_dump
end
if loaded and @update_cache then
begin
FileUtils.mkdir_p cache_dir
open local_file, 'wb' do |io|
io << spec_dump
end
rescue
end
end
specs
end
##
# Warn about legacy repositories if +exception+ indicates only legacy
# repositories are available, and yield to the block. Returns false if the
# exception indicates some other FetchError.
def warn_legacy(exception)
uri = exception.uri.to_s
if uri =~ /specs\.#{Regexp.escape Gem.marshal_version}\.gz$/ then
alert_warning <<-EOF
RubyGems 1.2+ index not found for:
\t#{legacy_repos.join "\n\t"}
RubyGems will revert to legacy indexes degrading performance.
EOF
yield
return true
end
false
end
end
|