Lorenzo Mancini wrote:
> Hello,
>
> PyInstaller doesn't currently have analysis support to detect libraries
> used through ctypes.
>
> The attached patch implements a minimal support for most common uses of
> ctypes, namely:
>
> * CDLL("library.so")
> * ctypes.DLL("library.so")
> * cdll.library
> * cdll.LoadLibrary("library.so")
>
> (and their windows counterparts of course).
>
> The patch adds some bytecode magic in scan_code to detect the above uses
> of ctypes, and gives PyModule instances the ability to store binary file
> names for the referenced libraries (see mf.py). Analysis class (see
> Build.py) is finally given awareness of this new ability for PyModule
> instances, so that it can update the binaries' list accordingly.
The improved attached version:
* checks for ctypes' presence before doing anything;
* correctly resolves full pathnames for the imported libraries using
ctypes.util.find_library;
* emits warnings if the analyzed ctypes code contains a full or
relative path to the library, and drops it if so - even if we included
it in the final package, it would be required on pyinstaller's side to
patch the compiled pyc for things to work correctly.
--
Lorenzo Mancini
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"PyInstaller" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/PyInstaller?hl=en
-~----------~----~----~----~------~----~------~--~---
Index: deploy/tools/PyInstaller/bindepend.py
===================================================================
--- deploy/tools/PyInstaller/bindepend.py (revision 21414)
+++ deploy/tools/PyInstaller/bindepend.py (working copy)
@@ -354,7 +354,12 @@
m = re.search(r"\s+(.*?)\s+\(.*\)", line)
if m:
lib = m.group(1)
- if os.path.exists(lib):
+ # If a dylib lists itself as dependency (?), using its bare
filename
+ # instead of full path, it won't probably be found as is on the
+ # filesystem; we skip the check.
+ if lib == os.path.basename(pth):
+ continue
+ elif os.path.exists(lib):
rslt.append(lib)
else:
print 'E: cannot find path %s (needed by %s)' % \
Index: deploy/tools/PyInstaller/Build.py
===================================================================
--- deploy/tools/PyInstaller/Build.py (revision 21414)
+++ deploy/tools/PyInstaller/Build.py (working copy)
@@ -318,8 +318,12 @@
zipfiles.append((os.path.basename(str(mod.owner)),
str(mod.owner), 'ZIPFILE'))
elif modnm == '__main__':
- pass
+ # This and the next case are about mf.PyModule
+ # instances. Take care of their required shared
+ # libraries via ctypes.
+ binaries.extend(mod.binaries)
else:
+ binaries.extend(mod.binaries)
pure.append((modnm, fnm, 'PYMODULE'))
binaries.extend(bindepend.Dependencies(binaries,
platform=target_platform))
Index: deploy/tools/PyInstaller/mf.py
===================================================================
--- deploy/tools/PyInstaller/mf.py (revision 21414)
+++ deploy/tools/PyInstaller/mf.py (working copy)
@@ -25,6 +25,12 @@
except ImportError:
zipimport = None
+try:
+ # ctypes is supported starting with python 2.5
+ import ctypes
+except ImportError:
+ ctypes = None
+
import suffixes
try:
@@ -78,7 +84,7 @@
def _getsuffixes(self):
return suffixes.get_suffixes(self.target_platform)
-
+
def getmod(self, nm, getsuffixes=None, loadco=marshal.loads):
if getsuffixes is None:
getsuffixes = self._getsuffixes
@@ -508,6 +514,45 @@
def ispackage(self, nm):
return self.modules[nm].ispackage()
+ def _resolveCtypesImports(self, mod):
+ """Completes ctypes BINARY entries intended in module with their
+ full path.
+ """
+
+ if sys.platform.startswith("linux"):
+ envvar = "LD_LIBRARY_PATH"
+ elif sys.platform.startswith("darwin"):
+ envvar = "DYLD_LIBRARY_PATH"
+ else:
+ envvar = "PATH"
+
+ def _savePaths():
+ old = os.environ.get(envvar, None)
+ os.environ[envvar] = os.pathsep.join(self.path)
+ return old
+
+ def _restorePaths(old):
+ del os.environ[envvar]
+ if old:
+ os.environ[envvar] = old
+
+ cbinaries = list(mod.binaries)
+ mod.binaries = []
+ for cbin in cbinaries:
+
+ # Executes find_library using the local paths of ImportTracker as
+ # library search paths, then replaces original values.
+
+ old = _savePaths()
+ from ctypes.util import find_library
+ cpath = find_library(os.path.splitext(cbin)[0])
+ _restorePaths(old)
+
+ if cpath is None:
+ print "W: library %s required via ctypes not found" % (cbin,)
+ else:
+ mod.binaries.append((cbin, cpath, "BINARY"))
+
def doimport(self, nm, ctx, fqname):
# Not that nm is NEVER a dotted name at this point
assert ("." not in nm), nm
@@ -536,6 +581,10 @@
# or
# mod = director.getmod(nm)
if mod:
+
+ if ctypes and isinstance(mod, PyModule):
+ self._resolveCtypesImports(mod)
+
mod.__name__ = fqname
self.modules[fqname] = mod
# now look for hooks
@@ -602,6 +651,7 @@
self._all = []
self.imports = []
self.warnings = []
+ self.binaries = []
self._xref = {}
def ispackage(self):
@@ -614,7 +664,7 @@
self._xref[nm] = 1
def __str__(self):
- return "<Module %s %s %s>" % (self.__name__, self.__file__,
self.imports)
+ return "<Module %s %s %s %s>" % (self.__name__, self.__file__,
self.imports, self.binaries)
class BuiltinModule(Module):
typ = 'BUILTIN'
@@ -641,7 +691,7 @@
self.scancode()
def scancode(self):
- self.imports, self.warnings, allnms = scan_code(self.co)
+ self.imports, self.warnings, self.binaries, allnms = scan_code(self.co)
if allnms:
self._all = allnms
@@ -715,6 +765,8 @@
STORE_FAST = dis.opname.index('STORE_FAST')
STORE_GLOBAL = dis.opname.index('STORE_GLOBAL')
LOAD_GLOBAL = dis.opname.index('LOAD_GLOBAL')
+LOAD_ATTR = dis.opname.index('LOAD_ATTR')
+LOAD_NAME = dis.opname.index('LOAD_NAME')
EXEC_STMT = dis.opname.index('EXEC_STMT')
try:
SET_LINENO = dis.opname.index('SET_LINENO')
@@ -767,12 +819,14 @@
instrs.append((op, oparg, incondition, curline))
return instrs
-def scan_code(co, m=None, w=None, nested=0):
+def scan_code(co, m=None, w=None, b=None, nested=0):
instrs = pass1(co.co_code)
if m is None:
m = []
if w is None:
w = []
+ if b is None:
+ b = []
all = None
lastname = None
level = -1 # import-level, same behaviour as up to Python 2.4
@@ -833,7 +887,99 @@
w.append("W: %s %s exec statement detected at line %s" % (lvl,
cndtl, curline))
else:
lastname = None
+
+ if ctypes:
+ # ctypes scanning requires a scope wider than one bytecode
instruction,
+ # so the code resides in a separate function for clarity.
+ ctypesb, ctypesw = scan_code_for_ctypes(co, instrs, i)
+ b.extend(ctypesb)
+ w.extend(ctypesw)
+
for c in co.co_consts:
if isinstance(c, type(co)):
- scan_code(c, m, w, 1)
- return m, w, all
+ # FIXME: "all" was not updated here nor returned. Was it the
desired
+ # behaviour?
+ _, _, _, all_nested = scan_code(c, m, w, b, 1)
+ if all_nested:
+ all.extend(all_nested)
+ return m, w, b, all
+
+def scan_code_for_ctypes(co, instrs, i):
+ """Detects ctypes dependencies, using reasonable heuristics that should
+ cover most common ctypes usages; returns a tuple of two lists, one
+ containing names of binaries detected as dependencies, the other containing
+ warnings.
+ """
+
+ def _libFromConst(i):
+ """Extracts library name from an expected LOAD_CONST instruction and
+ appends it to local binaries list.
+ """
+ op, oparg, conditional, curline = instrs[i]
+ if op == LOAD_CONST:
+ soname = co.co_consts[oparg]
+ b.append(soname)
+
+ b = []
+
+ op, oparg, conditional, curline = instrs[i]
+
+ if op in (LOAD_GLOBAL, LOAD_NAME):
+ name = co.co_names[oparg]
+
+ if name in ("CDLL", "WinDLL"):
+ # Guesses ctypes imports of this type: CDLL("library.so")
+
+ # LOAD_GLOBAL 0 (CDLL) <--- we "are" here right now
+ # LOAD_CONST 1 ('library.so')
+
+ _libFromConst(i+1)
+
+ elif name == "ctypes":
+ # Guesses ctypes imports of this type: ctypes.DLL("library.so")
+
+ # LOAD_GLOBAL 0 (ctypes) <--- we "are" here right now
+ # LOAD_ATTR 1 (CDLL)
+ # LOAD_CONST 1 ('library.so')
+
+ op2, oparg2, conditional2, curline2 = instrs[i+1]
+ if op2 == LOAD_ATTR:
+ if co.co_names[oparg2] in ("CDLL", "WinDLL"):
+ # Fetch next, and finally get the library name
+ _libFromConst(i+2)
+
+ elif name == ("cdll", "windll"):
+ # Guesses ctypes imports of these types:
+
+ # * cdll.library (only valid on Windows)
+
+ # LOAD_GLOBAL 0 (cdll) <--- we "are" here right now
+ # LOAD_ATTR 1 (library)
+
+ # * cdll.LoadLibrary("library.so")
+
+ # LOAD_GLOBAL 0 (cdll) <--- we "are" here right
now
+ # LOAD_ATTR 1 (LoadLibrary)
+ # LOAD_CONST 1 ('library.so')
+
+ op2, oparg2, conditional2, curline2 = instrs[i+1]
+ if op2 == LOAD_ATTR:
+ if co.co_names[oparg2] != "LoadLibrary":
+ # First type
+ soname = co.co_names[oparg2] + ".dll"
+ b.append(soname)
+ else:
+ # Second type, needs to fetch one more instruction
+ _libFromConst(i+2)
+
+ # If any of the libraries has been requested with anything different from
+ # the bare filename, drop that entry and warn the user - pyinstaller would
+ # need to patch the compiled pyc file to make it work correctly!
+
+ w = []
+ for bin in list(b):
+ if bin != os.path.basename(bin):
+ b.remove(bin)
+ w.append("W: ignoring %s - ctypes imports only supported using
bare filenames" % (bin,))
+
+ return b, w