Skip to content

Commit c3c5038

Browse files
committed
Update libpython finder
Tune the finder by referring the update of PyCall.jl.
1 parent f2d375c commit c3c5038

File tree

2 files changed

+189
-86
lines changed

2 files changed

+189
-86
lines changed

lib/pycall/libpython/finder.rb

Lines changed: 117 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'pycall/error'
22
require 'fiddle'
3+
require 'pathname'
34

45
module PyCall
56
module LibPython
@@ -39,60 +40,94 @@ def find_python_config(python = nil)
3940
def find_libpython(python = nil)
4041
debug_report("find_libpython(#{python.inspect})")
4142
python, python_config = find_python_config(python)
43+
suffix = python_config[:SHLIB_SUFFIX]
4244

43-
# Try LIBPYTHON environment variable first.
44-
if (libpython = ENV['LIBPYTHON'])
45-
if File.file?(libpython)
45+
candidate_paths(python_config) do |path|
46+
debug_report("Candidate: #{path}")
47+
normalized = normalize_path(path, suffix)
48+
if normalized
49+
debug_report("Trying to dlopen: #{normalized}")
4650
begin
47-
return dlopen(libpython)
51+
return dlopen(normalized)
4852
rescue Fiddle::DLError
49-
debug_report "#{$!.class}: #{$!.message}"
50-
else
51-
debug_report "Success to dlopen #{libpython.inspect} from ENV['LIBPYTHON']"
53+
debug_report "dlopen(#{normalized.inspect}) => #{$!.class}: #{$!.message}"
5254
end
55+
else
56+
debug_report("Not found.")
5357
end
54-
warn "WARNING(#{self}.#{__method__}) Ignore the wrong libpython location specified in ENV['LIBPYTHON']."
5558
end
59+
end
5660

57-
# Find libpython (we hope):
58-
set_PYTHONHOME(python_config)
59-
libs = make_libs(python_config)
60-
libpaths = make_libpaths(python_config)
61-
multiarch = python_config[:MULTIARCH] || python_config[:multiarch]
62-
libs.each do |lib|
63-
libpaths.each do |libpath|
64-
libpath_libs = [ File.join(libpath, lib) ]
65-
libpath_libs << File.join(libpath, multiarch, lib) if multiarch
66-
libpath_libs.each do |libpath_lib|
67-
[ libpath_lib, "#{libpath_lib}.#{LIBSUFFIX}" ].each do |fullname|
68-
unless File.file? fullname
69-
debug_report "Unable to find #{fullname}"
70-
next
71-
end
72-
begin
73-
return dlopen(libpath_lib)
74-
rescue Fiddle::DLError
75-
debug_report "#{$!.class}: #{$!.message}"
76-
else
77-
debug_report "Success to dlopen #{libpaht_lib}"
78-
end
79-
end
80-
end
81-
end
61+
def candidate_names(python_config)
62+
names = []
63+
names << python_config[:LDLIBRARY] if python_config[:LDLIBRARY]
64+
suffix = python_config[:SHLIB_SUFFIX]
65+
if python_config[:LIBRARY]
66+
ext = File.extname(python_config[:LIBRARY])
67+
names << python_config[:LIBRARY].delete_suffix(ext) + suffix
68+
end
69+
dlprefix = if windows? then "" else "lib" end
70+
sysdata = {
71+
v_major: python_config[:version_major],
72+
VERSION: python_config[:VERSION],
73+
ABIFLAGS: python_config[:ABIFLAGS],
74+
}
75+
[
76+
"python%{VERSION}%{ABIFLAGS}" % sysdata,
77+
"python%{VERSION}" % sysdata,
78+
"python%{v_major}" % sysdata,
79+
"python"
80+
].each do |stem|
81+
names << "#{dlprefix}#{stem}#{suffix}"
8282
end
8383

84-
# Find libpython in the system path
85-
libs.each do |lib|
86-
begin
87-
return dlopen(lib)
88-
rescue Fiddle::DLError
89-
debug_report "#{$!.class}: #{$!.message}"
90-
else
91-
debug_report "Success to dlopen #{lib}"
84+
names.compact!
85+
names.uniq!
86+
87+
debug_report("candidate_names: #{names}")
88+
return names
89+
end
90+
91+
def candidate_paths(python_config)
92+
# The candidate library that linked by executable
93+
yield python_config[:linked_libpython]
94+
95+
lib_dirs = make_libpaths(python_config)
96+
lib_basenames = candidate_names(python_config)
97+
98+
# candidates by absolute paths
99+
lib_dirs.each do |dir|
100+
lib_basenames.each do |name|
101+
yield File.join(dir, name)
92102
end
93103
end
94104

95-
raise ::PyCall::PythonNotFound
105+
# library names for searching in system library paths
106+
lib_basenames.each do |name|
107+
yield name
108+
end
109+
end
110+
111+
def normalize_path(path, suffix, apple_p=apple?)
112+
return nil if path.nil?
113+
case
114+
when path.nil?,
115+
Pathname.new(path).relative?
116+
nil
117+
when File.exist?(path)
118+
File.realpath(path)
119+
when File.exist?(path + suffix)
120+
File.realpath(path + suffix)
121+
when apple_p
122+
normalize_path(remove_suffix_apple(path), ".so", false)
123+
else
124+
nil
125+
end
126+
end
127+
128+
# Strip off .so or .dylib
129+
def remove_suffix_apple(path)
130+
path.sub(/\.(?:dylib|so)\z/, '')
96131
end
97132

98133
def investigate_python_config(python)
@@ -119,54 +154,40 @@ def python_investigator_py
119154
File.expand_path('../../python/investigator.py', __FILE__)
120155
end
121156

122-
def set_PYTHONHOME(python_config)
123-
if !ENV.has_key?('PYTHONHOME') && python_config[:conda]
124-
case RUBY_PLATFORM
125-
when /mingw32/, /cygwin/, /mswin/
126-
ENV['PYTHONHOME'] = python_config[:exec_prefix]
127-
else
128-
ENV['PYTHONHOME'] = python_config.values_at(:prefix, :exec_prefix).join(':')
129-
end
130-
end
131-
end
157+
def make_libpaths(python_config)
158+
libpaths = python_config.values_at(:LIBPL, :srcdir, :LIBDIR)
132159

133-
def make_libs(python_config)
134-
libs = []
135-
%i(INSTSONAME LDLIBRARY).each do |key|
136-
lib = python_config[key]
137-
libs << lib << File.basename(lib) if lib
138-
end
139-
if (lib = python_config[:LIBRARY])
140-
libs << File.basename(lib, File.extname(lib))
160+
if windows?
161+
libpaths << File.dirname(python_config[:executable])
162+
else
163+
libpaths << File.expand_path('../../lib', python_config[:executable])
141164
end
142165

143-
v = python_config[:VERSION]
144-
libs << "#{LIBPREFIX}python#{v}" << "#{LIBPREFIX}python"
145-
libs.uniq!
146-
147-
debug_report "libs: #{libs.inspect}"
148-
return libs
149-
end
150-
151-
def make_libpaths(python_config)
152-
executable = python_config[:executable]
153-
libpaths = [ python_config[:LIBDIR] ]
154-
if Fiddle::WINDOWS
155-
libpaths << File.dirname(executable)
156-
else
157-
libpaths << File.expand_path('../../lib', executable)
166+
if apple?
167+
libpaths << python_config[:PYTHONFRAMEWORKPREFIX]
158168
end
159-
libpaths << python_config[:PYTHONFRAMEWORKPREFIX]
169+
160170
exec_prefix = python_config[:exec_prefix]
161-
libpaths << exec_prefix << File.join(exec_prefix, 'lib')
171+
libpaths << exec_prefix
172+
libpaths << File.join(exec_prefix, 'lib')
173+
162174
libpaths.compact!
175+
libpaths.uniq!
163176

164177
debug_report "libpaths: #{libpaths.inspect}"
165178
return libpaths
166179
end
167180

168181
private
169182

183+
def windows?
184+
Fiddle::WINDOWS
185+
end
186+
187+
def apple?
188+
RUBY_PLATFORM.include?("darwin")
189+
end
190+
170191
def dlopen(libname)
171192
Fiddle.dlopen(libname).tap do |handle|
172193
debug_report("dlopen(#{libname.inspect}) = #{handle.inspect}") if handle
@@ -185,3 +206,22 @@ def debug?
185206
end
186207
end
187208
end
209+
210+
if __FILE__ == $0
211+
require "pp"
212+
python, python_config = PyCall::LibPython::Finder.find_python_config
213+
214+
puts "python_config:"
215+
pp python_config
216+
217+
puts "\ncandidate_names:"
218+
p PyCall::LibPython::Finder.candidate_names(python_config)
219+
220+
puts "\nlib_dirs:"
221+
p PyCall::LibPython::Finder.make_libpaths(python_config)
222+
223+
puts "\ncandidate_paths:"
224+
PyCall::LibPython::Finder.candidate_paths(python_config) do |path|
225+
puts "- #{path}"
226+
end
227+
end

lib/pycall/python/investigator.py

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,75 @@
1-
from distutils.sysconfig import get_config_var
1+
#!/usr/bin/env python
2+
3+
import ctypes.util
4+
from distutils.sysconfig import get_config_var, get_python_version
5+
import os
26
import sys
37

4-
def conda():
5-
return 'conda' in sys.version or 'Continuum' in sys.version
8+
is_windows = os.name == "nt"
9+
10+
def linked_libpython():
11+
if is_windows:
12+
return _linked_libpython_windows()
13+
return _linked_libpython_unix()
14+
15+
class Dl_info(ctypes.Structure):
16+
_fields_ = [
17+
("dli_fname", ctypes.c_char_p),
18+
("dli_fbase", ctypes.c_void_p),
19+
("dli_sname", ctypes.c_char_p),
20+
("dli_saddr", ctypes.c_void_p),
21+
]
22+
23+
def _linked_libpython_unix():
24+
libdl = ctypes.CDLL(ctypes.util.find_library("dl"))
25+
libdl.dladdr.argtypes = [ctypes.c_void_p, ctypes.POINTER(Dl_info)]
26+
libdl.dladdr.restype = ctypes.c_int
27+
28+
dlinfo = Dl_info()
29+
retcode = libdl.dladdr(
30+
ctypes.cast(ctypes.pythonapi.Py_GetVersion, ctypes.c_void_p),
31+
ctypes.pointer(dlinfo))
32+
if retcode == 0: # means error
33+
return None
34+
path = os.path.realpath(dlinfo.dli_fname.decode())
35+
if path == os.path.realpath(sys.executable):
36+
return None
37+
return path
38+
39+
def _linked_libpython_windows():
40+
# Based on: https://stackoverflow.com/a/16659821
41+
from ctypes.wintypes import HANDLE, LPWSTR, DWORD
42+
43+
GetModuleFileName = ctypes.windll.kernel32.GetModuleFileNameW
44+
GetModuleFileName.argtypes = [HANDLE, LPWSTR, DWORD]
45+
GetModuleFileName.restype = DWORD
46+
47+
MAX_PATH = 260
48+
try:
49+
buf = ctypes.create_unicode_buffer(MAX_PATH)
50+
GetModuleFileName(ctypes.pythonapi._handle, buf, MAX_PATH)
51+
return buf.value
52+
except (ValueError, OSError):
53+
return None
54+
55+
print("linked_libpython: {val}".format(val=(linked_libpython() or "None")))
56+
57+
sys_keys = [ "executable", "exec_prefix", "prefix" ]
58+
59+
for var in sys_keys:
60+
print("{var}: {val}".format(var=var, val=(getattr(sys, var) or "None")))
61+
62+
config_keys = [ "INSTSONAME", "LIBDIR", "LIBPL", "LIBRARY", "LDLIBRARY",
63+
"MULTIARCH", "PYTHONFRAMEWORKPREFIX", "SHLIB_SUFFIX", "srcdir" ]
64+
65+
for var in config_keys:
66+
print("{var}: {val}".format(var=var, val=(get_config_var(var) or "None")))
67+
68+
print("ABIFLAGS: {val}".format(val=get_config_var("ABIFLAGS") or get_config_var("abiflags") or "None"))
69+
70+
version = get_python_version() or \
71+
"{v.major}.{v.minor}".format(v=sys.version_info) or \
72+
get_config_var("VERSION")
73+
print("VERSION: {val}".format(val=version))
674

7-
for var in ('executable', 'exec_prefix', 'prefix'):
8-
print(var + ': ' + str(getattr(sys, var)))
9-
print('conda: ' + ('true' if conda() else 'false'))
10-
print('multiarch: ' + str(getattr(getattr(sys, 'implementation', sys), '_multiarch', None)))
11-
for var in ('VERSION', 'INSTSONAME', 'LIBRARY', 'LDLIBRARY', 'LIBDIR', 'PYTHONFRAMEWORKPREFIX', 'MULTIARCH'):
12-
print(var + ': ' + str(get_config_var(var)))
75+
print("version_major: {val}".format(val=sys.version_info.major))

0 commit comments

Comments
 (0)