require 'ffi' require 'tmpdir' module FFI class Compiler def initialize(name, &block) @name = name @defines = [] @include_paths = [] @library_paths = [] @libraries = [] @headers = [] @functions = [] if block_given? yield self create_rakefile! end end def have_func?(func) main = <<-C_FILE extern void #{func}(); int main(int argc, char **argv) { #{func}(); return 0; } C_FILE if try_compile(main) @functions << func return true end false end def have_header?(header, *paths) try_header(header, @include_paths) || try_header(header, paths) end def have_library?(libname, *paths) try_library(libname, @library_paths) || try_library(libname, paths) end def create_rakefile! create_rakefile(@name) end private def try_header(header, paths) main = <<-C_FILE #include <#{header}> int main(int argc, char **argv) { return 0; } C_FILE if paths.empty? && try_compile(main) @headers << header return true end paths.each do |path| if try_compile(main, "-I#{path}") @include_paths << path @headers << header return true end end false end def try_library(libname, paths) main = <<-C_FILE int main(int argc, char **argv) { return 0; } C_FILE if paths.empty? && try_compile(main) @libraries << libname return true end paths.each do |path| if try_compile(main, "-L#{path}", "-l#{libname}") @library_paths << path @libraries << libname end end end def try_compile(src, *opts) Dir.mktmpdir do |dir| path = File.join(dir, 'ffi-test.c') File.open(path, "w") do |f| f << src end begin return system "cc #{opts.join(' ')} -o #{File.join(dir, 'ffi-test')} #{path} >& /dev/null" rescue return false end end end def create_rakefile(name) lib_name = FFI.map_library_name(name) pic_flags = '-fPIC' so_flags = '' ld_flags = '' cc = 'cc' cxx = 'c++' iflags = @include_paths.uniq.map{ |p| "-I#{p}" }.join(' ') defines = @functions.uniq.map { |f| "-DHAVE_#{f.upcase}=1" }.join(' ') defines << " " + @headers.uniq.map { |h| "-DHAVE_#{h.upcase.sub(/\./, '_')}=1" }.join(' ') if FFI::Platform.mac? pic_flags = '' ld_flags += ' -dynamiclib ' elsif FFI::Platform.name =~ /linux/ so_flags += " -shared -Wl,-soname,#{lib_name} " end cflags = "#{pic_flags} #{iflags} #{defines}".strip ld_flags += so_flags ld_flags += @library_paths.map { |path| "-L#{path}" }.join(' ') ld_flags.strip! File.open('Rakefile', 'w') do |f| f.puts <<-RAKEFILE require 'rake/clean' CC = '#{cc}' CXX = '#{cxx}' LD = FileList['*.cpp'].empty? ? CC : CXX CFLAGS = '#{cflags}' CXXFLAGS = '#{cflags}' LDFLAGS = '#{ld_flags}' LIBS = '#{@libraries.map {|l| "-l#{l}" }.join(" ")}' OBJ_FILES = FileList['*.c', '*.cpp'].map { |f| f.gsub(/\.(c|cpp)$/, '.o') } CLEAN.include(OBJ_FILES) desc "Compile C file to object file" rule '.o' => [ '.c' ] do |t| sh "\#{CC} \#{CFLAGS} -o \#{t.name} -c \#{t.source}" end desc "Compile C++ file to object file" rule '.o' => [ '.cpp' ] do |t| sh "\#{CXX} \#{CXXFLAGS} -o \#{t.name} -c \#{t.source} " end desc "Compile to dynamic library" task :compile => "#{lib_name}" file "#{lib_name}" => OBJ_FILES do |t| sh "\#{LD} \#{LDFLAGS} -o \#{t.name} \#{t.prerequisites.join(' ')} \#{LIBS}" end task :default => :compile RAKEFILE end end end end