require 'optparse' require 'ostruct' require 'pathname' require 'zlib' require 'amalgalite' module Amalgalite # # Pack items into an amalgalite database. # class Packer attr_reader :packing_list attr_reader :dbfile attr_reader :options class << self def default_options { :table_name => Requires::Bootstrap::DEFAULT_TABLE, :filename_column => Requires::Bootstrap::DEFAULT_FILENAME_COLUMN, :contents_column => Requires::Bootstrap::DEFAULT_CONTENTS_COLUMN, :compressed_column => Requires::Bootstrap::DEFAULT_COMPRESSED_COLUMN, :strip_prefix => Dir.pwd, :compressed => false, :verbose => false, } end # # compress data # def gzip( data ) zipped = StringIO.new Zlib::GzipWriter.wrap( zipped ) do |io| io.write( data ) end return zipped.string end # # uncompress gzip data # def gunzip( data ) data = StringIO.new( data ) Zlib::GzipReader.new( data ).read end # # return the files in their dependency order for use for packing into a # database # def amalgalite_require_order @require_order ||= %w[ amalgalite.rb amalgalite/sqlite3/database/function.rb amalgalite/aggregate.rb amalgalite/blob.rb amalgalite/boolean.rb amalgalite/busy_timeout.rb amalgalite/column.rb amalgalite/statement.rb amalgalite/trace_tap.rb amalgalite/profile_tap.rb amalgalite/type_map.rb amalgalite/type_maps/storage_map.rb amalgalite/type_maps/text_map.rb amalgalite/type_maps/default_map.rb amalgalite/function.rb amalgalite/progress_handler.rb amalgalite/csv_table_importer.rb amalgalite/database.rb amalgalite/index.rb amalgalite/memory_database.rb amalgalite/paths.rb amalgalite/table.rb amalgalite/view.rb amalgalite/schema.rb amalgalite/version.rb amalgalite/sqlite3/version.rb amalgalite/sqlite3/constants.rb amalgalite/sqlite3/status.rb amalgalite/sqlite3/database/status.rb amalgalite/sqlite3.rb amalgalite/taps/io.rb amalgalite/taps/console.rb amalgalite/taps.rb amalgalite/packer.rb amalgalite/core_ext/kernel/require.rb amalgalite/requires.rb ] end end # # Create a new packer instance with the list of items to pack and all the # options # def initialize( options = {} ) @options = Packer.default_options.merge( options ) @dbfile = @options[:dbfile] || Requires::Bootstrap::DEFAULT_DB end # # The SQL to create the table for storing ruby code # def create_table_sql <<-create CREATE TABLE #{options[:table_name]} ( id INTEGER PRIMARY KEY AUTOINCREMENT, #{options[:filename_column]} TEXT UNIQUE, #{options[:compressed_column]} BOOLEAN, #{options[:contents_column]} BLOB ); create end # # Make sure that the dbfile exists and has the appropriate schema. # def check_db( db ) if db.schema.tables[ options[:table_name] ] and options[:drop_table] then STDERR.puts "Dropping table #{options[:table_name]}" if options[:verbose] db.execute("DROP TABLE #{options[:table_name]}") db.reload_schema! end unless db.schema.tables[ options[:table_name] ] db.execute( create_table_sql ) db.reload_schema! end end # # Stores all the .rb files in the list into the given database. The prefix # is the file system path to remove from the front of the path on each file # # manifest is an array of OpenStructs. # def pack_files( manifest ) db = Amalgalite::Database.new( dbfile ) check_db( db ) max_width = manifest.collect{ |m| m.require_path.length }.sort.last contents_column = db.schema.tables[ options[:table_name] ].columns[ options[:contents_column] ] db.transaction do |trans| manifest.each do |file_info| msg = " -> #{file_info.require_path.ljust( max_width )} : " begin if options[:merge] then trans.execute( "DELETE FROM #{options[:table_name]} WHERE #{options[:filename_column]} = ?", file_info.require_path ) end trans.prepare("INSERT INTO #{options[:table_name]}(#{options[:filename_column]}, #{options[:compressed_column]}, #{options[:contents_column]}) VALUES( $filename, $compressed, $contents)") do |stmt| contents = IO.readlines( file_info.file_path ) if options[:self] then contents.each { |l| l.gsub!( /^(\s*require .*)$/m, "# commented out by #{self.class.name} \\1") } end contents = contents.join if options[:compressed] then contents = Packer.gzip( contents ) end content_io = StringIO.new( contents ) stmt.execute( "$filename" => file_info.require_path, "$contents" => Amalgalite::Blob.new( :io => content_io, :column => contents_column ), "$compressed" => options[:compressed] ) STDERR.puts "#{msg} stored #{file_info.file_path}" if options[:verbose] end rescue => e STDERR.puts "#{msg} error #{e}" end end end end # # given a file, see if it can be found in the ruby load path, if so, return that # full path # def full_path_of( rb_file ) $LOAD_PATH.each do |load_path| guess = File.expand_path( File.join( load_path, rb_file ) ) return guess if File.exist?( guess ) end return nil end # # Make the manifest for packing # def make_manifest( file_list ) manifest = [] prefix_path = ::Pathname.new( options[:strip_prefix] ) file_list.each do |f| file_path = ::Pathname.new( File.expand_path( f ) ) m = ::OpenStruct.new # if it is a directory then grab all the .rb files from it if File.directory?( file_path ) then manifest.concat( make_manifest( Dir.glob( File.join( f, "**", "*.rb" ) ) ) ) next elsif File.readable?( file_path ) then m.require_path = file_path.relative_path_from( prefix_path ) m.file_path = file_path.realpath.to_s elsif lp = full_path_of( f ) then m.require_path = f m.file_path = lp else STDERR.puts "Unable to add #{f} to the manifest, cannot find the file on disk" next end # Make sure that we can handle files without the .rb extension # if we have to. This means bin/foo works as a require path # without requiring bin/foo to actually be bin/foo.rb m.require_path = m.require_path.to_s.sub(/\.rb\Z/,'') manifest << m end return manifest end # # Given a list of files pack them into the associated database and table. # def pack( file_list ) manifest = make_manifest( file_list ) pack_files( manifest ) end end end