Skip to content
Fetching contributors…
Cannot retrieve contributors at this time
418 lines (375 sloc) 12.7 KB
#!/usr/bin/ruby
# MacRuby AOT Compiler.
#
# This file is covered by the Ruby license.
#
# Copyright (C) 2009-2010, Apple Inc
require 'optparse'
require 'rbconfig'
class Compiler
NAME = File.basename(__FILE__)
VALID_ARCHS = ['i386', 'x86_64']
def initialize(argv)
@archs = []
@internal = argv.delete('--internal')
@frameworks = %w{Foundation}
@linkf = []
# 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('--static', "Create a standalone static executable") { @static = true }
opts.on('--framework <name>', "Link standalone static executable with given framework") { |p| @frameworks << p }
opts.on('--sdk <path>', "Use SDK when compiling standalone static executable") { |x| @sdk = x }
opts.on('--dylib', "Create a dynamic library") { @dylib = true }
opts.on('--compatibility_version <VERSION>', "Compatibility Version for linking") { |ver| @linkf << "-compatibility_version #{ver}" }
opts.on('--current_version <VERSION>', "Current Version for linking") { |ver| @linkf << "-current_version #{ver}" }
opts.on('--install_name <NAME>', "Install Name for linking") { |name| @linkf << "-install_name #{name}" }
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
@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
if @sdk and !File.exist?(@sdk)
die "Given SDK path `#{@sdk}' doesn't exist."
end
@frameworks.uniq!
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')
@strip = locate('strip')
# Misc.
@tmpdir = (ENV['TMPDIR'] or '/tmp')
@tmpfiles = []
@llc_flags = '-relocation-model=pic -disable-fp-elim '
if system("#{@llc} -help | grep jit-enable-eh >& /dev/null")
@llc_flags << '-jit-enable-eh'
else
@llc_flags << '-enable-eh'
end
end
def run
@uses_bs_flags = ''
if @static
$stderr.puts "Warning: static compilation is currently a work in progress and provided for development only. The compilation process may simply fail or generate non-functional machine code objects. Use it at your own risk."
# BridgeSupport compilation is only enabled during static compilation.
@frameworks.each do |f|
p =
if File.exist?(f)
"#{f}/Resources/BridgeSupport/#{File.basename(f)}Full.bridgesupport"
else
base = (@sdk || '')
File.join(base, "/System/Library/Frameworks/#{f}.framework/Resources/BridgeSupport/#{f}Full.bridgesupport")
end
if File.exist?(p)
@uses_bs_flags << "--uses-bs #{p} "
else
$stderr.puts "Couldn't locate the Full BridgeSupport file for framework `#{f}' at path `#{p}', compilation might generate a unusable binary!"
end
end
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) if File.exist?(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|
# Locate the kernel bitcode file if needed.
env = ''
if @sdk
kernel_path = File.join(@sdk, "usr/lib/libmacruby-kernel-#{arch}.bc")
if File.exist?(kernel_path)
env = "/usr/bin/env VM_KERNEL_PATH=\"#{kernel_path}\""
end
end
# Compile the file into LLVM bitcode.
bc = gen_tmpfile(base + arch, 'bc')
execute("#{env} arch -#{arch} #{@macruby} #{@uses_bs_flags} --emit-llvm \"#{bc}\" #{init_func} \"#{path}\"")
# Compile the bitcode as assembly.
asm = gen_tmpfile(base + arch, 's')
execute("#{@llc} \"#{bc}\" -o=\"#{asm}\" -march=#{llc_arch(arch)} #{@llc_flags}")
# Compile the assembly.
tmp_obj = gen_tmpfile(base + arch, 'o')
execute("#{@gcc} -fexceptions -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 rb_mrep_register(void *);
void *#{init_func}(void *, void *);
__attribute__((constructor)) static void __init__(void) {
rb_mrep_register((void *)#{init_func});
}
}
EOS
# Build.
main = gen_tmpfile('main', 'c')
File.open(main, 'w') { |io| io.write(main_txt) }
linkf = @internal ? "-L. -lmacruby" : "-L#{RbConfig::CONFIG['libdir']} -lmacruby"
execute("#{@gcxx} \"#{main}\" -fexceptions -dynamic -bundle -undefined suppress -flat_namespace #{arch_flags} #{linkf} \"#{obj}\" -o \"#{output}\"")
strip(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" : "-L#{RbConfig::CONFIG['libdir']} -lmacruby"
objs = objs_data.map { |obj, f| "\"#{obj}\"" }.join(' ')
execute("#{@gcxx} \"#{main}\" -dynamiclib -dynamic -undefined suppress -flat_namespace #{arch_flags} #{@linkf.join(' ')} #{objs} -o \"#{output}\"")
strip(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_init_jit(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();
rb_vm_init_jit();
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.
path = @internal ? "-L." : "-L#{RbConfig::CONFIG['libdir']}"
linkf = ""
if @sdk
path = ''
linkf << "-F#{@sdk}/System/Library/Frameworks -L#{@sdk}/usr/lib "
end
linkf << "-lobjc -licucore -lauto "
@frameworks.each { |f| linkf << "-framework #{f} " }
linkf << (@static ?
"#{path} #{RbConfig::CONFIG['LIBRUBYARG_STATIC_REALLY']}" :
"#{path} -lmacruby")
line = "#{@gcxx} -o \"#{output}\" #{arch_flags} #{linkf} "
objs.each { |o| line << " \"#{o}\"" }
execute(line)
strip(output)
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 strip(bin)
execute("#{@strip} -x \"#{bin}\"")
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.