ruby/lib/rubygems/source_info_cache.rb

396 строки
9.0 KiB
Ruby

require 'fileutils'
require 'rubygems'
require 'rubygems/source_info_cache_entry'
require 'rubygems/user_interaction'
##
# SourceInfoCache stores a copy of the gem index for each gem source.
#
# There are two possible cache locations, the system cache and the user cache:
# * The system cache is preferred if it is writable or can be created.
# * The user cache is used otherwise
#
# Once a cache is selected, it will be used for all operations.
# SourceInfoCache will not switch between cache files dynamically.
#
# Cache data is a Hash mapping a source URI to a SourceInfoCacheEntry.
#
#--
# To keep things straight, this is how the cache objects all fit together:
#
# Gem::SourceInfoCache
# @cache_data = {
# source_uri => Gem::SourceInfoCacheEntry
# @size = source index size
# @source_index = Gem::SourceIndex
# ...
# }
class Gem::SourceInfoCache
include Gem::UserInteraction
##
# The singleton Gem::SourceInfoCache. If +all+ is true, a full refresh will
# be performed if the singleton instance is being initialized.
def self.cache(all = false)
return @cache if @cache
@cache = new
@cache.refresh all if Gem.configuration.update_sources
@cache
end
def self.cache_data
cache.cache_data
end
##
# The name of the system cache file.
def self.latest_system_cache_file
File.join File.dirname(system_cache_file),
"latest_#{File.basename system_cache_file}"
end
##
# The name of the latest user cache file.
def self.latest_user_cache_file
File.join File.dirname(user_cache_file),
"latest_#{File.basename user_cache_file}"
end
##
# Reset all singletons, discarding any changes.
def self.reset
@cache = nil
@system_cache_file = nil
@user_cache_file = nil
end
##
# Search all source indexes. See Gem::SourceInfoCache#search.
def self.search(*args)
cache.search(*args)
end
##
# Search all source indexes returning the source_uri. See
# Gem::SourceInfoCache#search_with_source.
def self.search_with_source(*args)
cache.search_with_source(*args)
end
##
# The name of the system cache file. (class method)
def self.system_cache_file
@system_cache_file ||= Gem.default_system_source_cache_dir
end
##
# The name of the user cache file.
def self.user_cache_file
@user_cache_file ||=
ENV['GEMCACHE'] || Gem.default_user_source_cache_dir
end
def initialize # :nodoc:
@cache_data = nil
@cache_file = nil
@dirty = false
@only_latest = true
end
##
# The most recent cache data.
def cache_data
return @cache_data if @cache_data
cache_file # HACK writable check
@only_latest = true
@cache_data = read_cache_data latest_cache_file
@cache_data
end
##
# The name of the cache file.
def cache_file
return @cache_file if @cache_file
@cache_file = (try_file(system_cache_file) or
try_file(user_cache_file) or
raise "unable to locate a writable cache file")
end
##
# Write the cache to a local file (if it is dirty).
def flush
write_cache if @dirty
@dirty = false
end
def latest_cache_data
latest_cache_data = {}
cache_data.each do |repo, sice|
latest = sice.source_index.latest_specs
new_si = Gem::SourceIndex.new
new_si.add_specs(*latest)
latest_sice = Gem::SourceInfoCacheEntry.new new_si, sice.size
latest_cache_data[repo] = latest_sice
end
latest_cache_data
end
##
# The name of the latest cache file.
def latest_cache_file
File.join File.dirname(cache_file), "latest_#{File.basename cache_file}"
end
##
# The name of the latest system cache file.
def latest_system_cache_file
self.class.latest_system_cache_file
end
##
# The name of the latest user cache file.
def latest_user_cache_file
self.class.latest_user_cache_file
end
##
# Merges the complete cache file into this Gem::SourceInfoCache.
def read_all_cache_data
if @only_latest then
@only_latest = false
all_data = read_cache_data cache_file
cache_data.update all_data do |source_uri, latest_sice, all_sice|
all_sice.source_index.gems.update latest_sice.source_index.gems
Gem::SourceInfoCacheEntry.new all_sice.source_index, latest_sice.size
end
begin
refresh true
rescue Gem::RemoteFetcher::FetchError
end
end
end
##
# Reads cached data from +file+.
def read_cache_data(file)
# Marshal loads 30-40% faster from a String, and 2MB on 20061116 is small
data = open file, 'rb' do |fp| fp.read end
cache_data = Marshal.load data
cache_data.each do |url, sice|
next unless sice.is_a?(Hash)
update
cache = sice['cache']
size = sice['size']
if cache.is_a?(Gem::SourceIndex) and size.is_a?(Numeric) then
new_sice = Gem::SourceInfoCacheEntry.new cache, size
cache_data[url] = new_sice
else # irreperable, force refetch.
reset_cache_for url, cache_data
end
end
cache_data
rescue Errno::ENOENT
{}
rescue => e
if Gem.configuration.really_verbose then
say "Exception during cache_data handling: #{e.class} - #{e}"
say "Cache file was: #{file}"
say "\t#{e.backtrace.join "\n\t"}"
end
{}
end
##
# Refreshes each source in the cache from its repository. If +all+ is
# false, only latest gems are updated.
def refresh(all)
Gem.sources.each do |source_uri|
cache_entry = cache_data[source_uri]
if cache_entry.nil? then
cache_entry = Gem::SourceInfoCacheEntry.new nil, 0
cache_data[source_uri] = cache_entry
end
update if cache_entry.refresh source_uri, all
end
flush
end
def reset_cache_for(url, cache_data)
say "Reseting cache for #{url}" if Gem.configuration.really_verbose
sice = Gem::SourceInfoCacheEntry.new Gem::SourceIndex.new, 0
sice.refresh url, false # HACK may be unnecessary, see ::cache and #refresh
cache_data[url] = sice
cache_data
end
def reset_cache_data
@cache_data = nil
@only_latest = true
end
##
# Force cache file to be reset, useful for integration testing of rubygems
def reset_cache_file
@cache_file = nil
end
##
# Searches all source indexes. See Gem::SourceIndex#search for details on
# +pattern+ and +platform_only+. If +all+ is set to true, the full index
# will be loaded before searching.
def search(pattern, platform_only = false, all = false)
read_all_cache_data if all
cache_data.map do |source_uri, sic_entry|
next unless Gem.sources.include? source_uri
# TODO - Remove this gunk after 2008/11
unless pattern.kind_of? Gem::Dependency then
pattern = Gem::Dependency.new pattern, Gem::Requirement.default
end
sic_entry.source_index.search pattern, platform_only
end.flatten.compact
end
##
# Searches all source indexes for +pattern+. If +only_platform+ is true,
# only gems matching Gem.platforms will be selected. Returns an Array of
# pairs containing the Gem::Specification found and the source_uri it was
# found at.
def search_with_source(pattern, only_platform = false, all = false)
read_all_cache_data if all
results = []
cache_data.map do |source_uri, sic_entry|
next unless Gem.sources.include? source_uri
# TODO - Remove this gunk after 2008/11
unless pattern.kind_of?(Gem::Dependency)
pattern = Gem::Dependency.new(pattern, Gem::Requirement.default)
end
sic_entry.source_index.search(pattern, only_platform).each do |spec|
results << [spec, source_uri]
end
end
results
end
##
# Set the source info cache data directly. This is mainly used for unit
# testing when we don't want to read a file system to grab the cached source
# index information. The +hash+ should map a source URL into a
# SourceInfoCacheEntry.
def set_cache_data(hash)
@cache_data = hash
update
end
##
# The name of the system cache file.
def system_cache_file
self.class.system_cache_file
end
##
# Determine if +path+ is a candidate for a cache file. Returns +path+ if
# it is, nil if not.
def try_file(path)
return path if File.writable? path
return nil if File.exist? path
dir = File.dirname path
unless File.exist? dir then
begin
FileUtils.mkdir_p dir
rescue RuntimeError, SystemCallError
return nil
end
end
return path if File.writable? dir
nil
end
##
# Mark the cache as updated (i.e. dirty).
def update
@dirty = true
end
##
# The name of the user cache file.
def user_cache_file
self.class.user_cache_file
end
##
# Write data to the proper cache files.
def write_cache
if not File.exist?(cache_file) or not @only_latest then
open cache_file, 'wb' do |io|
io.write Marshal.dump(cache_data)
end
end
open latest_cache_file, 'wb' do |io|
io.write Marshal.dump(latest_cache_data)
end
end
reset
end