ruby/sample/iseq_loader.rb

244 строки
6.4 KiB
Ruby

#
# iseq_loader.rb - sample of compiler/loader for binary compiled file
#
# Usage as a compiler: ruby iseq_loader.rb [file or directory] ...
#
# It compiles and stores specified files.
# If directories are specified, then compiles and stores all *.rb files.
# (using Dir.glob)
#
# TODO: add remove option
# TODO: add verify option
#
# Usage as a loader: simply require this file with the following setting.
#
# Setting with environment variables.
#
# * RUBY_ISEQ_LOADER_STORAGE to select storage type
# * dbm: use dbm
# * fs: [default] use file system. locate a compiled binary files in same
# directory of scripts like Rubinius. foo.rb.yarb will be created for foo.rb.
# * fs2: use file system. locate compiled file in specified directory.
# * nothing: do nothing.
#
# * RUBY_ISEQ_LOADER_STORAGE_DIR to select directory
# * default: ~/.ruby_binaries/
#
# * RUBY_ISEQ_LOADER_STORAGE_COMPILE_IF_NOT_COMPILED
# * true: store compiled file if compiled data is not available.
# * false: [default] do nothing if there is no compiled iseq data.
class RubyVM::InstructionSequence
$ISEQ_LOADER_LOADED = 0
$ISEQ_LOADER_COMPILED = 0
$ISEQ_LOADER_IGNORED = 0
LAUNCHED_TIME = Time.now
COMPILE_FILE_ENABLE = false || true
COMPILE_VERBOSE = $VERBOSE || false # || true
COMPILE_DEBUG = ENV['RUBY_ISEQ_LOADER_DEBUG']
COMPILE_IF_NOT_COMPILED = ENV['RUBY_ISEQ_LOADER_STORAGE_COMPILE_IF_NOT_COMPILED'] == 'true'
at_exit{
STDERR.puts "[ISEQ_LOADER] #{Process.pid} time: #{Time.now - LAUNCHED_TIME}, " +
"loaded: #{$ISEQ_LOADER_LOADED}, " +
"compiled: #{$ISEQ_LOADER_COMPILED}, " +
"ignored: #{$ISEQ_LOADER_IGNORED}"
} if COMPILE_VERBOSE
unless cf_dir = ENV['RUBY_ISEQ_LOADER_STORAGE_DIR']
cf_dir = File.expand_path("~/.ruby_binaries")
unless File.exist?(cf_dir)
Dir.mkdir(cf_dir)
end
end
CF_PREFIX = "#{cf_dir}/cb."
class NullStorage
def load_iseq fname; end
def compile_and_save_isq fname; end
def unlink_compiled_iseq; end
end
class BasicStorage
def initialize
require 'digest/sha1'
end
def load_iseq fname
iseq_key = iseq_key_name(fname)
if compiled_iseq_exist?(fname, iseq_key) && compiled_iseq_is_younger?(fname, iseq_key)
$ISEQ_LOADER_LOADED += 1
STDERR.puts "[ISEQ_LOADER] #{Process.pid} load #{fname} from #{iseq_key}" if COMPILE_DEBUG
binary = read_compiled_iseq(fname, iseq_key)
iseq = RubyVM::InstructionSequence.load_from_binary(binary)
# p [extra_data(iseq.path), RubyVM::InstructionSequence.load_from_binary_extra_data(binary)]
# raise unless extra_data(iseq.path) == RubyVM::InstructionSequence.load_from_binary_extra_data(binary)
iseq
elsif COMPILE_IF_NOT_COMPILED
compile_and_save_iseq(fname, iseq_key)
else
$ISEQ_LOADER_IGNORED += 1
# p fname
nil
end
end
def extra_data fname
"SHA-1:#{::Digest::SHA1.file(fname).digest}"
end
def compile_and_save_iseq fname, iseq_key = iseq_key_name(fname)
$ISEQ_LOADER_COMPILED += 1
STDERR.puts "[RUBY_COMPILED_FILE] compile #{fname}" if COMPILE_DEBUG
iseq = RubyVM::InstructionSequence.compile_file(fname)
binary = iseq.to_binary(extra_data(fname))
write_compiled_iseq(fname, iseq_key, binary)
iseq
end
# def unlink_compiled_iseq; nil; end # should implement at sub classes
private
def iseq_key_name fname
fname
end
# should implement at sub classes
# def compiled_iseq_younger? fname, iseq_key; end
# def compiled_iseq_exist? fname, iseq_key; end
# def read_compiled_file fname, iseq_key; end
# def write_compiled_file fname, iseq_key, binary; end
end
class FSStorage < BasicStorage
def initialize
super
require 'fileutils'
@dir = CF_PREFIX + "files"
unless File.directory?(@dir)
FileUtils.mkdir_p(@dir)
end
end
def unlink_compiled_iseq
File.unlink(compile_file_path)
end
private
def iseq_key_name fname
"#{fname}.yarb" # same directory
end
def compiled_iseq_exist? fname, iseq_key
File.exist?(iseq_key)
end
def compiled_iseq_is_younger? fname, iseq_key
File.mtime(iseq_key) >= File.mtime(fname)
end
def read_compiled_iseq fname, iseq_key
File.open(iseq_key, 'rb'){|f| f.read}
end
def write_compiled_iseq fname, iseq_key, binary
File.open(iseq_key, 'wb'){|f| f.write(binary)}
end
end
class FS2Storage < FSStorage
def iseq_key_name fname
@dir + fname.gsub(/[^A-Za-z0-9\._-]/){|c| '%02x' % c.ord} # special directory
end
end
class DBMStorage < BasicStorage
def initialize
require 'dbm'
@db = DBM.open(CF_PREFIX+'db')
end
def unlink_compiled_iseq
@db.delete fname
end
private
def date_key_name fname
"date.#{fname}"
end
def iseq_key_name fname
"body.#{fname}"
end
def compiled_iseq_exist? fname, iseq_key
@db.has_key? iseq_key
end
def compiled_iseq_is_younger? fname, iseq_key
date_key = date_key_name(fname)
if @db.has_key? date_key
@db[date_key].to_i >= File.mtime(fname).to_i
end
end
def read_compiled_iseq fname, iseq_key
@db[iseq_key]
end
def write_compiled_iseq fname, iseq_key, binary
date_key = date_key_name(fname)
@db[iseq_key] = binary
@db[date_key] = Time.now.to_i
end
end
STORAGE = case ENV['RUBY_ISEQ_LOADER_STORAGE']
when 'dbm'
DBMStorage.new
when 'fs'
FSStorage.new
when 'fs2'
FS2Storage.new
when 'null'
NullStorage.new
else
FSStorage.new
end
STDERR.puts "[ISEQ_LOADER] use #{STORAGE.class} " if COMPILE_VERBOSE
def self.load_iseq fname
STORAGE.load_iseq(fname)
end
def self.compile_and_save_iseq fname
STORAGE.compile_and_save_iseq fname
end
def self.unlink_compiled_iseq fname
STORAGE.unlink_compiled_iseq fname
end
end
if __FILE__ == $0
ARGV.each{|path|
if File.directory?(path)
pattern = File.join(path, '**/*.rb')
Dir.glob(pattern){|file|
begin
RubyVM::InstructionSequence.compile_and_save_iseq(file)
rescue SyntaxError => e
STDERR.puts e
end
}
else
RubyVM::InstructionSequence.compile_and_save_iseq(path)
end
}
end