6 require 'rubygems/format'
9 require 'builder/xchar'
14 # Top level class for building the gem repository index.
18 include Gem::UserInteraction
21 # Index install location
23 attr_reader :dest_directory
26 # Index build directory
28 attr_reader :directory
31 # Create an indexer that will index the gems in +directory+.
33 def initialize(directory)
34 unless ''.respond_to? :to_xs then
35 fail "Gem::Indexer requires that the XML Builder library be installed:" \
36 "\n\tgem install builder"
39 @dest_directory = directory
40 @directory = File.join Dir.tmpdir, "gem_generate_index_#{$$}"
42 marshal_name = "Marshal.#{Gem.marshal_version}"
44 @master_index = File.join @directory, 'yaml'
45 @marshal_index = File.join @directory, marshal_name
47 @quick_dir = File.join @directory, 'quick'
49 @quick_marshal_dir = File.join @quick_dir, marshal_name
51 @quick_index = File.join @quick_dir, 'index'
52 @latest_index = File.join @quick_dir, 'latest_index'
54 @specs_index = File.join @directory, "specs.#{Gem.marshal_version}"
55 @latest_specs_index = File.join @directory,
56 "latest_specs.#{Gem.marshal_version}"
62 "#{@latest_specs_index}.gz",
67 "#{@marshal_index}.Z",
70 @files = files.map do |path|
71 path.sub @directory, ''
76 # Abbreviate the spec for downloading. Abbreviated specs are only used for
77 # searching, downloading and related activities and do not need deployment
78 # specific information (e.g. list of files). So we abbreviate the spec,
79 # making it much smaller for quicker downloads.
84 spec.rdoc_options = []
85 spec.extra_rdoc_files = []
91 # Build various indicies
93 def build_indicies(index)
94 progress = ui.progress_reporter index.size,
95 "Generating quick index gemspecs for #{index.size} gems",
98 index.each do |original_name, spec|
99 spec_file_name = "#{original_name}.gemspec.rz"
100 yaml_name = File.join @quick_dir, spec_file_name
101 marshal_name = File.join @quick_marshal_dir, spec_file_name
103 yaml_zipped = Gem.deflate spec.to_yaml
104 open yaml_name, 'wb' do |io| io.write yaml_zipped end
106 marshal_zipped = Gem.deflate Marshal.dump(spec)
107 open marshal_name, 'wb' do |io| io.write marshal_zipped end
109 progress.updated original_name
114 say "Generating specs index"
116 open @specs_index, 'wb' do |io|
117 specs = index.sort.map do |_, spec|
118 platform = spec.original_platform
119 platform = Gem::Platform::RUBY if platform.nil? or platform.empty?
120 [spec.name, spec.version, platform]
123 specs = compact_specs specs
125 Marshal.dump specs, io
128 say "Generating latest specs index"
130 open @latest_specs_index, 'wb' do |io|
131 specs = index.latest_specs.sort.map do |spec|
132 platform = spec.original_platform
133 platform = Gem::Platform::RUBY if platform.nil? or platform.empty?
134 [spec.name, spec.version, platform]
137 specs = compact_specs specs
139 Marshal.dump specs, io
142 say "Generating quick index"
144 quick_index = File.join @quick_dir, 'index'
145 open quick_index, 'wb' do |io|
146 io.puts index.sort.map { |_, spec| spec.original_name }
149 say "Generating latest index"
151 latest_index = File.join @quick_dir, 'latest_index'
152 open latest_index, 'wb' do |io|
153 io.puts index.latest_specs.sort.map { |spec| spec.original_name }
156 say "Generating Marshal master index"
158 open @marshal_index, 'wb' do |io|
162 progress = ui.progress_reporter index.size,
163 "Generating YAML master index for #{index.size} gems (this may take a while)",
166 open @master_index, 'wb' do |io|
167 io.puts "--- !ruby/object:#{index.class}"
170 gems = index.sort_by { |name, gemspec| gemspec.sort_obj }
171 gems.each do |original_name, gemspec|
172 yaml = gemspec.to_yaml.gsub(/^/, ' ')
173 yaml = yaml.sub(/\A ---/, '') # there's a needed extra ' ' here
174 io.print " #{original_name}:"
177 progress.updated original_name
183 say "Compressing indicies"
184 # use gzip for future files.
186 compress quick_index, 'rz'
187 paranoid quick_index, 'rz'
189 compress latest_index, 'rz'
190 paranoid latest_index, 'rz'
192 compress @marshal_index, 'Z'
193 paranoid @marshal_index, 'Z'
195 compress @master_index, 'Z'
196 paranoid @master_index, 'Z'
199 gzip @latest_specs_index
203 # Collect specifications from .gem files from the gem directory.
206 index = Gem::SourceIndex.new
208 progress = ui.progress_reporter gem_file_list.size,
209 "Loading #{gem_file_list.size} gems from #{@dest_directory}",
212 gem_file_list.each do |gemfile|
213 if File.size(gemfile.to_s) == 0 then
214 alert_warning "Skipping zero-length gem: #{gemfile}"
219 spec = Gem::Format.from_file_by_path(gemfile).spec
221 unless gemfile =~ /\/#{Regexp.escape spec.original_name}.*\.gem\z/i then
222 alert_warning "Skipping misnamed gem: #{gemfile} => #{spec.full_name} (#{spec.original_name})"
229 index.gems[spec.original_name] = spec
231 progress.updated spec.original_name
233 rescue SignalException => e
234 alert_error "Received signal, exiting"
236 rescue Exception => e
237 alert_error "Unable to process #{gemfile}\n#{e.message} (#{e.class})\n\t#{e.backtrace.join "\n\t"}"
247 # Compacts Marshal output for the specs index data source by using identical
248 # objects as much as possible.
250 def compact_specs(specs)
255 specs.map do |(name, version, platform)|
256 names[name] = name unless names.include? name
257 versions[version] = version unless versions.include? version
258 platforms[platform] = platform unless platforms.include? platform
260 [names[name], versions[version], platforms[platform]]
265 # Compress +filename+ with +extension+.
267 def compress(filename, extension)
268 data = Gem.read_binary filename
270 zipped = Gem.deflate data
272 open "#{filename}.#{extension}", 'wb' do |io|
278 # List of gem file names to index.
281 Dir.glob(File.join(@dest_directory, "gems", "*.gem"))
285 # Builds and installs indexicies.
288 make_temp_directories
289 index = collect_specs
292 rescue SignalException
294 FileUtils.rm_rf @directory
298 # Zlib::GzipWriter wrapper that gzips +filename+ on disk.
301 Zlib::GzipWriter.open "#{filename}.gz" do |io|
302 io.write Gem.read_binary(filename)
307 # Install generated indicies into the destination directory.
310 verbose = Gem.configuration.really_verbose
312 say "Moving index into production dir #{@dest_directory}" if verbose
314 @files.each do |file|
315 src_name = File.join @directory, file
316 dst_name = File.join @dest_directory, file
318 FileUtils.rm_rf dst_name, :verbose => verbose
319 FileUtils.mv src_name, @dest_directory, :verbose => verbose,
325 # Make directories for index generation
327 def make_temp_directories
328 FileUtils.rm_rf @directory
329 FileUtils.mkdir_p @directory, :mode => 0700
330 FileUtils.mkdir_p @quick_marshal_dir
334 # Ensure +path+ and path with +extension+ are identical.
336 def paranoid(path, extension)
337 data = Gem.read_binary path
338 compressed_data = Gem.read_binary "#{path}.#{extension}"
340 unless data == Gem.inflate(compressed_data) then
341 raise "Compressed file #{compressed_path} does not match uncompressed file #{path}"
346 # Sanitize the descriptive fields in the spec. Sometimes non-ASCII
347 # characters will garble the site index. Non-ASCII characters will
348 # be replaced by their XML entity equivalent.
351 spec.summary = sanitize_string(spec.summary)
352 spec.description = sanitize_string(spec.description)
353 spec.post_install_message = sanitize_string(spec.post_install_message)
354 spec.authors = spec.authors.collect { |a| sanitize_string(a) }
360 # Sanitize a single string.
362 def sanitize_string(string)
363 # HACK the #to_s is in here because RSpec has an Array of Arrays of
364 # Strings for authors. Need a way to disallow bad values on gempsec
365 # generation. (Probably won't happen.)
366 string ? string.to_s.to_xs : string