2007-11-10 10:48:56 +03:00
|
|
|
require 'fileutils'
|
|
|
|
require 'tmpdir'
|
2008-06-18 02:04:18 +04:00
|
|
|
require 'zlib'
|
2007-11-10 10:48:56 +03:00
|
|
|
|
|
|
|
require 'rubygems'
|
|
|
|
require 'rubygems/format'
|
|
|
|
|
|
|
|
begin
|
|
|
|
require 'builder/xchar'
|
|
|
|
rescue LoadError
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Top level class for building the gem repository index.
|
2008-04-01 02:40:06 +04:00
|
|
|
|
2007-11-10 10:48:56 +03:00
|
|
|
class Gem::Indexer
|
|
|
|
|
|
|
|
include Gem::UserInteraction
|
|
|
|
|
|
|
|
##
|
|
|
|
# Index install location
|
|
|
|
|
|
|
|
attr_reader :dest_directory
|
|
|
|
|
|
|
|
##
|
|
|
|
# Index build directory
|
|
|
|
|
|
|
|
attr_reader :directory
|
|
|
|
|
2008-04-01 02:40:06 +04:00
|
|
|
##
|
2007-11-10 10:48:56 +03:00
|
|
|
# Create an indexer that will index the gems in +directory+.
|
2008-04-01 02:40:06 +04:00
|
|
|
|
2007-11-10 10:48:56 +03:00
|
|
|
def initialize(directory)
|
|
|
|
unless ''.respond_to? :to_xs then
|
|
|
|
fail "Gem::Indexer requires that the XML Builder library be installed:" \
|
|
|
|
"\n\tgem install builder"
|
|
|
|
end
|
|
|
|
|
|
|
|
@dest_directory = directory
|
|
|
|
@directory = File.join Dir.tmpdir, "gem_generate_index_#{$$}"
|
|
|
|
|
|
|
|
marshal_name = "Marshal.#{Gem.marshal_version}"
|
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
@master_index = File.join @directory, 'yaml'
|
|
|
|
@marshal_index = File.join @directory, marshal_name
|
2008-04-01 02:40:06 +04:00
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
@quick_dir = File.join @directory, 'quick'
|
|
|
|
|
|
|
|
@quick_marshal_dir = File.join @quick_dir, marshal_name
|
|
|
|
|
|
|
|
@quick_index = File.join @quick_dir, 'index'
|
|
|
|
@latest_index = File.join @quick_dir, 'latest_index'
|
|
|
|
|
|
|
|
@specs_index = File.join @directory, "specs.#{Gem.marshal_version}"
|
|
|
|
@latest_specs_index = File.join @directory,
|
|
|
|
"latest_specs.#{Gem.marshal_version}"
|
|
|
|
|
|
|
|
files = [
|
|
|
|
@specs_index,
|
|
|
|
"#{@specs_index}.gz",
|
|
|
|
@latest_specs_index,
|
|
|
|
"#{@latest_specs_index}.gz",
|
|
|
|
@quick_dir,
|
|
|
|
@master_index,
|
|
|
|
"#{@master_index}.Z",
|
|
|
|
@marshal_index,
|
|
|
|
"#{@marshal_index}.Z",
|
|
|
|
]
|
|
|
|
|
|
|
|
@files = files.map do |path|
|
|
|
|
path.sub @directory, ''
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Abbreviate the spec for downloading. Abbreviated specs are only used for
|
|
|
|
# searching, downloading and related activities and do not need deployment
|
|
|
|
# specific information (e.g. list of files). So we abbreviate the spec,
|
|
|
|
# making it much smaller for quicker downloads.
|
|
|
|
|
|
|
|
def abbreviate(spec)
|
|
|
|
spec.files = []
|
|
|
|
spec.test_files = []
|
|
|
|
spec.rdoc_options = []
|
|
|
|
spec.extra_rdoc_files = []
|
|
|
|
spec.cert_chain = []
|
|
|
|
spec
|
2007-11-10 10:48:56 +03:00
|
|
|
end
|
|
|
|
|
2008-04-01 02:40:06 +04:00
|
|
|
##
|
2008-06-18 02:04:18 +04:00
|
|
|
# Build various indicies
|
|
|
|
|
|
|
|
def build_indicies(index)
|
|
|
|
progress = ui.progress_reporter index.size,
|
|
|
|
"Generating quick index gemspecs for #{index.size} gems",
|
|
|
|
"Complete"
|
|
|
|
|
|
|
|
index.each do |original_name, spec|
|
|
|
|
spec_file_name = "#{original_name}.gemspec.rz"
|
|
|
|
yaml_name = File.join @quick_dir, spec_file_name
|
|
|
|
marshal_name = File.join @quick_marshal_dir, spec_file_name
|
|
|
|
|
|
|
|
yaml_zipped = Gem.deflate spec.to_yaml
|
|
|
|
open yaml_name, 'wb' do |io| io.write yaml_zipped end
|
|
|
|
|
|
|
|
marshal_zipped = Gem.deflate Marshal.dump(spec)
|
|
|
|
open marshal_name, 'wb' do |io| io.write marshal_zipped end
|
|
|
|
|
|
|
|
progress.updated original_name
|
|
|
|
end
|
|
|
|
|
|
|
|
progress.done
|
|
|
|
|
|
|
|
say "Generating specs index"
|
|
|
|
|
|
|
|
open @specs_index, 'wb' do |io|
|
|
|
|
specs = index.sort.map do |_, spec|
|
|
|
|
platform = spec.original_platform
|
2008-06-26 06:06:00 +04:00
|
|
|
platform = Gem::Platform::RUBY if platform.nil? or platform.empty?
|
2008-06-18 02:04:18 +04:00
|
|
|
[spec.name, spec.version, platform]
|
|
|
|
end
|
|
|
|
|
|
|
|
specs = compact_specs specs
|
|
|
|
|
|
|
|
Marshal.dump specs, io
|
|
|
|
end
|
|
|
|
|
|
|
|
say "Generating latest specs index"
|
|
|
|
|
|
|
|
open @latest_specs_index, 'wb' do |io|
|
|
|
|
specs = index.latest_specs.sort.map do |spec|
|
2008-06-26 06:06:00 +04:00
|
|
|
platform = spec.original_platform
|
|
|
|
platform = Gem::Platform::RUBY if platform.nil? or platform.empty?
|
|
|
|
[spec.name, spec.version, platform]
|
2008-06-18 02:04:18 +04:00
|
|
|
end
|
|
|
|
|
|
|
|
specs = compact_specs specs
|
|
|
|
|
|
|
|
Marshal.dump specs, io
|
|
|
|
end
|
|
|
|
|
|
|
|
say "Generating quick index"
|
|
|
|
|
|
|
|
quick_index = File.join @quick_dir, 'index'
|
|
|
|
open quick_index, 'wb' do |io|
|
|
|
|
io.puts index.sort.map { |_, spec| spec.original_name }
|
|
|
|
end
|
|
|
|
|
|
|
|
say "Generating latest index"
|
|
|
|
|
|
|
|
latest_index = File.join @quick_dir, 'latest_index'
|
|
|
|
open latest_index, 'wb' do |io|
|
|
|
|
io.puts index.latest_specs.sort.map { |spec| spec.original_name }
|
|
|
|
end
|
|
|
|
|
|
|
|
say "Generating Marshal master index"
|
|
|
|
|
|
|
|
open @marshal_index, 'wb' do |io|
|
|
|
|
io.write index.dump
|
|
|
|
end
|
|
|
|
|
|
|
|
progress = ui.progress_reporter index.size,
|
|
|
|
"Generating YAML master index for #{index.size} gems (this may take a while)",
|
|
|
|
"Complete"
|
|
|
|
|
|
|
|
open @master_index, 'wb' do |io|
|
|
|
|
io.puts "--- !ruby/object:#{index.class}"
|
|
|
|
io.puts "gems:"
|
|
|
|
|
|
|
|
gems = index.sort_by { |name, gemspec| gemspec.sort_obj }
|
|
|
|
gems.each do |original_name, gemspec|
|
|
|
|
yaml = gemspec.to_yaml.gsub(/^/, ' ')
|
|
|
|
yaml = yaml.sub(/\A ---/, '') # there's a needed extra ' ' here
|
|
|
|
io.print " #{original_name}:"
|
|
|
|
io.puts yaml
|
|
|
|
|
|
|
|
progress.updated original_name
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
progress.done
|
|
|
|
|
|
|
|
say "Compressing indicies"
|
|
|
|
# use gzip for future files.
|
|
|
|
|
|
|
|
compress quick_index, 'rz'
|
|
|
|
paranoid quick_index, 'rz'
|
|
|
|
|
|
|
|
compress latest_index, 'rz'
|
|
|
|
paranoid latest_index, 'rz'
|
|
|
|
|
|
|
|
compress @marshal_index, 'Z'
|
|
|
|
paranoid @marshal_index, 'Z'
|
|
|
|
|
|
|
|
compress @master_index, 'Z'
|
|
|
|
paranoid @master_index, 'Z'
|
|
|
|
|
|
|
|
gzip @specs_index
|
|
|
|
gzip @latest_specs_index
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Collect specifications from .gem files from the gem directory.
|
|
|
|
|
|
|
|
def collect_specs
|
|
|
|
index = Gem::SourceIndex.new
|
|
|
|
|
|
|
|
progress = ui.progress_reporter gem_file_list.size,
|
|
|
|
"Loading #{gem_file_list.size} gems from #{@dest_directory}",
|
|
|
|
"Loaded all gems"
|
|
|
|
|
|
|
|
gem_file_list.each do |gemfile|
|
|
|
|
if File.size(gemfile.to_s) == 0 then
|
|
|
|
alert_warning "Skipping zero-length gem: #{gemfile}"
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
begin
|
|
|
|
spec = Gem::Format.from_file_by_path(gemfile).spec
|
|
|
|
|
|
|
|
unless gemfile =~ /\/#{Regexp.escape spec.original_name}.*\.gem\z/i then
|
|
|
|
alert_warning "Skipping misnamed gem: #{gemfile} => #{spec.full_name} (#{spec.original_name})"
|
|
|
|
next
|
2007-11-10 10:48:56 +03:00
|
|
|
end
|
2008-06-18 02:04:18 +04:00
|
|
|
|
|
|
|
abbreviate spec
|
|
|
|
sanitize spec
|
|
|
|
|
|
|
|
index.gems[spec.original_name] = spec
|
|
|
|
|
|
|
|
progress.updated spec.original_name
|
|
|
|
|
|
|
|
rescue SignalException => e
|
|
|
|
alert_error "Received signal, exiting"
|
|
|
|
raise
|
|
|
|
rescue Exception => e
|
|
|
|
alert_error "Unable to process #{gemfile}\n#{e.message} (#{e.class})\n\t#{e.backtrace.join "\n\t"}"
|
2007-11-10 10:48:56 +03:00
|
|
|
end
|
|
|
|
end
|
2008-06-18 02:04:18 +04:00
|
|
|
|
|
|
|
progress.done
|
|
|
|
|
|
|
|
index
|
2007-11-10 10:48:56 +03:00
|
|
|
end
|
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
##
|
|
|
|
# Compacts Marshal output for the specs index data source by using identical
|
|
|
|
# objects as much as possible.
|
2007-11-10 10:48:56 +03:00
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
def compact_specs(specs)
|
|
|
|
names = {}
|
|
|
|
versions = {}
|
|
|
|
platforms = {}
|
2007-11-10 10:48:56 +03:00
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
specs.map do |(name, version, platform)|
|
|
|
|
names[name] = name unless names.include? name
|
|
|
|
versions[version] = version unless versions.include? version
|
|
|
|
platforms[platform] = platform unless platforms.include? platform
|
2007-11-10 10:48:56 +03:00
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
[names[name], versions[version], platforms[platform]]
|
|
|
|
end
|
|
|
|
end
|
2007-11-10 10:48:56 +03:00
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
##
|
|
|
|
# Compress +filename+ with +extension+.
|
|
|
|
|
|
|
|
def compress(filename, extension)
|
|
|
|
data = Gem.read_binary filename
|
|
|
|
|
|
|
|
zipped = Gem.deflate data
|
|
|
|
|
|
|
|
open "#{filename}.#{extension}", 'wb' do |io|
|
|
|
|
io.write zipped
|
2007-11-10 10:48:56 +03:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
##
|
|
|
|
# List of gem file names to index.
|
|
|
|
|
|
|
|
def gem_file_list
|
|
|
|
Dir.glob(File.join(@dest_directory, "gems", "*.gem"))
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Builds and installs indexicies.
|
|
|
|
|
2007-11-10 10:48:56 +03:00
|
|
|
def generate_index
|
2008-06-26 06:06:00 +04:00
|
|
|
make_temp_directories
|
2008-06-18 02:04:18 +04:00
|
|
|
index = collect_specs
|
|
|
|
build_indicies index
|
|
|
|
install_indicies
|
2007-11-10 10:48:56 +03:00
|
|
|
rescue SignalException
|
|
|
|
ensure
|
|
|
|
FileUtils.rm_rf @directory
|
|
|
|
end
|
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
##
|
|
|
|
# Zlib::GzipWriter wrapper that gzips +filename+ on disk.
|
|
|
|
|
|
|
|
def gzip(filename)
|
|
|
|
Zlib::GzipWriter.open "#{filename}.gz" do |io|
|
|
|
|
io.write Gem.read_binary(filename)
|
|
|
|
end
|
2007-11-10 10:48:56 +03:00
|
|
|
end
|
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
##
|
|
|
|
# Install generated indicies into the destination directory.
|
|
|
|
|
|
|
|
def install_indicies
|
|
|
|
verbose = Gem.configuration.really_verbose
|
|
|
|
|
|
|
|
say "Moving index into production dir #{@dest_directory}" if verbose
|
|
|
|
|
|
|
|
@files.each do |file|
|
|
|
|
src_name = File.join @directory, file
|
|
|
|
dst_name = File.join @dest_directory, file
|
|
|
|
|
|
|
|
FileUtils.rm_rf dst_name, :verbose => verbose
|
2008-06-26 06:06:00 +04:00
|
|
|
FileUtils.mv src_name, @dest_directory, :verbose => verbose,
|
|
|
|
:force => true
|
2008-06-18 02:04:18 +04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2008-06-26 06:06:00 +04:00
|
|
|
##
|
|
|
|
# Make directories for index generation
|
|
|
|
|
|
|
|
def make_temp_directories
|
|
|
|
FileUtils.rm_rf @directory
|
|
|
|
FileUtils.mkdir_p @directory, :mode => 0700
|
|
|
|
FileUtils.mkdir_p @quick_marshal_dir
|
|
|
|
end
|
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
##
|
|
|
|
# Ensure +path+ and path with +extension+ are identical.
|
|
|
|
|
|
|
|
def paranoid(path, extension)
|
|
|
|
data = Gem.read_binary path
|
|
|
|
compressed_data = Gem.read_binary "#{path}.#{extension}"
|
|
|
|
|
|
|
|
unless data == Gem.inflate(compressed_data) then
|
|
|
|
raise "Compressed file #{compressed_path} does not match uncompressed file #{path}"
|
|
|
|
end
|
2007-11-10 10:48:56 +03:00
|
|
|
end
|
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
##
|
2007-11-10 10:48:56 +03:00
|
|
|
# Sanitize the descriptive fields in the spec. Sometimes non-ASCII
|
|
|
|
# characters will garble the site index. Non-ASCII characters will
|
|
|
|
# be replaced by their XML entity equivalent.
|
2008-06-18 02:04:18 +04:00
|
|
|
|
2007-11-10 10:48:56 +03:00
|
|
|
def sanitize(spec)
|
|
|
|
spec.summary = sanitize_string(spec.summary)
|
|
|
|
spec.description = sanitize_string(spec.description)
|
|
|
|
spec.post_install_message = sanitize_string(spec.post_install_message)
|
|
|
|
spec.authors = spec.authors.collect { |a| sanitize_string(a) }
|
2008-06-26 06:06:00 +04:00
|
|
|
|
2007-11-10 10:48:56 +03:00
|
|
|
spec
|
|
|
|
end
|
|
|
|
|
2008-06-18 02:04:18 +04:00
|
|
|
##
|
2007-11-10 10:48:56 +03:00
|
|
|
# Sanitize a single string.
|
2008-06-18 02:04:18 +04:00
|
|
|
|
2007-11-10 10:48:56 +03:00
|
|
|
def sanitize_string(string)
|
|
|
|
# HACK the #to_s is in here because RSpec has an Array of Arrays of
|
|
|
|
# Strings for authors. Need a way to disallow bad values on gempsec
|
|
|
|
# generation. (Probably won't happen.)
|
|
|
|
string ? string.to_s.to_xs : string
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|