Repository: pyinstaller/pyinstaller

Issue:
reenable scanning code for ctypes dlls
Recently I noticed that method scan_code_for_ctypes() is not used in python3 branch. We should use it again.



Discussion:
I found out that this is outside of my knowledge: It requires understanding of Python bytecode.

So it looks like this to be my job :-)

Should this go into release 3.0 or 3.1?

- @htgoebel  Rather 3.0 if possible, because it might ease some hooks that are bundling additional dlls.
- I'm just implementing some preparation for this - collect all modules that needs scanning.

> I'm just implementing some preparation for this - collect all modules that needs scanning.

Fine. It'll take some days until I can start working on this.

@matysek Do we have a test-case for this? `import/test_ctypes_cdll_c.py` passes with both Python 2.7 and 3.3

@htgoebel Do you mean in py.test suite? In py.test there is nothing like that.

Could you make a new bug report with a list of tests that are important and you'd like me to move them to py.test?

@matysek  I meant test-cases at all. There is only one: `test_ctypes_cdll_c`. And this is a no-brainer since the resp. dll/so is collected anyway.

I'll implement some more :-) And I'll implement them for the new test-suite. This is a good training for me.

@htgoebel Just to let you know that yesterday I implemented support for .toc files:
- Just create a new test script in ./functional/scripts/pyi_scriptname.py and
- then just create .toc like ./functional/logs/pyi_scriptname.toc and 
- it should be picked  up automatically

More hints:
- the new suite allow you to pass pyinstaller cli options to  pyinstaller and options that the executable should be run with.

@matysek I've seen this. Thanks. I just pushed support for having the source within the test-function, see 7c0bcf1c.

Regarding the ctypes-tests: We have to thing about how to test this at all. The `test_ctypes_CDLL_find_library__nss_files` I just added passes, but `libnss_files.so` is not included in the frozen app. Am I missing something here?

libnss_files\* is in the _unix_excludes list in ./PyInstaller/depend/dylib.py.

IC. Thanks. Any idea what to use instead? ghostscript should be available on may systems, too.

What about libusb?  I think that the `hook-usb.py` could be dropped when resolving ctypes imports is working.

libusb sounds good. I'll add this, too.


Resolving patch:
diff --git a/PyInstaller/building/build_main.py b/PyInstaller/building/build_main.py
--- a/PyInstaller/building/build_main.py
+++ b/PyInstaller/building/build_main.py
@@ -36,7 +36,7 @@
 from .osx import BUNDLE
 from .toc_conversion import DependencyProcessor
 from .utils import _check_guts_toc_mtime
-from ..depend.utils import create_py3_base_library
+from ..depend.utils import create_py3_base_library, scan_code_for_ctypes
 from ..archive import pyz_crypto
 from ..utils.misc import get_path_to_toplevel_modules, get_unicode_modules, mtime
 from ..configure import get_importhooks_dir
@@ -412,13 +412,14 @@ def assemble(self):
 
         ### Look for dlls that are imported by Python 'ctypes' module.
         # First get code objects of all modules that import 'ctypes'.
-        logger.info('Looking for ctypes DLLs - TODO')
+        logger.info('Looking for ctypes DLLs')
         ctypes_code_objs = self.graph.get_co_using_ctypes()  # dict like:  {'module1': code_obj, 'module2': code_obj}
-        for m in ctypes_code_objs:
-            logger.debug('  %s' % m)
-        # TODO Use function PyInstaller.depend.utils:scan_code_for_ctypes() to get dlls that might be needed.
-        # TODO add dlls found in ctypes to self.binaries.
-
+        for name, co in ctypes_code_objs.items():
+            # Get dlls that might be needed by ctypes.
+            logger.debug('Scanning %s for shared libraries or dlls', name)
+            ctypes_binaries, ctypes_warnings = scan_code_for_ctypes(co)
+            self.binaries.extend(set(ctypes_binaries))
+            # TODO: print and write ctypes_warnings
 
         # Analyze run-time hooks.
         # Run-time hooks has to be executed before user scripts. Add them
diff --git a/PyInstaller/depend/utils.py b/PyInstaller/depend/utils.py
--- a/PyInstaller/depend/utils.py
+++ b/PyInstaller/depend/utils.py
@@ -26,7 +26,6 @@
 from ..compat import is_darwin, is_unix, is_py2, is_py27, BYTECODE_MAGIC, PY3_BASE_MODULES
 from .. import log as logging
 
-
 logger = logging.getLogger(__name__)
 
 
@@ -90,341 +89,272 @@ def _write_long(f, x):
         raise
 
 
-### TODO Minimize this code to only resolving ctypes imports.
 # This code does not work with Python 3 and is not used
 # with modulegraph.
-if is_py2:
-    IMPORT_NAME = dis.opname.index('IMPORT_NAME')
-    IMPORT_FROM = dis.opname.index('IMPORT_FROM')
-    try:
-        IMPORT_STAR = dis.opname.index('IMPORT_STAR')
-    except:
-        IMPORT_STAR = None
-    STORE_NAME = dis.opname.index('STORE_NAME')
-    STORE_FAST = dis.opname.index('STORE_FAST')
-    STORE_GLOBAL = dis.opname.index('STORE_GLOBAL')
-    try:
-        STORE_MAP = dis.opname.index('STORE_MAP')
-    except:
-        STORE_MAP = None
-    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')
-    except ValueError:
-        SET_LINENO = None
-    BUILD_LIST = dis.opname.index('BUILD_LIST')
-    LOAD_CONST = dis.opname.index('LOAD_CONST')
-    LOAD_CONST_level = LOAD_CONST
-    if is_py27:
-        COND_OPS = set([dis.opname.index('POP_JUMP_IF_TRUE'),
-                        dis.opname.index('POP_JUMP_IF_FALSE'),
-                        dis.opname.index('JUMP_IF_TRUE_OR_POP'),
-                        dis.opname.index('JUMP_IF_FALSE_OR_POP'),
-        ])
-    else:
-        COND_OPS = set([dis.opname.index('JUMP_IF_FALSE'),
-                        dis.opname.index('JUMP_IF_TRUE'),
-        ])
-    JUMP_FORWARD = dis.opname.index('JUMP_FORWARD')
-    try:
-        STORE_DEREF = dis.opname.index('STORE_DEREF')
-    except ValueError:
-        STORE_DEREF = None
-    STORE_OPS = set([STORE_NAME, STORE_FAST, STORE_GLOBAL, STORE_DEREF, STORE_MAP])
-    #IMPORT_STAR -> IMPORT_NAME mod ; IMPORT_STAR
-    #JUMP_IF_FALSE / JUMP_IF_TRUE / JUMP_FORWARD
-    HASJREL = set(dis.hasjrel)
-
-
-    # TODO Drop this function. What is it useful for?
-    def pass1(code):
-        instrs = []
-        i = 0
-        n = len(code)
-        curline = 0
-        incondition = 0
-        out = 0
-        while i < n:
-            if i >= out:
-                incondition = 0
-            c = code[i]
-            i = i + 1
-            op = ord(c)
-            if op >= dis.HAVE_ARGUMENT:
-                oparg = ord(code[i]) + ord(code[i + 1]) * 256
-                i = i + 2
-            else:
-                oparg = None
-            if not incondition and op in COND_OPS:
-                incondition = 1
-                out = oparg
-                if op in HASJREL:
-                    out += i
-            elif incondition and op == JUMP_FORWARD:
-                out = max(out, i + oparg)
-            if op == SET_LINENO:
-                curline = oparg
-            else:
-                instrs.append((op, oparg, incondition, curline))
-        return instrs
-
-
-    # TODO This function could be dropped. Modulegraph is doing code scanning.
-    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 = []
-        lastname = None
-        level = -1  # import-level, same behaviour as up to Python 2.4
-        for i, (op, oparg, conditional, curline) in enumerate(instrs):
-            if op == IMPORT_NAME:
-                if level <= 0:
-                    name = lastname = co.co_names[oparg]
-                else:
-                    name = lastname = co.co_names[oparg]
-                    #print 'import_name', name, `lastname`, level
-                m.append((name, nested, conditional, level))
-            elif op == IMPORT_FROM:
-                name = co.co_names[oparg]
-                #print 'import_from', name, `lastname`, level,
-                if level > 0 and (not lastname or lastname[-1:] == '.'):
-                    name = lastname + name
-                else:
-                    name = lastname + '.' + name
-                    #print name
-                m.append((name, nested, conditional, level))
-                assert lastname is not None
-            elif op == IMPORT_STAR:
-                assert lastname is not None
-                m.append((lastname + '.*', nested, conditional, level))
-            elif op == STORE_NAME:
-                if co.co_names[oparg] == "__all__":
-                    j = i - 1
-                    pop, poparg, pcondtl, pline = instrs[j]
-                    if pop != BUILD_LIST:
-                        w.append("W: __all__ is built strangely at line %s" % pline)
-                    else:
-                        all = []
-                        while j > 0:
-                            j = j - 1
-                            pop, poparg, pcondtl, pline = instrs[j]
-                            if pop == LOAD_CONST:
-                                all.append(co.co_consts[poparg])
-                            else:
-                                break
-            elif op in STORE_OPS:
-                pass
-            elif op == LOAD_CONST_level:
-                # starting with Python 2.5, _each_ import is preceeded with a
-                # LOAD_CONST to indicate the relative level.
-                if isinstance(co.co_consts[oparg], (int, long)):
-                    level = co.co_consts[oparg]
-            elif op == LOAD_GLOBAL:
-                name = co.co_names[oparg]
-                cndtl = ['', 'conditional'][conditional]
-                lvl = ['top-level', 'delayed'][nested]
-                if name == "__import__":
-                    w.append("W: %s %s __import__ hack detected at line %s" % (lvl, cndtl, curline))
-                elif name == "eval":
-                    w.append("W: %s %s eval hack detected at line %s" % (lvl, cndtl, curline))
-            elif op == EXEC_STMT:
-                cndtl = ['', 'conditional'][conditional]
-                lvl = ['top-level', 'delayed'][nested]
-                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)):
-                # FIXME: "all" was not updated here nor returned. Was it the desired
-                # behaviour?
-                _, _, _, all_nested = scan_code(c, m, w, b, 1)
-                all.extend(all_nested)
-        return m, w, b, all
-
-
-    # TODO Reuse this code with modulegraph implementation
-    # TODO Port this code to Python 3.
-    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.
-        """
+LOAD_CONST = dis.opmap['LOAD_CONST']
+LOAD_GLOBAL = dis.opmap['LOAD_GLOBAL']
+LOAD_NAME = dis.opmap['LOAD_NAME']
+LOAD_ATTR = dis.opmap['LOAD_ATTR']
+COND_OPS = set([dis.opmap['POP_JUMP_IF_TRUE'],
+                dis.opmap['POP_JUMP_IF_FALSE'],
+                dis.opmap['JUMP_IF_TRUE_OR_POP'],
+                dis.opmap['JUMP_IF_FALSE_OR_POP'],
+            ])
+JUMP_FORWARD = dis.opmap['JUMP_FORWARD']
+HASJREL = set(dis.hasjrel)
+assert 'SET_LINENO' not in dis.opmap  # safty belt
 
-        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)
+if is_py2:
+    _cOrd = ord
+else:
+    _cOrd = int
 
-        b = []
+def pass1(code):
+    """
+    Parse the bytecode int a list of easy-usable tokens:
+      (op, oparg, incondition, curline)
+    """
+    instrs = []
+    i = 0
+    n = len(code)
+    # TODO reestablish line numbers or remove them at all
+    curline = 0
+    incondition = 0
+    out = 0
+    while i < n:
+        if i >= out:
+            incondition = 0
+        c = code[i]
+        i = i + 1
+        op = _cOrd(c)
+        if op >= dis.HAVE_ARGUMENT:
+            oparg = _cOrd(code[i]) + _cOrd(code[i + 1]) * 256
+            i = i + 2
+        else:
+            oparg = None
+        if not incondition and op in COND_OPS:
+            incondition = 1
+            out = oparg
+            if op in HASJREL:
+                out += i
+        elif incondition and op == JUMP_FORWARD:
+            out = max(out, i + oparg)
+        instrs.append((op, oparg, incondition, curline))
+    return instrs
+
+
+def scan_code_for_ctypes(co):
+    instrs = pass1(co.co_code)
+    warnings = []
+    binaries = []
+
+    for i in range(len(instrs)):
+        # ctypes scanning requires a scope wider than one bytecode
+        # instruction, so the code resides in a separate function
+        # for clarity.
+        ctypesb, ctypesw = scan_code_instruction_for_ctypes(co, instrs, i)
+        binaries.extend(ctypesb)
+        warnings.extend(ctypesw)
+
+    for c in co.co_consts:
+        if isinstance(c, type(co)):
+            nested_binaries, nested_warnings = scan_code_for_ctypes(c)
+            binaries.extend(nested_binaries)
+            warnings.extend(nested_warnings)
+    return binaries, warnings
+
+
+# TODO Port this code to Python 3.
+def scan_code_instruction_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 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')
-
+        if op == LOAD_CONST:
+            soname = co.co_consts[oparg]
+            binaries.add(soname)
+
+    warnings = []
+    binaries = set()
+
+    op, oparg, conditional, curline = instrs[i]
+    expected_ops = (LOAD_GLOBAL, LOAD_NAME)
+
+    if op not in expected_ops:
+        return [], []
+
+    name = co.co_names[oparg]
+    if name == "ctypes":
+        # Guesses ctypes has been imported as `import ctypes` and
+        # the members are accessed like: ctypes.CDLL("library.so")
+        #
+        #   LOAD_GLOBAL 0 (ctypes) <--- we "are" here right now
+        #   LOAD_ATTR 1 (CDLL)
+        #   LOAD_CONST 1 ('library.so')
+        #
+        # In this case "strip" the `ctypes` by advancing and expecting
+        # `LOAD_ATTR` next.
+        i += 1
+        expected_ops = (LOAD_ATTR,)
+        op, oparg, conditional, curline = instrs[i]
+        if op not in expected_ops:
+            return [], []
+        name = co.co_names[oparg]
+
+    if name in ("CDLL", "WinDLL", "OleDLL", "PyDLL"):
+        # 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 in ("cdll", "windll", "oledll", "pydll"):
+        # 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')
+        i += 1
+        op, oparg, conditional, curline = instrs[i]
+        if op == LOAD_ATTR:
+            if co.co_names[oparg] == "LoadLibrary":
+                # Second type, needs to fetch one more instruction
                 _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 in ("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 binary in list(b):
-            # 'binary' might be in some cases None. Some Python modules might contain
-            # code like the following. For example PyObjC.objc._bridgesupport contain
-            # code like that.
-            #
-            #     dll = ctypes.CDLL(None)
-            if binary:
-                if binary != os.path.basename(binary):
-                    w.append("W: ignoring %s - ctypes imports only supported using bare filenames" % (binary,))
             else:
-                # None values has to be removed too.
-                b.remove(binary)
-
-        return b, w
-
-
-    # TODO Reuse this code with modulegraph implementation
-    # TODO Port this code to Python 3.
-    def _resolveCtypesImports(cbinaries):
-        """Completes ctypes BINARY entries for modules with their full path.
-        """
-        from ctypes.util import find_library
-        from ..config import CONF
+                # First type
+                soname = co.co_names[oparg] + ".dll"
+                binaries.add(soname)
+    elif op == LOAD_ATTR and name in ("util", ):
+        # Guesses ctypes imports of these types::
+        #
+        #  ctypes.util.find_library('gs')
+        #
+        #     LOAD_GLOBAL   0 (ctype)
+        #     LOAD_ATTR     1 (util) <--- we "are" here right now
+        #     LOAD_ATTR     1 (find_library)
+        #     LOAD_CONST    1 ('gs')
+        i += 1
+        op, oparg, conditional, curline = instrs[i]
+        if op == LOAD_ATTR:
+            if co.co_names[oparg] == "find_library":
+                i += 1
+                op, oparg, conditional, curline = instrs[i]
+                if op == LOAD_CONST:
+                    libname = co.co_consts[oparg]
+                    soname = ctypes.util.find_library(libname)
+                    binaries.add(soname)
+
+    # If any of the libraries has been requested with anything
+    # different then the bare filename, drop that entry and warn
+    # the user - pyinstaller would need to patch the compiled pyc
+    # file to make it work correctly!
+
+    for binary in list(binaries):
+        # 'binary' might be in some cases None. Some Python
+        # modules might contain code like the following. For
+        # example PyObjC.objc._bridgesupport contain code like
+        # that.
+        #     dll = ctypes.CDLL(None)
+        if not binary:
+            # None values has to be removed too.
+            binaries.remove(binary)
+        elif binary != os.path.basename(binary):
+            warnings.append("W: ignoring %s - ctypes imports only supported using bare filenames" % binary)
+
+    binaries = _resolveCtypesImports(binaries)
+    return binaries, warnings
+
+
+# TODO Reuse this code with modulegraph implementation
+# TODO Port this code to Python 3.
+def _resolveCtypesImports(cbinaries):
+    """Completes ctypes BINARY entries for modules with their full path.
+    """
+    from ctypes.util import find_library
+    from ..config import CONF
 
-        if is_unix:
-            envvar = "LD_LIBRARY_PATH"
-        elif is_darwin:
-            envvar = "DYLD_LIBRARY_PATH"
+    if is_unix:
+        envvar = "LD_LIBRARY_PATH"
+    elif is_darwin:
+        envvar = "DYLD_LIBRARY_PATH"
+    else:
+        envvar = "PATH"
+
+    def _setPaths():
+        path = os.pathsep.join(CONF['pathex'])
+        old = compat.getenv(envvar)
+        if old is not None:
+            path = os.pathsep.join((path, old))
+        compat.setenv(envvar, path)
+        return old
+
+    def _restorePaths(old):
+        if old is None:
+            compat.unsetenv(envvar)
         else:
-            envvar = "PATH"
-
-        def _setPaths():
-            path = os.pathsep.join(CONF['pathex'])
-            old = compat.getenv(envvar)
-            if old is not None:
-                path = os.pathsep.join((path, old))
-            compat.setenv(envvar, path)
-            return old
-
-        def _restorePaths(old):
-            if old is None:
-                compat.unsetenv(envvar)
+            compat.setenv(envvar, old)
+
+    ret = []
+
+    # Try to locate the shared library on disk. This is done by
+    # executing ctypes.utile.find_library prepending ImportTracker's
+    # local paths to library search paths, then replaces original values.
+    old = _setPaths()
+    for cbin in cbinaries:
+        # Ignore annoying warnings like:
+        # 'W: library kernel32.dll required via ctypes not found'
+        # 'W: library coredll.dll required via ctypes not found'
+        if cbin in ['coredll.dll', 'kernel32.dll']:
+            continue
+        ext = os.path.splitext(cbin)[1]
+        # On Windows, only .dll files can be loaded.
+        if os.name == "nt" and ext.lower() in [".so", ".dylib"]:
+            continue
+        cpath = find_library(os.path.splitext(cbin)[0])
+        if is_unix:
+            # CAVEAT: find_library() is not the correct function. Ctype's
+            # documentation says that it is meant to resolve only the filename
+            # (as a *compiler* does) not the full path. Anyway, it works well
+            # enough on Windows and Mac. On Linux, we need to implement
+            # more code to find out the full path.
+            if cpath is None:
+                cpath = cbin
+                # "man ld.so" says that we should first search LD_LIBRARY_PATH
+            # and then the ldcache
+            for d in compat.getenv(envvar, '').split(os.pathsep):
+                if os.path.isfile(os.path.join(d, cpath)):
+                    cpath = os.path.join(d, cpath)
+                    break
             else:
-                compat.setenv(envvar, old)
-
-        ret = []
-
-        # Try to locate the shared library on disk. This is done by
-        # executing ctypes.utile.find_library prepending ImportTracker's
-        # local paths to library search paths, then replaces original values.
-        old = _setPaths()
-        for cbin in cbinaries:
-            # Ignore annoying warnings like:
-            # 'W: library kernel32.dll required via ctypes not found'
-            # 'W: library coredll.dll required via ctypes not found'
-            if cbin in ['coredll.dll', 'kernel32.dll']:
-                continue
-            ext = os.path.splitext(cbin)[1]
-            # On Windows, only .dll files can be loaded.
-            if os.name == "nt" and ext.lower() in [".so", ".dylib"]:
-                continue
-            cpath = find_library(os.path.splitext(cbin)[0])
-            if is_unix:
-                # CAVEAT: find_library() is not the correct function. Ctype's
-                # documentation says that it is meant to resolve only the filename
-                # (as a *compiler* does) not the full path. Anyway, it works well
-                # enough on Windows and Mac. On Linux, we need to implement
-                # more code to find out the full path.
-                if cpath is None:
-                    cpath = cbin
-                    # "man ld.so" says that we should first search LD_LIBRARY_PATH
-                # and then the ldcache
-                for d in compat.getenv(envvar, '').split(os.pathsep):
-                    if os.path.isfile(os.path.join(d, cpath)):
-                        cpath = os.path.join(d, cpath)
+                text = compat.exec_command("/sbin/ldconfig", "-p")
+                for L in text.strip().splitlines():
+                    if cpath in L:
+                        cpath = L.split("=>", 1)[1].strip()
+                        assert os.path.isfile(cpath)
                         break
                 else:
-                    text = compat.exec_command("/sbin/ldconfig", "-p")
-                    for L in text.strip().splitlines():
-                        if cpath in L:
-                            cpath = L.split("=>", 1)[1].strip()
-                            assert os.path.isfile(cpath)
-                            break
-                    else:
-                        cpath = None
-            if cpath is None:
-                logger.warn("library %s required via ctypes not found", cbin)
-            else:
-                ret.append((cbin, cpath, "BINARY"))
-        _restorePaths(old)
-        return ret
+                    cpath = None
+        if cpath is None:
+            logger.warn("library %s required via ctypes not found", cbin)
+        else:
+            ret.append((cbin, cpath, "BINARY"))
+    _restorePaths(old)
+    return ret
 
 
 def is_path_to_egg(pth):
@@ -444,4 +374,4 @@ def is_path_to_egg(pth):
             if os.path.isfile(eggpth):
                 # eggs can also be directories!
                 return True
-    return False
\ No newline at end of file
+    return False


Test patch:
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
--- a/tests/functional/conftest.py
+++ b/tests/functional/conftest.py
@@ -18,6 +18,7 @@
 import sys
 import inspect
 import textwrap
+import io
 
 from PyInstaller import configure
 from PyInstaller import main as pyi_main
@@ -59,15 +60,24 @@ def test_source(self, source, *args, **kwargs):
                        will be saved into a temporary file which is
                        then passed on to `test_script`.
 
+        :param test_id: Test-id for parametrized tests. If given, it
+                        will be appended to the script filename,
+                        separated by two underscores.
+
         All other arguments are passed streigth on to `test_script`.
 
         """
         testname = inspect.stack()[1][3]
+        if 'test_id' in kwargs:
+            # For parametrized test append the test-id.
+            testname = testname + '__' + kwargs['test_id']
+            del kwargs['test_id']
+
         scriptfile = os.path.join(os.path.abspath(self._tmpdir),
                                   testname + '.py')
-        source = textwrap.dedent(source).encode('utf-8')
-        with open(scriptfile, 'w') as ofh:
-            print('# -*- coding: utf-8 -*-', file=ofh)
+        source = textwrap.dedent(source)
+        with io.open(scriptfile, 'w', encoding='utf-8') as ofh:
+            print(u'# -*- coding: utf-8 -*-', file=ofh)
             print(source, file=ofh)
         return self.test_script(scriptfile, *args, **kwargs)
 
diff --git a/tests/functional/test_import.py b/tests/functional/test_import.py
--- a/tests/functional/test_import.py
+++ b/tests/functional/test_import.py
@@ -7,14 +7,14 @@
 # The full license is in the file COPYING.txt, distributed with this software.
 #-----------------------------------------------------------------------------
 
+from __future__ import unicode_literals
 
 import os
 import pytest
 import ctypes, ctypes.util
 
 from PyInstaller.compat import is_win
-from PyInstaller.utils.tests import skipif, importorskip, xfail_py2
-
+from PyInstaller.utils.tests import skipif, importorskip, xfail_py2, skipif_notwin
 
 # Directory with data for some tests.
 _DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
@@ -47,14 +47,113 @@ def test_ctypes_CDLL_c(pyi_builder):
         """)
 
 
-# libnss_files.so should be available in most Unix/Linux systems using
-# glibc
-@skipif(not ctypes.util.find_library('nss_files'),
-        reason="required libnss_files.so missing")
-def test_ctypes_CDLL_find_library__nss_files(pyi_builder):
-    pyi_builder.test_source(
-        """
-        import ctypes, ctypes.util
-        lib = ctypes.CDLL(ctypes.util.find_library('nss_files'))
-        assert lib is not None
-        """)
+def skip_if_lib_missing(libname, text=None):
+    """
+    pytest decorator to evaluate the required shared lib.
+
+    :param libname: Name of the required library.
+    :param text: Text to put into the reason message
+                 (defaults to 'lib%s.so' % libname)
+
+    :return: pytest decorator with a reason.
+    """
+    soname = ctypes.util.find_library(libname)
+    if not text:
+        text = "lib%s.so" % libname
+    # Return pytest decorator.
+    return skipif(not (soname and ctypes.CDLL(soname)),
+                  reason="required %s missing" % text)
+
+
+_template_ctypes_CDLL_find_library = """
+    import ctypes, ctypes.util, sys, os
+    lib = ctypes.CDLL(ctypes.util.find_library(%(libname)r))
+    print(lib)
+    assert lib is not None and lib._name is not None
+    if getattr(sys, 'frozen', False):
+        soname = ctypes.util.find_library(%(libname)r)
+        print(soname)
+        libfile = os.path.join(sys._MEIPASS, soname)
+        print(libfile)
+        assert os.path.isfile(libfile), '%%s is missing' %% soname
+        print('>>> file found')
+    """
+
+_template_ctypes_test = """
+        print(lib)
+        assert lib is not None and lib._name is not None
+        import sys, os
+        if getattr(sys, 'frozen', False):
+            libfile = os.path.join(sys._MEIPASS, %(soname)r)
+            print(libfile)
+            assert os.path.isfile(libfile), '%(soname)s is missing'
+            print('>>> file found')
+    """
+
+# Ghostscript's libgs.so should be available in may Unix/Linux systems
+@skip_if_lib_missing('gs', 'libgs.so (Ghostscript)')
+def test_ctypes_CDLL_find_library__gs(pyi_builder):
+    libname = 'gs'
+    pyi_builder.test_source(_template_ctypes_CDLL_find_library % locals())
+
+
+#-- Generate test-cases for the different types of ctypes objects.
+
+libname = 'gs'
+reason = 'libgs.so (Ghostscript)'
+parameters = []
+ids = []
+for prefix in ('', 'ctypes.'):
+    for funcname in  ('CDLL', 'PyDLL', 'WinDLL', 'OleDLL', 'cdll.LoadLibrary'):
+        ids.append('%s_%s' % (prefix+funcname, libname))
+        params = (prefix+funcname, libname, reason, ids[-1])
+        # Workaround a problem in pytest: skipif on a parameter will
+        # completely replace the skipif on the test function. See
+        # https://github.com/pytest-dev/pytest/issues/954
+        # :todo: reanable this when pytest supports this
+        #if funcname in ("WinDLL", "OleDLL"):
+        #    # WinDLL, OleDLL only work on windows.
+        #    params = skipif_notwin(params)
+        parameters.append(params)
+
+@pytest.mark.parametrize("funcname,libname,reason,test_id", parameters, ids=ids)
+@skip_if_lib_missing(libname, reason)
+def test_ctypes_gen(pyi_builder, funcname, libname, reason, test_id):
+    # Workaround, see above.
+    # :todo: remove this workaround (see above)
+    if not is_win and funcname.endswith(("WinDLL", "OleDLL")):
+        pytest.skip('%s requires windows' % funcname)
+    # evaluate the soname here, so the test-code contains a constant
+    soname = ctypes.util.find_library(libname)
+    source = """
+        import ctypes ; from ctypes import *
+        lib = %s(%%(soname)r)
+    """ % funcname + _template_ctypes_test
+    source = source +_template_ctypes_test
+    pyi_builder.test_source(source % locals(), test_id=test_id)
+
+
+# TODO: Add test-cases forthe prefabricated library loaders supporting
+# attribute accesses on windows. Example::
+#
+#   cdll.kernel32.GetModuleHandleA(None)
+#
+# Of course we need to use dlls which is not are commony available on
+# windows but mot excluded in PyInstaller.depend.dylib
+
+
+@skip_if_lib_missing('usb-1.0')
+def test_ctypes_CDLL_find_library__usb(pyi_builder):
+    libname = 'usb-1.0'
+    pyi_builder.test_source(_template_ctypes_CDLL_find_library % locals())
+
+
+@skip_if_lib_missing('usb-1.0')
+def test_ctypes_CDLL__usb(pyi_builder):
+    # evaluate the soname here, so the test-code contains a constant
+    soname = ctypes.util.find_library('usb-1.0')
+    script = """
+        import ctypes
+        lib = ctypes.CDLL(%(soname)r)
+    """ + _template_ctypes_test
+    pyi_builder.test_source(script % locals())