Top level class for building the gem repository index.
Build indexes for RubyGems 1.2.0 and newer when true
Index install location
Specs index install location
Latest specs index install location
Prerelease specs index install location
Index build directory
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 60
def initialize(directory, options = {})
require 'fileutils'
require 'tmpdir'
require 'zlib'
unless defined?(Builder::XChar)
raise "Gem::Indexer requires that the XML Builder library be installed:" +
"\n\tgem install builder"
end
options = { :build_modern => true }.merge options
@build_modern = options[:build_modern]
@dest_directory = directory
@directory = Dir.mktmpdir 'gem_generate_index'
marshal_name = "Marshal.#{Gem.marshal_version}"
@master_index = File.join @directory, 'yaml'
@marshal_index = File.join @directory, marshal_name
@quick_dir = File.join @directory, 'quick'
@quick_marshal_dir = File.join @quick_dir, marshal_name
@quick_marshal_dir_base = File.join "quick", marshal_name # FIX: UGH
@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}")
@prerelease_specs_index =
File.join(@directory, "prerelease_specs.#{Gem.marshal_version}")
@dest_specs_index =
File.join(@dest_directory, "specs.#{Gem.marshal_version}")
@dest_latest_specs_index =
File.join(@dest_directory, "latest_specs.#{Gem.marshal_version}")
@dest_prerelease_specs_index =
File.join(@dest_directory, "prerelease_specs.#{Gem.marshal_version}")
@files = []
end
Create an indexer that will index the gems in directory
.
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 107
def build_indices
specs = map_gems_to_specs gem_file_list
Gem::Specification._resort! specs
build_marshal_gemspecs specs
build_modern_indices specs if @build_modern
compress_indices
end
Build various indices
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 119
def build_marshal_gemspecs(specs)
count = specs.count
progress = ui.progress_reporter count,
"Generating Marshal quick index gemspecs for #{count} gems",
"Complete"
files = []
Gem.time 'Generated Marshal quick index gemspecs' do
specs.each do |spec|
next if spec.default_gem?
spec_file_name = "#{spec.original_name}.gemspec.rz"
marshal_name = File.join @quick_marshal_dir, spec_file_name
marshal_zipped = Gem.deflate Marshal.dump(spec)
File.open marshal_name, 'wb' do |io|
io.write marshal_zipped
end
files << marshal_name
progress.updated spec.original_name
end
progress.done
end
@files << @quick_marshal_dir
files
end
Builds Marshal
quick index gemspecs.
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 155
def build_modern_index(index, file, name)
say "Generating #{name} index"
Gem.time "Generated #{name} index" do
open(file, 'wb') do |io|
specs = index.map do |*spec|
# We have to splat here because latest_specs is an array, while the
# others are hashes.
spec = spec.flatten.last
platform = spec.original_platform
# win32-api-1.0.4-x86-mswin32-60
unless String === platform
alert_warning "Skipping invalid platform in gem: #{spec.full_name}"
next
end
platform = Gem::Platform::RUBY if platform.nil? or platform.empty?
[spec.name, spec.version, platform]
end
specs = compact_specs(specs)
Marshal.dump(specs, io)
end
end
end
Build a single index for RubyGems 1.2 and newer
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 185
def build_modern_indices(specs)
prerelease, released = specs.partition do |s|
s.version.prerelease?
end
latest_specs =
Gem::Specification._latest_specs specs
build_modern_index(released.sort, @specs_index, 'specs')
build_modern_index(latest_specs.sort, @latest_specs_index, 'latest specs')
build_modern_index(prerelease.sort, @prerelease_specs_index,
'prerelease specs')
@files += [@specs_index,
"#{@specs_index}.gz",
@latest_specs_index,
"#{@latest_specs_index}.gz",
@prerelease_specs_index,
"#{@prerelease_specs_index}.gz"]
end
Builds indices for RubyGems 1.2 and newer. Handles full, latest, prerelease
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 253
def compact_specs(specs)
names = {}
versions = {}
platforms = {}
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
[names[name], versions[version], platforms[platform]]
end
end
Compacts Marshal
output for the specs index data source by using identical objects as much as possible.
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 270
def compress(filename, extension)
data = Gem.read_binary filename
zipped = Gem.deflate data
File.open "#{filename}.#{extension}", 'wb' do |io|
io.write zipped
end
end
Compress filename
with extension
.
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 237
def compress_indices
say "Compressing indices"
Gem.time 'Compressed indices' do
if @build_modern
gzip @specs_index
gzip @latest_specs_index
gzip @prerelease_specs_index
end
end
end
Compresses indices on disk
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 283
def gem_file_list
Gem::Util.glob_files_in_dir("*.gem", File.join(@dest_directory, "gems"))
end
List of gem file names to index.
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 290
def generate_index
make_temp_directories
build_indices
install_indices
rescue SignalException
ensure
FileUtils.rm_rf @directory
end
Builds and installs indices.
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 302
def gzip(filename)
Zlib::GzipWriter.open "#{filename}.gz" do |io|
io.write Gem.read_binary(filename)
end
end
Zlib::GzipWriter
wrapper that gzips filename
on disk.
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 311
def install_indices
verbose = Gem.configuration.really_verbose
say "Moving index into production dir #{@dest_directory}" if verbose
files = @files
files.delete @quick_marshal_dir if files.include? @quick_dir
if files.include? @quick_marshal_dir and not files.include? @quick_dir
files.delete @quick_marshal_dir
dst_name = File.join(@dest_directory, @quick_marshal_dir_base)
FileUtils.mkdir_p File.dirname(dst_name), :verbose => verbose
FileUtils.rm_rf dst_name, :verbose => verbose
FileUtils.mv(@quick_marshal_dir, dst_name,
:verbose => verbose, :force => true)
end
files = files.map do |path|
path.sub(/^#{Regexp.escape @directory}\/?/, '') # HACK?
end
files.each do |file|
src_name = File.join @directory, file
dst_name = File.join @dest_directory, file
FileUtils.rm_rf dst_name, :verbose => verbose
FileUtils.mv(src_name, @dest_directory,
:verbose => verbose, :force => true)
end
end
Install generated indices into the destination directory.
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 347
def make_temp_directories
FileUtils.rm_rf @directory
FileUtils.mkdir_p @directory, :mode => 0700
FileUtils.mkdir_p @quick_marshal_dir
end
Make directories for index generation
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 205
def map_gems_to_specs(gems)
gems.map do |gemfile|
if File.size(gemfile) == 0
alert_warning "Skipping zero-length gem: #{gemfile}"
next
end
begin
spec = Gem::Package.new(gemfile).spec
spec.loaded_from = gemfile
spec.abbreviate
spec.sanitize
spec
rescue SignalException
alert_error "Received signal, exiting"
raise
rescue Exception => e
msg = ["Unable to process #{gemfile}",
"#{e.message} (#{e.class})",
"\t#{e.backtrace.join "\n\t"}"].join("\n")
alert_error msg
end
end.compact
end
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 356
def paranoid(path, extension)
data = Gem.read_binary path
compressed_data = Gem.read_binary "#{path}.#{extension}"
unless data == Gem::Util.inflate(compressed_data)
raise "Compressed file #{compressed_path} does not match uncompressed file #{path}"
end
end
Ensure path
and path with extension
are identical.
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 368
def update_index
make_temp_directories
specs_mtime = File.stat(@dest_specs_index).mtime
newest_mtime = Time.at 0
updated_gems = gem_file_list.select do |gem|
gem_mtime = File.stat(gem).mtime
newest_mtime = gem_mtime if gem_mtime > newest_mtime
gem_mtime >= specs_mtime
end
if updated_gems.empty?
say 'No new gems'
terminate_interaction 0
end
specs = map_gems_to_specs updated_gems
prerelease, released = specs.partition { |s| s.version.prerelease? }
files = build_marshal_gemspecs specs
Gem.time 'Updated indexes' do
update_specs_index released, @dest_specs_index, @specs_index
update_specs_index released, @dest_latest_specs_index, @latest_specs_index
update_specs_index(prerelease,
@dest_prerelease_specs_index,
@prerelease_specs_index)
end
compress_indices
verbose = Gem.configuration.really_verbose
say "Updating production dir #{@dest_directory}" if verbose
files << @specs_index
files << "#{@specs_index}.gz"
files << @latest_specs_index
files << "#{@latest_specs_index}.gz"
files << @prerelease_specs_index
files << "#{@prerelease_specs_index}.gz"
files = files.map do |path|
path.sub(/^#{Regexp.escape @directory}\/?/, '') # HACK?
end
files.each do |file|
src_name = File.join @directory, file
dst_name = File.join @dest_directory, file # REFACTOR: duped above
FileUtils.mv src_name, dst_name, :verbose => verbose,
:force => true
File.utime newest_mtime, newest_mtime, dst_name
end
end
Perform an in-place update of the repository from newly added gems.
# File tmp/rubies/ruby-2.7.6/lib/rubygems/indexer.rb, line 430
def update_specs_index(index, source, dest)
specs_index = Marshal.load Gem.read_binary(source)
index.each do |spec|
platform = spec.original_platform
platform = Gem::Platform::RUBY if platform.nil? or platform.empty?
specs_index << [spec.name, spec.version, platform]
end
specs_index = compact_specs specs_index.uniq.sort
File.open dest, 'wb' do |io|
Marshal.dump specs_index, io
end
end
Combines specs in index
and source
then writes out a new copy to dest
. For a latest index, does not ensure the new file is minimal.