WxPython:PyInstaller 失败,没有名为 _core_ 的模块

WxPython: PyInstaller fails with No module named _core_

我正在使用 PyInstaller 将我的 wxpython (3.0.2.0) 应用程序转换为二进制文件。在 Ubuntu 12.04 上构建和执行时,二进制文件工作正常。但是,如果我在 Ubuntu 14.04 上构建,我会收到以下错误。 (当我直接启动 python 脚本时,应用程序可以工作,即 python my_application.py 即使在 Ubuntu 14.04 中也是如此)。知道使用 PyInstaller 打包应用程序时可能缺少什么吗?

$ ./my_application 
Traceback (most recent call last):
  File "<string>", line 22, in <module>
  File "/usr/local/lib/python2.7/dist-packages/PyInstaller/loader/pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "/local/workspace/my_application/out00-PYZ.pyz/wx", line 45, in <module>
  File "/usr/local/lib/python2.7/dist-packages/PyInstaller/loader/pyi_importers.py", line 270, in load_module
    exec(bytecode, module.__dict__)
  File "/local/workspace/my_application/out00-PYZ.pyz/wx._core", line 4, in <module>
**ImportError: No module named _core_**

我的 PyInstaller 规范文件如下所示:

...
pyz = PYZ(a.pure)
exe = EXE(pyz,
          a.scripts,
          exclude_binaries=True,
          name='my_application',
          debug=False,
          onefile = True,
          strip=None,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=None,
               upx=True,
               name='my_application')

从根本上说,问题出在 PyInstaller 版本上——您需要使用 develop 版本。此问题已被发现并记录在 PyInstaller Github issue.

要安装最新版本并纠正 - 在命令提示符下键入:

$ pip install git+https://github.com/pyinstaller/pyinstaller

这直接从 github 安装最新版本的 pyinstaller(这个 branch on github. Until recently, PyInstaller had a separate python3 branch, but this has been merged back into the develop branch. If you need to use Python 3.x, you will need this branch - 通过将 @develop 附加到 pip install 命令来获得这个)

上述方法依赖于您在系统上安装了 git 以获取 pyinstaller 代码(我想现在对于开发人员来说很可能)。如果没有,您可以

  1. 使用 apt-get install git 安装 git(您可能需要 sudo
  2. 下载 pyinstaller-develop zip 文件(here) and install manually. Note as per the wiki as of Oct 2014, 这应该支持 2.7 和 3.x。

就个人而言 - 我更喜欢选项 1,因为您可以避免自己从压缩源代码树构建的所有潜在问题。

测试

我在 Ubuntu 14.04、64 位、wxpython 3.0.2.0 和 python 2.7.6 上测试了这个,使用了来自 wxPython 网页的简单 "Hello world" app . OP 的问题恰好在安装 pyinstaller 开发版之前重现。安装开发版本后,应用程序正确构建并 运行 作为可执行文件。


使用 pip 与 git 的文档 - https://pip.pypa.io/en/latest/reference/pip_install.html#git

从您的问题中不清楚您在 Ubuntu 12.04 安装与 14.04 版本上使用的是哪个版本的 PyInstaller。您在 12.04 上安装的版本似乎没有出现与 14.04 上安装的标准版本相同的问题。

如果由于某种原因不需要 PyInstaller 开发版本,请进行一些修复。

来自 PyInstaller.loader.pyi_importersBuiltinImporterFrozenImporterCExtensionImporter 的实例附加到 sys.meta_path。并且 find_module 方法在导入模块时按顺序调用,直到其中一个成功。

CExtensionImporter 只选择加载 C 扩展的众多后缀之一,f.e。 wx._core_.i386-linux-gnu.so。这就是它无法加载 C 扩展 wx._core_.so 的原因。

错误代码;

class CExtensionImporter(object):
    def __init__(self):
        # Find the platform specific suffix. On Windows it is .pyd, on Linux/Unix .so.
        for ext, mode, typ in imp.get_suffixes():
            if typ == imp.C_EXTENSION:
                self._c_ext_tuple = (ext, mode, typ)
                self._suffix = ext  # Just string like .pyd  or  .so
                break

修复;

1.运行时挂钩
使用 运行time hooks 可以在不更改代码的情况下解决问题。这是修复 'WxPython' 问题的快速修复。
这个 运行time hook 改变了 CExtensionImporter 实例的一些私有属性。要使用这个钩子,将 --runtime-hook=wx-run-hook.py 赋给 pyinstaller

wx-运行-hook.py

import sys
import imp

sys.meta_path[-1]._c_ext_tuple = imp.get_suffixes()[1]
sys.meta_path[-1]._suffix = sys.meta_path[-1]._c_ext_tuple[0]

第二个 运行 时间挂钩完全替换了 sys.meta_path[-1] 中的对象。所以它应该适用于大多数情况。用作 pyinstaller --runtime-hook=pyinstaller-run-hook.py application.py

pyinstaller-运行-hook.py

import sys
import imp

from PyInstaller.loader import pyi_os_path

class CExtensionImporter(object):
    """
    PEP-302 hook for sys.meta_path to load Python C extension modules.

    C extension modules are present on the sys.prefix as filenames:

        full.module.name.pyd
        full.module.name.so
    """
    def __init__(self):
        # TODO cache directory content for faster module lookup without file system access.
        # Find the platform specific suffix. On Windows it is .pyd, on Linux/Unix .so.
        self._c_ext_tuples = [(ext, mode, typ) for ext, mode, typ in imp.get_suffixes() if typ == imp.C_EXTENSION]

        # Create hashmap of directory content for better performance.
        files = pyi_os_path.os_listdir(sys.prefix)
        self._file_cache = set(files)

    def find_module(self, fullname, path=None):
        imp.acquire_lock()
        module_loader = None  # None means - no module found by this importer.

        # Look in the file list of sys.prefix path (alias PYTHONHOME).
        for ext, mode, typ in self._c_ext_tuples:
            if fullname + ext in self._file_cache:
                module_loader = self
                self._suffix = ext
                self._c_ext_tuple = (ext, mode, typ)
                break

        imp.release_lock()
        return module_loader

    def load_module(self, fullname, path=None):
        imp.acquire_lock()

        try:
            # PEP302 If there is an existing module object named 'fullname'
            # in sys.modules, the loader must use that existing module.
            module = sys.modules.get(fullname)

            if module is None:
                filename = pyi_os_path.os_path_join(sys.prefix, fullname + self._suffix)
                fp = open(filename, 'rb')
                module = imp.load_module(fullname, fp, filename, self._c_ext_tuple)
                # Set __file__ attribute.
                if hasattr(module, '__setattr__'):
                    module.__file__ = filename
                else:
                    # Some modules (eg: Python for .NET) have no __setattr__
                    # and dict entry have to be set.
                    module.__dict__['__file__'] = filename

        except Exception:
            # Remove 'fullname' from sys.modules if it was appended there.
            if fullname in sys.modules:
                sys.modules.pop(fullname)
            # Release the interpreter's import lock.
            imp.release_lock()
            raise  # Raise the same exception again.

        # Release the interpreter's import lock.
        imp.release_lock()

        return module

    ### Optional Extensions to the PEP302 Importer Protocol

    def is_package(self, fullname):
        """
        Return always False since C extension modules are never packages.
        """
        return False

    def get_code(self, fullname):
        """
        Return None for a C extension module.
        """
        if fullname + self._suffix in self._file_cache:
            return None
        else:
            # ImportError should be raised if module not found.
            raise ImportError('No module named ' + fullname)

    def get_source(self, fullname):
        """
        Return None for a C extension module.
        """
        if fullname + self._suffix in self._file_cache:
            return None
        else:
            # ImportError should be raised if module not found.
            raise ImportError('No module named ' + fullname)

    def get_data(self, path):
        """
        This returns the data as a string, or raise IOError if the "file"
        wasn't found. The data is always returned as if "binary" mode was used.

        The 'path' argument is a path that can be constructed by munging
        module.__file__ (or pkg.__path__ items)
        """
        # Since __file__ attribute works properly just try to open and read it.
        fp = open(path, 'rb')
        content = fp.read()
        fp.close()
        return content

    # TODO Do we really need to implement this method?
    def get_filename(self, fullname):
        """
        This method should return the value that __file__ would be set to
        if the named module was loaded. If the module is not found, then
        ImportError should be raised.
        """
        if fullname + self._suffix in self._file_cache:
            return pyi_os_path.os_path_join(sys.prefix, fullname + self._suffix)
        else:
            # ImportError should be raised if module not found.
            raise ImportError('No module named ' + fullname)

#This may overwrite some other object
#sys.meta_path[-1] = CExtensionImporter()

#isinstance(object, CExtensionImporter)
#type(object) == CExtensioImporter
#the above two doesn't work here

#grab the index of instance of CExtensionImporter

for i, obj in enumerate(sys.meta_path):
    if obj.__class__.__name__ == CExtensionImporter.__name__:
        sys.meta_path[i] = CExtensionImporter()
        break

2。代码更改

class CExtensionImporter(object):
    def __init__(self):
        # Find the platform specific suffix. On Windows it is .pyd, on Linux/Unix .so.
        self._c_ext_tuples = [(ext, mode, typ) for ext, mode, typ in imp.get_suffixes() if typ == imp.C_EXTENSION]

        files = pyi_os_path.os_listdir(sys.prefix)
        self._file_cache = set(files)

因为imp.get_suffixes returns类型imp.C_EXTENSION的后缀不止一个,而且只有找到一个模块才能提前知道正确的,所以我把它们都存储在列表 self._c_ext_tuples。正确的后缀设置在 self._suffix 中,它与 self._c_ext_tuple 一起通过 load_module 方法使用,如果找到模块则从 find_module 方法使用。

def find_module(self, fullname, path=None):
    imp.acquire_lock()
    module_loader = None  # None means - no module found by this importer.

    # Look in the file list of sys.prefix path (alias PYTHONHOME).
    for ext, mode, typ in self._c_ext_tuples:
        if fullname + ext in self._file_cache:
            module_loader = self
            self._suffix = ext
            self._c_ext_tuple = (ext, mode, typ)
            break

    imp.release_lock()
    return module_loader