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 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
|
require 'rbconfig'
require 'logger'
module Mkrf
# The +Availability+ class is concerned with libraries, headers, and
# functions. It can be easily wrapped (see <tt>Mkrf::Generator</tt> for an
# example) and should be able to be used as a basis for a variety of programs
# which need to determine functionality based on what libraries are available
# on the current system.
class Availability
# ruby 2.0+
if RbConfig::CONFIG['rubyarchhdrdir']
DEFAULT_INCLUDES = [RbConfig::CONFIG['rubyarchhdrdir'],RbConfig::CONFIG['rubyhdrdir'],
RbConfig::CONFIG['rubyhdrdir'] + "/" + RbConfig::CONFIG['arch'],
RbConfig::CONFIG["archdir"],RbConfig::CONFIG['sitelibdir'], "."]
else
DEFAULT_INCLUDES = [RbConfig::CONFIG['rubyhdrdir'],
RbConfig::CONFIG['rubyhdrdir'] + "/" + RbConfig::CONFIG['arch'],
RbConfig::CONFIG["archdir"],RbConfig::CONFIG['sitelibdir'], "."]
end
# These really shouldn't be static like this..
TEMP_SOURCE_FILE = "temp_source.c"
TEMP_EXECUTABLE = "temp_executable"
attr_reader :headers, :loaded_libs, :includes, :logger, :defines
# Create a new Availability instance.
#
# Valid keys for the options hash include:
# * <tt>:loaded_libs</tt> -- libraries to load by default
# * <tt>:library_paths</tt> -- libraries paths to include by default
# * <tt>:headers</tt> -- headers to load by default
# * <tt>:compiler</tt> -- which compiler to use when determining availability
# * <tt>:includes</tt> -- directories that should be searched for include files
def initialize(options = {})
@loaded_libs = [(options[:loaded_libs] || RbConfig::CONFIG["LIBS"].gsub('-l', '').split)].flatten
@library_paths = [(options[:library_paths] || [])].flatten
# Not sure what COMMON_HEADERS looks like when populated
@headers = options[:headers] || [] # RbConfig::CONFIG["COMMON_HEADERS"]
@compiler = options[:compiler] || RbConfig::CONFIG["CC"]
@includes = [(options[:includes] || DEFAULT_INCLUDES)].flatten
@logger = Logger.new('mkrf.log')
@defines = []
end
# Include a library in the list of available libs. Returns +false+ if the
# library is not available. Returns non-false otherwise.
#
# Params:
# * <tt>library</tt> -- the library to be included as a string.
# * <tt>function</tt> -- a method to base the inclusion of the library on. +main+ by default.
# * <tt>paths</tt> -- an optional list of search paths if the library is not found in the default paths.
def include_library(library, function = "main", *paths)
paths.each do |library_dir|
@library_paths << library_dir
end
@loaded_libs << library if has_library?(library, function)
end
# Include a header in the list of availiable headers. Returns +false+ if the
# header is not available. Returns non-false otherwise. If the header is
# found, the preprocessor constant HAVE_BLAH is defined where BLAH is the name
# of the header in uppercase without the file extension.
#
# Params:
# * <tt>header</tt> -- the name of the header to be included as a string.
# * <tt>paths</tt> -- an optional list of search paths if the header is not found in the default paths.
def include_header(header, *paths)
@headers << header if has_header?(header, *paths)
end
# Returns a boolean whether indicating whether the library can be found
# by attempting to reference the function passed (+main+ by default).
#
# Params:
# * <tt>library</tt> -- the library to be included as a string
# * <tt>function</tt> -- a method to base the inclusion of the library on. +main+ by default.
# * <tt>paths</tt> -- an optional list of search paths if the library is not found in the default paths
def has_library?(library, function = "main", *paths)
logger.info "Checking for library: #{library}"
return true if library_already_loaded?(library)
return true if RUBY_PLATFORM =~ /mswin/ # TODO: find a way on windows
# Should this be only found_library? or a specialized version with
# path searching?
found_library?(library, function)
end
# Returns +true+ if the header is found in the default search path or in
# optional paths passed as an argument, +false+ otherwise. If the header is
# found, the preprocessor constant HAVE_BLAH is defined where BLAH is the name
# of the header in uppercase without the file extension.
#
# Params:
# * <tt>header</tt> -- the header to be searched for
# * <tt>paths</tt> -- an optional list of search paths if the header is not found in the default paths
def has_header?(header, *paths)
if header_already_loaded?(header) || header_can_link?(header) ||
header_found_in_paths?(header, paths)
defines << format("HAVE_%s", header.tr("a-z./\055", "A-Z___"))
return true
end
logger.warn "Header not found: #{header}"
return false
end
# Returns +true+ if the function is able to be called based on libraries and
# headers currently loaded. Returns +false+ otherwise.
#
# Params:
# * <tt>function</tt> -- the function to check for
def has_function?(function)
if can_link?(simple_call(function)) or can_link?(simple_reference(function))
logger.info "Function found: #{function}()"
return true
else
logger.warn "Function not found: #{function}()"
return false
end
end
# Returns the result of an attempt to compile and link the function body
# passed in
def can_link?(function_body)
silence_command_line do
create_source(function_body)
system(link_command)
end
ensure
FileUtils.rm_f TEMP_SOURCE_FILE
FileUtils.rm_f TEMP_EXECUTABLE
end
def with_headers(*args, &b)
with_stackable_attribute('headers', *args, &b)
end
def with_loaded_libs(*args, &b)
with_stackable_attribute('loaded_libs', *args, &b)
end
def with_includes(*args, &b)
with_stackable_attribute('includes', *args, &b)
end
# Returns a string of libraries formatted for compilation
def library_compile_string
if RUBY_PLATFORM =~ /mswin/
@loaded_libs.join(' ')
else
@loaded_libs.collect {|l| "-l#{l}"}.join(' ')
end
end
# Returns a string of libraries directories formatted for compilation
def library_paths_compile_string
if RUBY_PLATFORM =~ /mswin/
@library_paths.collect {|l| "/libpath:#{l}"}.join(' ')
else
@library_paths.collect {|l| "-L#{l}"}.join(' ')
end
end
def ldshared_string
if RUBY_PLATFORM =~ /mswin/
"link -nologo -incremental:no -debug -opt:ref -opt:icf -dll"
else
RbConfig::CONFIG['LDSHARED']
end
end
def ld_outfile(filename) # :nodoc:
if RUBY_PLATFORM =~ /mswin/
"-out:#{filename}"
else
"-o #{filename}"
end
end
# Returns a string of include directories formatted for compilation
def includes_compile_string
@includes.collect {|i| "-I#{i}"}.join(' ')
end
# Takes the name of an executable and an optional set of paths to search.
# If no paths are given, the environmental path is used by default.
# Returns the absolute path to an executable, or nil if not found.
def find_executable(bin, *paths)
paths = ENV['PATH'].split(File::PATH_SEPARATOR) if paths.empty?
paths.each do |path|
file = File.join(path, bin)
return file if File.executable?(file)
end
return nil
end
private
def found_library?(library, function)
library_found = with_loaded_libs(library) {
has_function? function
}
library_found ? logger.info("Library found: #{library}") :
logger.warn("Library not found: #{library}")
library_found
end
def header_can_link?(header)
has_header = with_headers(header) {
can_link?(simple_include(header))
}
if has_header
logger.info("Header found: #{header}")
return true
end
end
def library_already_loaded?(library)
if @loaded_libs.include? library
logger.info "Library already loaded: #{library}"
return true
end
return false
end
def header_already_loaded?(header)
if @headers.include? header
logger.info("Header already loaded: #{header}")
return true
end
return false
end
# def library_found_in_paths?(library, paths)
# paths.each do |include_path|
#
# if with_libs(include_path) { library_can_link?(header) }
# @libspath << include_path
# return true
# end
# end
#
# return false
#
# end
def header_found_in_paths?(header, paths)
paths.each do |include_path|
if with_includes(include_path) { header_can_link?(header) }
@includes << include_path
return true
end
end
return false
end
def with_stackable_attribute(attribute, *args)
args = args.to_a
instance_variable_set("@#{attribute}",
instance_variable_get("@#{attribute}") + args)
value = yield
instance_variable_set("@#{attribute}",
instance_variable_get("@#{attribute}") - args)
return value
end
def header_include_string
@headers.collect {|header| "#include <#{header}>"}.join('\n')
end
def link_command
# This current implementation just splats the library_paths in
# unconditionally. Is this problematic?
"#{@compiler} -o #{TEMP_EXECUTABLE}" +
" #{includes_compile_string} #{TEMP_SOURCE_FILE}" +
" #{library_paths_compile_string} #{library_compile_string}"
end
# Creates a temporary source file with the string passed
def create_source(src)
File.open(TEMP_SOURCE_FILE, "w+") do |f|
f.write(src)
end
end
# Basic skeleton for calling a function
def simple_call(func)
src = <<-SRC
#{header_include_string}
int main() { return 0; }
int t() { #{func}(); return 0; }
SRC
end
# Basic skeleton for referencing a function
def simple_reference(func)
src = <<-SRC
#{header_include_string}
int main() { return 0; }
int t() { void ((*volatile p)()); p = (void ((*)()))#{func}; return 0; }
SRC
end
# skeleton for testing includes
def simple_include(header)
src = <<-SRC
#{header_include_string}
#include <#{header}>
int main() { return 0; }
SRC
end
def silence_command_line
yield and return if $debug
silence_stream(STDERR) do
silence_stream(STDOUT) do
yield
end
end
end
# silence_stream taken from Rails ActiveSupport reporting.rb
# Silences any stream for the duration of the block.
#
# silence_stream(STDOUT) do
# puts 'This will never be seen'
# end
#
# puts 'But this will'
def silence_stream(stream)
old_stream = stream.dup
stream.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null')
stream.sync = true
yield
ensure
stream.reopen(old_stream)
end
end
end
|