Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

361 lines (321 sloc) 10.606 kB
#!/usr/bin/ruby
# MacRuby AOT Compiler.
#
# This file is covered by the Ruby license.
#
# Copyright (C) 2009, Apple Inc
require 'optparse'
require 'rbconfig'
class Compiler
NAME = File.basename(__FILE__)
VALID_ARCHS = ['i386', 'x86_64']
def initialize(argv)
@mode = :normal
@archs = []
@internal = argv.delete('--internal')
# Parse arguments.
OptionParser.new do |opts|
opts.banner = "Usage: #{NAME} [options] file..."
opts.on('-c', 'Compile and assemble, but do not link') { @dont_link = true }
opts.on('-o <file>', 'Place the output into <file>') { |output| @output = output }
#opts.on('--mode [MODE]', "Select compilation mode (normal or full)") { |mode| @mode = mode.intern }
opts.on('--static', "Create a standalone static executable") { @static = true }
opts.on('--dylib', "Create a dynamic library") { @dylib = true }
opts.on('-C', 'Compile, assemble and link a loadable object file') { @bundle = true }
opts.on('-a', '--arch <ARCH>', 'Compile for specified CPU architecture') { |arch| @archs << arch }
opts.on('-v', '--version', 'Display the version') { puts RUBY_DESCRIPTION; exit 1 }
opts.on('-V', '--verbose', 'Print every command line executed') { @verbose = true }
opts.on('-h', '--help', 'Display this information') { die opts }
begin
opts.parse!(argv)
rescue OptionParser::InvalidOption => e
die e, opts
end
die opts if argv.empty?
@files = argv
if @mode != :normal and @mode != :full
die "Invalid mode `#{@mode}'. Possible choices are: normal, full"
end
@archs.uniq!
@archs << RUBY_ARCH if @archs.empty?
@archs.each do |arch|
if not VALID_ARCHS.include?(arch)
die "Invalid CPU architecture `#{arch}'. Possible values are: " + VALID_ARCHS.join(", ")
end
end
end
# Locate necessary programs.
@macruby = locate(@internal ? './miniruby' : 'macruby')
if @internal
@llc = File.join(RbConfig::CONFIG['LLVM_PATH'], 'bin/llc')
die "llc not found as #{@llc}" unless File.exist?(@llc)
else
@llc = locate('llc', true)
end
@gcc = locate('gcc')
@gcxx = locate('g++')
@nm = locate('nm')
@lipo = locate('lipo')
# Misc.
@tmpdir = (ENV['TMPDIR'] or '/tmp')
@tmpfiles = []
end
def run
if @mode == :full
die "Full compilation mode is not implemented yet!"
end
if @dont_link or @bundle
die "Cannot specify --static when not building an executable" if @static
die "Cannot specify -c or -C when building a dynamic library" if @dylib
die "Cannot specify -c and -C at the same time" if @bundle and @dont_link
if @files.size > 1 and @output
die "Cannot specify -o with -c or -C and multiple input files"
end
@files.each do |file|
if File.extname(file) != '.rb'
die "Given input file `#{file}' must be a Ruby source file (.rb)"
end
if @bundle
compile_bundle(file, @output)
else
compile_object(file, @output)
end
end
else
die "Cannot specify --static and --dylib at the same time" if @dylib and @static
objs = @files.map do |file|
die "Given input file `#{file} must exist" unless File.exist?(file)
case File.extname(file)
when '.rb'
compile_object(file, nil)
when '.o'
[file, find_init_func(file)]
when '.dylib'
[file, nil]
else
die "Given input file `#{file}' must be either a Ruby source file (.rb) or a Mach-O object file (.o) or dynamic library (.dylib)"
end
end
if @dylib
die "-o must be specified when building a dynamic library" unless @output
compile_dylib(objs, @output)
else
compile_executable(objs, @output)
end
end
end
def cleanup
@tmpfiles.each { |x| File.delete(x) }
end
private
def compile_object(path, output)
base = File.basename(path, '.rb')
output ||= File.join(File.dirname(path), base + '.o')
# Generate init function (must be unique).
uuid = `uuidgen`.strip.gsub('-', '')
init_func = "MREP_#{uuid}"
tmp_objs = []
@archs.each do |arch|
# Compile the file into LLVM bitcode.
bc = gen_tmpfile(base + arch, 'bc')
execute("arch -#{arch} #{@macruby} --emit-llvm \"#{bc}\" #{init_func} \"#{path}\"")
# Compile the bitcode as assembly.
asm = gen_tmpfile(base + arch, 's')
execute("#{@llc} -f \"#{bc}\" -o=\"#{asm}\" -march=#{llc_arch(arch)} -relocation-model=pic -enable-eh")
# Compile the assembly.
tmp_obj = gen_tmpfile(base + arch, 'o')
execute("#{@gcc} -c -arch #{arch} \"#{asm}\" -o \"#{tmp_obj}\"")
tmp_objs << tmp_obj
end
# Link the architecture objects.
cli_tmp_objs = tmp_objs.map do |obj|
'"' + obj + '"'
end
execute("#{@lipo} -create #{cli_tmp_objs.join(' ')} -output \"#{output}\"")
[output, init_func]
end
def compile_bundle(file, output)
base = File.basename(file, '.rb')
obj = gen_tmpfile(base, 'o')
obj, init_func = compile_object(file, obj)
output ||= File.join(File.dirname(file), base + '.rbo')
# Generate main file.
main_txt = <<EOS
extern "C" {
void *#{init_func}(void *, void *);
void *rb_vm_top_self(void);
__attribute__((constructor)) static void __init__(void) {
#{init_func}(rb_vm_top_self(), 0);
}
}
EOS
# Build.
main = gen_tmpfile('main', 'c')
File.open(main, 'w') { |io| io.write(main_txt) }
linkf = @internal ? "-L. -lmacruby" : "-framework MacRuby"
execute("#{@gcxx} \"#{main}\" -dynamic -bundle -undefined suppress -flat_namespace #{arch_flags} #{linkf} \"#{obj}\" -o \"#{output}\"")
end
def compile_dylib(objs_data, output)
# Generate main file.
main_txt = <<EOS
extern "C" {
void rb_vm_aot_feature_provide(const char *, void *);
EOS
objs_data.each do |obj, init_func|
next if init_func == nil
main_txt << "void *#{init_func}(void *, void *);\n"
end
main_txt << <<EOS
__attribute__((constructor)) static void __init__(void) {
EOS
objs_data.each do |obj, init_func|
main_txt << "rb_vm_aot_feature_provide(\"#{feature_name(obj)}\", (void *)#{init_func});\n"
end
main_txt << "}}"
# Build.
main = gen_tmpfile('main', 'c')
File.open(main, 'w') { |io| io.write(main_txt) }
linkf = @internal ? "-L. -lmacruby" : "-framework MacRuby"
objs = objs_data.map { |obj, f| "\"#{obj}\"" }.join(' ')
execute("#{@gcxx} \"#{main}\" -dynamiclib -dynamic -undefined suppress -flat_namespace #{arch_flags} #{linkf} #{objs} -o \"#{output}\"")
end
def compile_executable(objs_data, output)
output ||= 'a.out'
raise if objs_data.empty?
die "first object file must be a Ruby source file or object" if objs_data[0][1] == nil
# Generate main file.
main_txt = <<EOS
extern "C" {
void ruby_sysinit(int *, char ***);
void ruby_init(void);
void ruby_init_loadpath(void);
void ruby_script(const char *);
void ruby_set_argv(int, char **);
void rb_vm_init_compiler(void);
void rb_vm_aot_feature_provide(const char *, void *);
void *rb_vm_top_self(void);
void rb_vm_print_current_exception(void);
void rb_exit(int);
EOS
objs_data.each do |obj, init_func|
next if init_func == nil
main_txt << "void *#{init_func}(void *, void *);\n"
end
main_txt << <<EOS
}
int main(int argc, char **argv)
{
const char *progname = argv[0];
ruby_sysinit(&argc, &argv);
if (argc > 0) {
argc--;
argv++;
}
ruby_init();
ruby_init_loadpath();
ruby_set_argv(argc, argv);
rb_vm_init_compiler();
ruby_script(progname);
try {
EOS
objs_data[1..-1].each do |obj, init_func|
next if init_func == nil
main_txt << "rb_vm_aot_feature_provide(\"#{feature_name(obj)}\", (void *)#{init_func});\n"
end
main_txt << <<EOS
void *self = rb_vm_top_self();
#{objs_data[0][1]}(self, 0);
}
catch (...) {
rb_vm_print_current_exception();
rb_exit(1);
}
rb_exit(0);
}
EOS
# Prepare objects.
objs = []
objs_data.each { |o, _| objs << o }
# Compile main file.
main = gen_tmpfile('main', 'mm')
File.open(main, 'w') { |io| io.write(main_txt) }
main_o = gen_tmpfile('main', 'o')
execute("#{@gcxx} \"#{main}\" -c #{arch_flags} -o \"#{main_o}\" -fobjc-gc")
objs.unshift(main_o)
# Link all objects into executable.
linkf = @static ?
"-L#{RbConfig::CONFIG['libdir']} #{RbConfig::CONFIG['LIBRUBYARG_STATIC_REALLY']}" :
"-framework MacRuby -lobjc"
line = "#{@gcxx} -o \"#{output}\" #{arch_flags} #{linkf} "
objs.each { |o| line << " \"#{o}\"" }
execute(line)
end
def execute(line)
$stderr.puts line if @verbose
ret = `#{line}`
unless $?.success?
die_str = "Error when executing `#{line}'"
die_str += "\n#{ret}" unless ret.empty?
die die_str
end
ret
end
def locate(progname, must_be_in_bindir=false)
path = File.join(Config::CONFIG['bindir'], progname)
unless File.exist?(path)
if must_be_in_bindir
die "Can't locate program `#{progname}' in #{Config::CONFIG['bindir']}"
end
path = `which #{progname}`.strip
die "Can't locate program `#{progname}'" if path.empty?
end
path
end
def llc_arch(arch)
# LLVM uses a different convention for architecture names.
case arch
when 'i386'; 'x86'
when 'x86_64'; 'x86-64'
else; arch
end
end
def arch_flags
@archs.map { |x| "-arch #{x}" }.join(' ')
end
def find_init_func(obj)
output = `#{@nm} -j "#{obj}"`
output.scan(/^_MREP_.*$/).reject { |func|
# Ignore non-main functions.
func.include?('ruby_scope')
}.map { |func|
# Ignore the very first character (_).
func[1..-1]
}[0]
end
def feature_name(obj)
# Remove trailing ./ if exists.
if obj[0..1] == './'
obj[0..1] = ''
end
if obj[0] == '/'
$stderr.puts "warning: object file path `#{obj}' is absolute and not relative, this might cause a problem later at runtime"
end
# Strip the extension.
obj = obj.sub(/#{File.extname(obj)}$/, '')
end
def gen_tmpfile(base, ext)
file = File.join(@tmpdir, "#{base}-#{$$}.#{ext}")
@tmpfiles << file
file
end
def die(*args)
$stderr.puts args
exit 1
end
end
app = Compiler.new(ARGV)
begin
app.run
ensure
app.cleanup
end
Jump to Line
Something went wrong with that request. Please try again.