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?
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 [spec.name, spec.version, spec.original_platform]
135 specs = compact_specs specs
137 Marshal.dump specs, io
140 say "Generating quick index"
142 quick_index = File.join @quick_dir, 'index'
143 open quick_index, 'wb' do |io|
144 io.puts index.sort.map { |_, spec| spec.original_name }
147 say "Generating latest index"
149 latest_index = File.join @quick_dir, 'latest_index'
150 open latest_index, 'wb' do |io|
151 io.puts index.latest_specs.sort.map { |spec| spec.original_name }
154 say "Generating Marshal master index"
156 open @marshal_index, 'wb' do |io|
160 progress = ui.progress_reporter index.size,
161 "Generating YAML master index for #{index.size} gems (this may take a while)",
164 open @master_index, 'wb' do |io|
165 io.puts "--- !ruby/object:#{index.class}"
168 gems = index.sort_by { |name, gemspec| gemspec.sort_obj }
169 gems.each do |original_name, gemspec|
170 yaml = gemspec.to_yaml.gsub(/^/, ' ')
171 yaml = yaml.sub(/\A ---/, '') # there's a needed extra ' ' here
172 io.print " #{original_name}:"
175 progress.updated original_name
181 say "Compressing indicies"
182 # use gzip for future files.
184 compress quick_index, 'rz'
185 paranoid quick_index, 'rz'
187 compress latest_index, 'rz'
188 paranoid latest_index, 'rz'
190 compress @marshal_index, 'Z'
191 paranoid @marshal_index, 'Z'
193 compress @master_index, 'Z'
194 paranoid @master_index, 'Z'
197 gzip @latest_specs_index
201 # Collect specifications from .gem files from the gem directory.
204 index = Gem::SourceIndex.new
206 progress = ui.progress_reporter gem_file_list.size,
207 "Loading #{gem_file_list.size} gems from #{@dest_directory}",
210 gem_file_list.each do |gemfile|
211 if File.size(gemfile.to_s) == 0 then
212 alert_warning "Skipping zero-length gem: #{gemfile}"
217 spec = Gem::Format.from_file_by_path(gemfile).spec
219 unless gemfile =~ /\/#{Regexp.escape spec.original_name}.*\.gem\z/i then
220 alert_warning "Skipping misnamed gem: #{gemfile} => #{spec.full_name} (#{spec.original_name})"
227 index.gems[spec.original_name] = spec
229 progress.updated spec.original_name
231 rescue SignalException => e
232 alert_error "Received signal, exiting"
234 rescue Exception => e
235 alert_error "Unable to process #{gemfile}\n#{e.message} (#{e.class})\n\t#{e.backtrace.join "\n\t"}"
245 # Compacts Marshal output for the specs index data source by using identical
246 # objects as much as possible.
248 def compact_specs(specs)
253 specs.map do |(name, version, platform)|
254 names[name] = name unless names.include? name
255 versions[version] = version unless versions.include? version
256 platforms[platform] = platform unless platforms.include? platform
258 [names[name], versions[version], platforms[platform]]
263 # Compress +filename+ with +extension+.
265 def compress(filename, extension)
266 data = Gem.read_binary filename
268 zipped = Gem.deflate data
270 open "#{filename}.#{extension}", 'wb' do |io|
276 # List of gem file names to index.
279 Dir.glob(File.join(@dest_directory, "gems", "*.gem"))
283 # Builds and installs indexicies.
286 FileUtils.rm_rf @directory
287 FileUtils.mkdir_p @directory, :mode => 0700
288 FileUtils.mkdir_p @quick_marshal_dir
290 index = collect_specs
293 rescue SignalException
295 FileUtils.rm_rf @directory
299 # Zlib::GzipWriter wrapper that gzips +filename+ on disk.
302 Zlib::GzipWriter.open "#{filename}.gz" do |io|
303 io.write Gem.read_binary(filename)
308 # Install generated indicies into the destination directory.
311 verbose = Gem.configuration.really_verbose
313 say "Moving index into production dir #{@dest_directory}" if verbose
315 @files.each do |file|
316 src_name = File.join @directory, file
317 dst_name = File.join @dest_directory, file
319 FileUtils.rm_rf dst_name, :verbose => verbose
320 FileUtils.mv src_name, @dest_directory, :verbose => verbose
325 # Ensure +path+ and path with +extension+ are identical.
327 def paranoid(path, extension)
328 data = Gem.read_binary path
329 compressed_data = Gem.read_binary "#{path}.#{extension}"
331 unless data == Gem.inflate(compressed_data) then
332 raise "Compressed file #{compressed_path} does not match uncompressed file #{path}"
337 # Sanitize the descriptive fields in the spec. Sometimes non-ASCII
338 # characters will garble the site index. Non-ASCII characters will
339 # be replaced by their XML entity equivalent.
342 spec.summary = sanitize_string(spec.summary)
343 spec.description = sanitize_string(spec.description)
344 spec.post_install_message = sanitize_string(spec.post_install_message)
345 spec.authors = spec.authors.collect { |a| sanitize_string(a) }
350 # Sanitize a single string.
352 def sanitize_string(string)
353 # HACK the #to_s is in here because RSpec has an Array of Arrays of
354 # Strings for authors. Need a way to disallow bad values on gempsec
355 # generation. (Probably won't happen.)
356 string ? string.to_s.to_xs : string