1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
|
require 'rake'
require 'rake/tasklib'
require 'rake/clean'
require 'ffi'
require 'shellwords'
require 'tmpdir'
require 'rbconfig'
require_relative 'platform'
module FFI
module Compiler
DEFAULT_CFLAGS = %w(-fexceptions -O -fno-omit-frame-pointer -fno-strict-aliasing)
DEFAULT_LDFLAGS = %w(-fexceptions)
class Flags
attr_accessor :raw
def initialize(flags)
@flags = flags
@raw = true # For backward compatibility
end
def <<(flag)
if @raw
@flags += flag.to_s.shellsplit
else
@flags << flag
end
end
def to_a
@flags
end
def to_s
@flags.shelljoin
end
end
class CompileTask < Rake::TaskLib
attr_reader :cflags, :cxxflags, :ldflags, :libs, :platform
attr_accessor :name, :ext_dir, :source_dirs, :exclude
def initialize(name)
@name = File.basename(name)
@ext_dir = File.dirname(name)
@source_dirs = [@ext_dir]
@exclude = []
@defines = []
@include_paths = []
@library_paths = []
@libraries = []
@headers = []
@functions = []
@cflags = Flags.new(ENV['CFLAGS']&.shellsplit || DEFAULT_CFLAGS.dup)
@cxxflags = Flags.new(ENV['CXXFLAGS']&.shellsplit || DEFAULT_CFLAGS.dup)
@ldflags = Flags.new(ENV['LDFLAGS']&.shellsplit || DEFAULT_LDFLAGS.dup)
@libs = []
@platform = Platform.system
@exports = []
yield self if block_given?
define_task!
end
def add_include_path(path)
@include_paths << path
end
def add_define(name, value=1)
@defines << "-D#{name}=#{value}"
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, paths: @library_paths) || try_library(libname, paths: paths)
end
def have_library(lib, func = nil, headers = nil, &b)
try_library(lib, function: func, headers: headers, paths: @library_paths)
end
def find_library(lib, func, *paths)
try_library(lib, function: func, paths: @library_paths) || try_library(libname, function: func, paths: paths)
end
def export(rb_file)
@exports << { :rb_file => rb_file, :header => File.join(@ext_dir, File.basename(rb_file).sub(/\.rb$/, '.h')) }
end
private
def define_task!
pic_flags = %w(-fPIC)
so_flags = []
if @platform.mac?
pic_flags = []
so_flags << '-bundle'
elsif @platform.name =~ /linux/
so_flags << "-shared -Wl,-soname,#{lib_name}"
else
so_flags << '-shared'
end
so_flags = so_flags.shelljoin
out_dir = "#{@platform.arch}-#{@platform.os}"
if @ext_dir != '.'
out_dir = File.join(@ext_dir, out_dir)
end
directory(out_dir)
CLOBBER.include(out_dir)
lib_name = File.join(out_dir, Platform.system.map_library_name(@name))
iflags = @include_paths.uniq.map { |p| "-I#{p}" }
@defines += @functions.uniq.map { |f| "-DHAVE_#{f.upcase}=1" }
@defines += @headers.uniq.map { |h| "-DHAVE_#{h.upcase.sub(/\./, '_')}=1" }
cflags = (@cflags.to_a + pic_flags + iflags + @defines).shelljoin
cxxflags = (@cxxflags.to_a + @cflags.to_a + pic_flags + iflags + @defines).shelljoin
ld_flags = (@library_paths.map { |path| "-L#{path}" } + @ldflags.to_a).shelljoin
libs = (@libraries.map { |l| "-l#{l}" } + @libs).shelljoin
src_files = []
obj_files = []
@source_dirs.each do |dir|
files = FileList["#{dir}/**/*.{c,cpp}"]
unless @exclude.empty?
files.delete_if { |f| f =~ Regexp.union(*@exclude) }
end
src_files += files
obj_files += files.ext('.o').map { |f| File.join(out_dir, f.sub(/^#{dir}\//, '')) }
end
index = 0
src_files.each do |src|
obj_file = obj_files[index]
if src =~ /\.c$/
file obj_file => [ src, File.dirname(obj_file) ] do |t|
sh "#{cc} #{cflags} -o #{t.name} -c #{t.prerequisites[0]}"
end
else
file obj_file => [ src, File.dirname(obj_file) ] do |t|
sh "#{cxx} #{cxxflags} -o #{t.name} -c #{t.prerequisites[0]}"
end
end
CLEAN.include(obj_file)
index += 1
end
ld = src_files.detect { |f| f =~ /\.cpp$/ } ? cxx : cc
# create all the directories for the output files
obj_files.map { |f| File.dirname(f) }.sort.uniq.map { |d| directory d }
desc "Build dynamic library"
file lib_name => obj_files do |t|
sh "#{ld} #{so_flags} -o #{t.name} #{t.prerequisites.shelljoin} #{ld_flags} #{libs}"
end
CLEAN.include(lib_name)
@exports.each do |e|
desc "Export #{e[:rb_file]}"
file e[:header] => [ e[:rb_file] ] do |t|
ruby "-I#{File.join(File.dirname(__FILE__), 'fake_ffi')} -I#{File.dirname(t.prerequisites[0])} #{File.join(File.dirname(__FILE__), 'exporter.rb')} #{t.prerequisites[0]} #{t.name}"
end
obj_files.each { |o| file o => [ e[:header] ] }
CLEAN.include(e[:header])
desc "Export API headers"
task :api_headers => [ e[:header] ]
end
task :default => [ lib_name ]
task :package => [ :api_headers ]
end
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, options = {})
func = options[:function] || 'main'
paths = options[:paths] || ''
main = <<-C_FILE
#{(options[:headers] || []).map {|h| "#include <#{h}>"}.join('\n')}
extern int #{func}();
int main() { return #{func}(); }
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
cflags = opts.shelljoin
output = File.join(dir, 'ffi-test')
begin
return system "#{cc} #{cflags} -o #{output} -c #{path} > #{path}.log 2>&1"
rescue
return false
end
end
end
def cc
@cc ||= (ENV['CC'] || RbConfig::CONFIG['CC'] || 'cc')
end
def cxx
@cxx ||= (ENV['CXX'] || RbConfig::CONFIG['CXX'] || 'c++')
end
end
end
end
|