#!/usr/bin/env ruby #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ # Generate the yaml/yaml.Z index files for a gem server directory. # # Usage: generate_yaml_index.rb --dir DIR [--verbose] $:.unshift '~/rubygems' if File.exist? "~/rubygems" require 'optparse' require 'rubygems' require 'zlib' require 'digest/sha2' begin require 'builder/xchar' rescue LoadError fail "index_gem_repository requires that the XML Builder library be installed" end Gem.manage_gems ###################################################################### # Mixin that provides a +compress+ method for compressing files on # disk. # module Compressor # Compress the given file. def compress(filename, ext="rz") File.open(filename + ".#{ext}", "w") do |file| file.write(zip(File.read(filename))) end end # Return a compressed version of the given string. def zip(string) Zlib::Deflate.deflate(string) end # Return an uncompressed version of a compressed string. def unzip(string) Zlib::Inflate.inflate(string) end end ###################################################################### # Announcer provides a way of announcing activities to the user. # module Announcer # Announce +msg+ to the user. def announce(msg) puts msg if @options[:verbose] end end ###################################################################### # Abstract base class for building gem indicies. Uses the template # pattern with subclass specialization in the +begin_index+, # +end_index+ and +cleanup+ methods. # class AbstractIndexBuilder include Compressor include Announcer # Build a Gem index. Yields to block to handle the details of the # actual building. Calls +begin_index+, # +end_index+ and +cleanup+ # at appropriate times to customize basic operations. def build if ! @enabled yield else unless File.exist?(@directory) FileUtils.mkdir_p(@directory) end fail "not a directory: #{@directory}" unless File.directory?(@directory) File.open(File.join(@directory, @filename), "w") do |file| @file = file start_index yield end_index end cleanup end ensure @file = nil end # Called immediately before the yield in build. The index file is # open and availabe as @file. def start_index end # Called immediately after the yield in build. The index file is # still open and available as @file. def end_index end # Called from within builder after the index file has been closed. def cleanup end end ###################################################################### # Construct the master Gem index file. # class MasterIndexBuilder < AbstractIndexBuilder def initialize(filename, options) @filename = filename @options = options @directory = options[:directory] @enabled = true end def start_index super @file.puts "--- !ruby/object:Gem::Cache" @file.puts "gems:" end def cleanup super index_file_name = File.join(@directory, @filename) compress(index_file_name, "Z") paranoid(index_file_name, "#{index_file_name}.Z") end def add(spec) @file.puts " #{spec.full_name}: #{nest(spec.to_yaml)}" end def nest(yaml_string) yaml_string[4..-1].gsub(/\n/, "\n ") end private def paranoid(fn, compressed_fn) data = File.read(fn) compressed_data = File.read(compressed_fn) if data != unzip(compressed_data) fail "Compressed file #{compressed_fn} does not match uncompressed file #{fn}" end end end ###################################################################### # Construct a quick index file and all of the individual specs to # support incremental loading. # class QuickIndexBuilder < AbstractIndexBuilder def initialize(filename, options) @filename = filename @options = options @directory = options[:quick_directory] @enabled = options[:quick] end def cleanup compress(File.join(@directory, @filename)) end def add(spec) return unless @enabled @file.puts spec.full_name fn = File.join(@directory, "#{spec.full_name}.gemspec.rz") File.open(fn, "w") do |gsfile| gsfile.write(zip(spec.to_yaml)) end end end ###################################################################### # Top level class for building the repository index. Initialize with # an options hash and call +build_index+. # class Indexer include Compressor include Announcer # Create an indexer with the options specified by the options hash. def initialize(options) @options = options.dup @directory = @options[:directory] @options[:quick_directory] = File.join(@directory, "quick") @master_index = MasterIndexBuilder.new("yaml", @options) @quick_index = QuickIndexBuilder.new("index", @options) end # Build the index. def build_index announce "Building Server Index" FileUtils.rm_r(@options[:quick_directory]) rescue nil @master_index.build do @quick_index.build do gem_file_list.each do |gemfile| spec = Gem::Format.from_file_by_path(gemfile).spec abbreviate(spec) sanitize(spec) announce " ... adding #{spec.full_name}" @master_index.add(spec) @quick_index.add(spec) end end end end # List of gem file names to index. def gem_file_list Dir.glob(File.join(@directory, "gems", "*.gem")) 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 end # 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. 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) } spec end # Sanitize a single string. def sanitize_string(string) string ? string.to_xs : string end end ###################################################################### # Top Level Functions ###################################################################### def handle_options(args) # default options options = { :directory => '.', :verbose => false, :quick => true, } args.options do |opts| opts.on_tail("--help", "show this message") do puts opts exit end opts.on( '-d', '--dir=DIRNAME', '--directory=DIRNAME', "repository base dir containing gems subdir", String) do |value| options[:directory] = value end opts.on('--[no-]quick', "include quick index") do |value| options[:quick] = value end opts.on('-v', '--verbose', "show verbose output") do |value| options[:verbose] = value end opts.on('-V', '--version', "show version") do |value| puts Gem::RubyGemsVersion exit end opts.parse! end if options[:directory].nil? puts "Error, must specify directory name. Use --help" exit elsif ! File.exist?(options[:directory]) || ! File.directory?(options[:directory]) puts "Error, unknown directory name #{directory}." exit end options end # Main program. def main_index(args) options = handle_options(args) Indexer.new(options).build_index end if __FILE__ == $0 then main_index(ARGV) end