Imported File#ftype spec from rubyspecs.
[rbx.git] / lib / rubygems / spec_fetcher.rb
blob29db889af5a505afadcf31170ae8ef91a8232018
1 require 'zlib'
3 require 'rubygems'
4 require 'rubygems/remote_fetcher'
5 require 'rubygems/user_interaction'
7 ##
8 # SpecFetcher handles metadata updates from remote gem repositories.
10 class Gem::SpecFetcher
12   include Gem::UserInteraction
14   ##
15   # The SpecFetcher cache dir.
17   attr_reader :dir # :nodoc:
19   ##
20   # Cache of latest specs
22   attr_reader :latest_specs # :nodoc:
24   ##
25   # Cache of all spces
27   attr_reader :specs # :nodoc:
29   @fetcher = nil
31   def self.fetcher
32     @fetcher ||= new
33   end
35   def self.fetcher=(fetcher) # :nodoc:
36     @fetcher = fetcher
37   end
39   def initialize
40     @dir = File.join Gem.user_home, '.gem', 'specs'
41     @update_cache = File.stat(Gem.user_home).uid == Process.uid
43     @specs = {}
44     @latest_specs = {}
46     @fetcher = Gem::RemoteFetcher.fetcher
47   end
49   ##
50   # Retuns the local directory to write +uri+ to.
52   def cache_dir(uri)
53     File.join @dir, "#{uri.host}%#{uri.port}", File.dirname(uri.path)
54   end
56   ##
57   # Fetch specs matching +dependency+.  If +all+ is true, all matching
58   # versions are returned.  If +matching_platform+ is false, all platforms are
59   # returned.
61   def fetch(dependency, all = false, matching_platform = true)
62     specs_and_sources = find_matching dependency, all, matching_platform
64     specs_and_sources.map do |spec_tuple, source_uri|
65       [fetch_spec(spec_tuple, URI.parse(source_uri)), source_uri]
66     end
68   rescue Gem::RemoteFetcher::FetchError => e
69     raise unless warn_legacy e do
70       require 'rubygems/source_info_cache'
72       return Gem::SourceInfoCache.search_with_source(dependency,
73                                                      matching_platform, all)
74     end
75   end
77   def fetch_spec(spec, source_uri)
78     spec = spec - [nil, 'ruby', '']
79     spec_file_name = "#{spec.join '-'}.gemspec"
81     uri = source_uri + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}"
83     cache_dir = cache_dir uri
85     local_spec = File.join cache_dir, spec_file_name
87     if File.exist? local_spec then
88       spec = Gem.read_binary local_spec
89     else
90       uri.path << '.rz'
92       spec = @fetcher.fetch_path uri
93       spec = Gem.inflate spec
95       if @update_cache then
96         FileUtils.mkdir_p cache_dir
98         open local_spec, 'wb' do |io|
99           io.write spec
100         end
101       end
102     end
104     # TODO: Investigate setting Gem::Specification#loaded_from to a URI
105     Marshal.load spec
106   end
108   ##
109   # Find spec names that match +dependency+.  If +all+ is true, all matching
110   # versions are returned.  If +matching_platform+ is false, gems for all
111   # platforms are returned.
113   def find_matching(dependency, all = false, matching_platform = true)
114     found = {}
116     list(all).each do |source_uri, specs|
117       found[source_uri] = specs.select do |spec_name, version, spec_platform|
118         dependency =~ Gem::Dependency.new(spec_name, version) and
119           (not matching_platform or Gem::Platform.match(spec_platform))
120       end
121     end
123     specs_and_sources = []
125     found.each do |source_uri, specs|
126       uri_str = source_uri.to_s
127       specs_and_sources.push(*specs.map { |spec| [spec, uri_str] })
128     end
130     specs_and_sources
131   end
133   ##
134   # Returns Array of gem repositories that were generated with RubyGems less
135   # than 1.2.
137   def legacy_repos
138     Gem.sources.reject do |source_uri|
139       source_uri = URI.parse source_uri
140       spec_path = source_uri + "specs.#{Gem.marshal_version}.gz"
142       begin
143         @fetcher.fetch_size spec_path
144       rescue Gem::RemoteFetcher::FetchError
145         begin
146           @fetcher.fetch_size(source_uri + 'yaml') # re-raise if non-repo
147         rescue Gem::RemoteFetcher::FetchError
148           alert_error "#{source_uri} does not appear to be a repository"
149           raise
150         end
151         false
152       end
153     end
154   end
156   ##
157   # Returns a list of gems available for each source in Gem::sources.  If
158   # +all+ is true, all versions are returned instead of only latest versions.
160   def list(all = false)
161     list = {}
163     file = all ? 'specs' : 'latest_specs'
165     Gem.sources.each do |source_uri|
166       source_uri = URI.parse source_uri
168       if all and @specs.include? source_uri then
169         list[source_uri] = @specs[source_uri]
170       elsif @latest_specs.include? source_uri then
171         list[source_uri] = @latest_specs[source_uri]
172       else
173         specs = load_specs source_uri, file
175         cache = all ? @specs : @latest_specs
177         cache[source_uri] = specs
178         list[source_uri] = specs
179       end
180     end
182     list
183   end
185   def load_specs(source_uri, file)
186     file_name = "#{file}.#{Gem.marshal_version}.gz"
188     spec_path = source_uri + file_name
190     cache_dir = cache_dir spec_path
192     local_file = File.join(cache_dir, file_name).chomp '.gz'
194     if File.exist? local_file then
195       local_size = File.stat(local_file).size
197       remote_file = spec_path.dup
198       remote_file.path = remote_file.path.chomp '.gz'
199       remote_size = @fetcher.fetch_size remote_file
201       spec_dump = Gem.read_binary local_file if remote_size == local_size
202     end
204     unless spec_dump then
205       loaded = true
207       spec_dump_gz = @fetcher.fetch_path spec_path
208       spec_dump = Gem.gunzip spec_dump_gz
209     end
211     specs = Marshal.load spec_dump
213     if loaded and @update_cache then
214       begin
215         FileUtils.mkdir_p cache_dir
217         open local_file, 'wb' do |io|
218           Marshal.dump specs, io
219         end
220       rescue
221       end
222     end
224     specs
225   end
227   ##
228   # Warn about legacy repositories if +exception+ indicates only legacy
229   # repositories are available, and yield to the block.  Returns false if the
230   # exception indicates some other FetchError.
232   def warn_legacy(exception)
233     uri = exception.uri.to_s
234     if uri =~ /specs\.#{Regexp.escape Gem.marshal_version}\.gz$/ then
235       alert_warning <<-EOF
236 RubyGems 1.2+ index not found for:
237 \t#{legacy_repos.join "\n\t"}
239 RubyGems will revert to legacy indexes degrading performance.
240       EOF
242       yield
244       return true
245     end
247     false
248   end