require 'pycall/error' require 'fiddle' module PyCall module LibPython module Finder case RUBY_PLATFORM when /cygwin/ libprefix = 'cyg' libsuffix = 'dll' when /mingw/, /mswin/ libprefix = '' libsuffix = 'dll' when /darwin/ libsuffix = 'dylib' end LIBPREFIX = libprefix || 'lib' LIBSUFFIX = libsuffix || 'so' class << self DEFAULT_PYTHON = [ -'python3', -'python', ].freeze def find_python_config(python = nil) python ||= DEFAULT_PYTHON Array(python).each do |python_cmd| begin python_config = investigate_python_config(python_cmd) return [python_cmd, python_config] unless python_config.empty? rescue end end raise ::PyCall::PythonNotFound end def find_libpython(python = nil) debug_report("find_libpython(#{python.inspect})") python, python_config = find_python_config(python) set_PYTHONHOME(python_config) libs = make_libs(python_config) libpaths = make_libpaths(python_config) # Try LIBPYTHON environment variable first. if (libpython = ENV['LIBPYTHON']) if File.file?(libpython) begin return dlopen(libpython) rescue Fiddle::DLError debug_report "#{$!.class}: #{$!.message}" else debug_report "Success to dlopen #{libpython.inspect} from ENV['LIBPYTHON']" end end warn "WARNING(#{self}.#{__method__}) Ignore the wrong libpython location specified in ENV['LIBPYTHON']." end # Find libpython (we hope): multiarch = python_config[:MULTIARCH] || python_config[:multiarch] libs.each do |lib| libpaths.each do |libpath| libpath_libs = [ File.join(libpath, lib) ] libpath_libs << File.join(libpath, multiarch, lib) if multiarch libpath_libs.each do |libpath_lib| [ libpath_lib, "#{libpath_lib}.#{LIBSUFFIX}" ].each do |fullname| unless File.file? fullname debug_report "Unable to find #{fullname}" next end begin return dlopen(libpath_lib) rescue Fiddle::DLError debug_report "#{$!.class}: #{$!.message}" else debug_report "Success to dlopen #{libpaht_lib}" end end end end end # Find libpython in the system path libs.each do |lib| begin return dlopen(lib) rescue Fiddle::DLError debug_report "#{$!.class}: #{$!.message}" else debug_report "Success to dlopen #{lib}" end end raise ::PyCall::PythonNotFound end def investigate_python_config(python) python_env = { 'PYTHONIOENCODING' => 'UTF-8' } debug_report("investigate_python_config(#{python.inspect})") IO.popen(python_env, [python, python_investigator_py], 'r') do |io| {}.tap do |config| io.each_line do |line| next unless line =~ /: / key, value = line.chomp.split(': ', 2) case value when 'True', 'true', 'False', 'false' value = (value == 'True' || value == 'true') end config[key.to_sym] = value if value != 'None' end end end rescue Errno::ENOENT raise PyCall::PythonInvestigationFailed end def python_investigator_py File.expand_path('../../python/investigator.py', __FILE__) end def set_PYTHONHOME(python_config) if !ENV.has_key?('PYTHONHOME') && python_config[:conda] case RUBY_PLATFORM when /mingw32/, /cygwin/, /mswin/ ENV['PYTHONHOME'] = python_config[:exec_prefix] else ENV['PYTHONHOME'] = python_config.values_at(:prefix, :exec_prefix).join(':') end end end def make_libs(python_config) libs = [] %i(INSTSONAME LDLIBRARY).each do |key| lib = python_config[key] libs << lib << File.basename(lib) if lib end if (lib = python_config[:LIBRARY]) libs << File.basename(lib, File.extname(lib)) end v = python_config[:VERSION] libs << "#{LIBPREFIX}python#{v}" << "#{LIBPREFIX}python" libs.uniq! debug_report "libs: #{libs.inspect}" return libs end def make_libpaths(python_config) executable = python_config[:executable] libpaths = [ python_config[:LIBDIR] ] if Fiddle::WINDOWS libpaths << File.dirname(executable) else libpaths << File.expand_path('../../lib', executable) end libpaths << python_config[:PYTHONFRAMEWORKPREFIX] exec_prefix = python_config[:exec_prefix] libpaths << exec_prefix << File.join(exec_prefix, 'lib') libpaths.compact! debug_report "libpaths: #{libpaths.inspect}" return libpaths end private def dlopen(libname) Fiddle.dlopen(libname).tap do |handle| debug_report("dlopen(#{libname.inspect}) = #{handle.inspect}") if handle end end def debug_report(message) return unless debug? $stderr.puts "DEBUG(find_libpython) #{message}" end def debug? @debug ||= (ENV['PYCALL_DEBUG_FIND_LIBPYTHON'] == '1') end end end end end