Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

445 lines (388 sloc) 13.445 kb
#!/usr/bin/ruby
# MacRuby AOT Compiler.
#
# This file is covered by the Ruby license.
#
# Copyright (C) 2012, The MacRuby Team
# Copyright (C) 2009-2011, Apple Inc
require 'optparse'
require 'rbconfig'
##
# The MacRuby::Compiler class is a lower level interface to the MacRuby
# compiler logic. It supports the same options that macrubyc does,
# except with slightly different names:
#
# :output:: name of the file to output
# :dylib:: boolean indicating whether to create a dynamic library
# :linkf:: array of options for linking (e.g. '-compatibility_version 0.9')
# :bundle:: boolean indicating the output should be compiled, assembled, and
# linked as a loadable object file
# :dont_link:: boolean indicating if the output file should be linked
# :archs:: array of architectures to compile for; see MacRuby::Compiler::VALID_ARCHS
# :files:: array of files to compile
# :internal:: used internally by the MacRuby build system to set miniruby
# as the compiler
#
# Example usage with <tt>macrubyc</tt>:
# `macrubyc -C -o program.rbo program.rb`
#
# becomes:
# load File.join(RbConfig::CONFIG['bindir'], 'macrubyc')
# MacRuby::Compiler.compile_file('program.rb')
class MacRuby::Compiler
VALID_ARCHS = ['i386', 'x86_64']
# Misc.
TMPDIR = (ENV['TMPDIR'] or '/tmp')
def self.compile_file(file, opts={})
base = File.basename(file, '.rb')
obj = File.join(File.dirname(file), base + '.rbo')
new({
bundle: true,
files: [file],
output: obj
}.merge(opts)).run
end
def locate(progname, must_be_in_bindir=false)
if ['.', '/'].include?(progname[0])
# Already a path.
unless File.exist?(progname)
raise "Can't locate program `#{progname}'"
end
progname
else
path = File.join(RbConfig::CONFIG['bindir'], progname)
unless File.exist?(path)
if must_be_in_bindir
raise "Can't locate program `#{progname}' in #{RbConfig::CONFIG['bindir']}"
end
path = `which #{progname}`.strip
raise "Can't locate program `#{progname}'" if path.empty?
end
path
end
end
def initialize(opts)
@frameworks = ['Foundation']
@archs = (opts[:archs] || []).uniq
@files = opts[:files] || []
@dont_link = opts[:dont_link]
@output = opts[:output]
@dylib = opts[:dylib]
@linkf = opts[:linkf]
@bundle = opts[:bundle]
@internal = opts[:internal]
@verbose = opts[:verbose]
@archs << RUBY_ARCH if @archs.empty?
@archs.each do |arch|
if not VALID_ARCHS.include?(arch)
raise ArgumentError, "Invalid CPU architecture `#{arch}'. Possible values are: " + VALID_ARCHS.join(", ")
end
end
@tmpfiles = []
# Locate necessary programs.
@gcc = locate('gcc')
@gcxx = locate('g++')
@nm = locate('nm')
@lipo = locate('lipo')
@strip = locate('strip')
if @internal
@macruby = locate('./miniruby')
@llc = locate(File.join(RbConfig::CONFIG['LLVM_PATH'], 'bin/llc'))
else
@macruby = locate('macruby')
@llc = locate('llc', true)
end
@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 @dont_link or @bundle
raise ArgumentError, "Cannot specify -c or -C when building a dynamic library" if @dylib
raise ArgumentError, "Cannot specify -c and -C at the same time" if @bundle and @dont_link
if @files.size > 1 and @output
raise ArgumentError, "Cannot specify -o with -c or -C and multiple input files"
end
@files.each do |file|
if File.extname(file) != '.rb'
raise ArgumentError, "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
objs = @files.map do |file|
raise ArgumentError, "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
raise ArgumentError, "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
raise ArgumentError, "-o must be specified when building a dynamic library" unless @output
compile_dylib(objs, @output)
else
compile_executable(objs, @output)
end
end
ensure
cleanup
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')
# @todo replace UUID generation with CFUUIDCreateString(nil, CFUUIDCreate(nil)) ?
# 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} #{@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?
raise "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 = ""
linkf << "-lobjc -licucore -lauto "
@frameworks.each { |f| linkf << "-framework #{f} " }
linkf << "#{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?
raise die_str
end
ret
end
def strip(bin)
execute("#{@strip} -x \"#{bin}\"")
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
end
### Execute only if not being required
if __FILE__ == $0
NAME = File.basename(__FILE__)
def die(*args)
$stderr.puts args
exit 1
end
options = { linkf: [], archs: [] }
options[:internal] = ARGV.delete('--internal')
OptionParser.new do |opts|
opts.banner = "Usage: #{NAME} [options] file..."
opts.on('-c', 'Compile and assemble, but do not link') { options[:dont_link] = true }
opts.on('-o <file>', 'Place the output into <file>') { |output| options[:output] = output }
opts.on('--dylib', 'Create a dynamic library') { options[:dylib] = true }
opts.on('--compatibility_version <VERSION>', 'Compatibility Version for linking') { |ver| options[:linkf] << "-compatibility_version #{ver}" }
opts.on('--current_version <VERSION>', 'Current Version for linking') { |ver| options[:linkf] << "-current_version #{ver}" }
opts.on('--install_name <NAME>', 'Install Name for linking') { |name| options[:linkf] << "-install_name #{name}" }
opts.on('-C', 'Compile, assemble and link a loadable object file') { options[:bundle] = true }
opts.on('-a', '--arch <ARCH>', 'Compile for specified CPU architecture') { |arch| options[:archs] << arch }
opts.on('-V', '--verbose', 'Print every command line executed') { options[:verbose] = true }
opts.on('-v', '--version', 'Display the version') {
puts RUBY_DESCRIPTION
exit 0
}
begin
opts.parse!(ARGV)
rescue OptionParser::InvalidOption => e
die e, opts
end
die opts if ARGV.empty?
options[:files] = ARGV
end
app = MacRuby::Compiler.new(options)
begin
app.run
rescue Exception => e
die e.message
end
end
Jump to Line
Something went wrong with that request. Please try again.