require 'pycall/error' require 'fiddle' require 'pathname' 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) suffix = python_config[:SHLIB_SUFFIX] use_conda = (ENV.fetch("CONDA_PREFIX", nil) == File.dirname(python_config[:executable])) python_home = if !ENV.key?("PYTHONHOME") || use_conda python_config[:PYTHONHOME] else ENV["PYTHONHOME"] end ENV["PYTHONHOME"] = python_home candidate_paths(python_config) do |path| debug_report("Candidate: #{path}") normalized = normalize_path(path, suffix) if normalized debug_report("Trying to dlopen: #{normalized}") begin return dlopen(normalized) rescue Fiddle::DLError debug_report "dlopen(#{normalized.inspect}) => #{$!.class}: #{$!.message}" end else debug_report("Not found.") end end end def candidate_names(python_config) names = [] names << python_config[:LDLIBRARY] if python_config[:LDLIBRARY] suffix = python_config[:SHLIB_SUFFIX] if python_config[:LIBRARY] ext = File.extname(python_config[:LIBRARY]) names << python_config[:LIBRARY].delete_suffix(ext) + suffix end dlprefix = if windows? then "" else "lib" end sysdata = { v_major: python_config[:version_major], VERSION: python_config[:VERSION], ABIFLAGS: python_config[:ABIFLAGS], } [ "python%{VERSION}%{ABIFLAGS}" % sysdata, "python%{VERSION}" % sysdata, "python%{v_major}" % sysdata, "python" ].each do |stem| names << "#{dlprefix}#{stem}#{suffix}" end names.compact! names.uniq! debug_report("candidate_names: #{names}") return names end def candidate_paths(python_config) # The candidate library that linked by executable yield python_config[:linked_libpython] lib_dirs = make_libpaths(python_config) lib_basenames = candidate_names(python_config) # candidates by absolute paths lib_dirs.each do |dir| lib_basenames.each do |name| yield File.join(dir, name) end end # library names for searching in system library paths lib_basenames.each do |name| yield name end end def normalize_path(path, suffix, apple_p=apple?) return nil if path.nil? case when path.nil?, Pathname.new(path).relative? nil when File.exist?(path) File.realpath(path) when File.exist?(path + suffix) File.realpath(path + suffix) when apple_p normalize_path(remove_suffix_apple(path), ".so", false) else nil end end # Strip off .so or .dylib def remove_suffix_apple(path) path.sub(/\.(?:dylib|so)\z/, '') 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 make_libpaths(python_config) libpaths = python_config.values_at(:LIBPL, :srcdir, :LIBDIR) if windows? libpaths << File.dirname(python_config[:executable]) else libpaths << File.expand_path('../../lib', python_config[:executable]) end if apple? libpaths << python_config[:PYTHONFRAMEWORKPREFIX] end exec_prefix = python_config[:exec_prefix] libpaths << exec_prefix libpaths << File.join(exec_prefix, 'lib') libpaths.compact! libpaths.uniq! debug_report "libpaths: #{libpaths.inspect}" return libpaths end private def windows? Fiddle::WINDOWS end def apple? RUBY_PLATFORM.include?("darwin") end 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 if __FILE__ == $0 require "pp" python, python_config = PyCall::LibPython::Finder.find_python_config puts "python_config:" pp python_config puts "\ncandidate_names:" p PyCall::LibPython::Finder.candidate_names(python_config) puts "\nlib_dirs:" p PyCall::LibPython::Finder.make_libpaths(python_config) puts "\ncandidate_paths:" PyCall::LibPython::Finder.candidate_paths(python_config) do |path| puts "- #{path}" end end