【问题标题】:Pyinstaller on a setuptools package安装工具包上的 Pyinstaller
【发布时间】:2018-07-30 18:51:54
【问题描述】:

我正在尝试在我使用 Click 库在 Python 中构建的 CLI 应用程序上运行 PyInstaller。我在使用 PyInstaller 构建项目时遇到问题。 PyInstaller 在其 GitHub wiki 中有一个名为 Recipe Setuptools Entry Point 的文档,该文档提供了有关如何将 PyInstaller 与 setuptools 包一起使用的信息,我正在将其用于此项目。但是,当我运行pyinstaller --onefile main.spec 时,它似乎找不到基本模块。

我的问题是:问题仅仅是我的文件夹结构的问题吗? Recipe Setuptools Entry Point 是否假定某种文件结构?

相关信息

Pyinstaller 输出

184 INFO: PyInstaller: 3.3.1
184 INFO: Python: 3.6.4
189 INFO: Platform: Darwin-16.7.0-x86_64-i386-64bit
193 INFO: UPX is available.
Traceback (most recent call last):
  File "/usr/local/bin/pyinstaller", line 11, in <module>
    sys.exit(run())
  File "/usr/local/lib/python3.6/site-packages/PyInstaller/__main__.py", line 94, in run
    run_build(pyi_config, spec_file, **vars(args))
  File "/usr/local/lib/python3.6/site-packages/PyInstaller/__main__.py", line 46, in run_build
    PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/PyInstaller/building/build_main.py", line 791, in main
    build(specfile, kw.get('distpath'), kw.get('workpath'), kw.get('clean_build'))
  File "/usr/local/lib/python3.6/site-packages/PyInstaller/building/build_main.py", line 737, in build
    exec(text, spec_namespace)
  File "<string>", line 40, in <module>
  File "<string>", line 26, in Entrypoint
  File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 582, in get_entry_info
    return get_distribution(dist).get_entry_info(group, name)
  File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 564, in get_distribution
    dist = get_provider(dist)
  File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 436, in get_provider
    return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
  File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 984, in require
    needed = self.resolve(parse_requirements(requirements))
  File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 870, in resolve
    raise DistributionNotFound(req, requirers)
pkg_resources.DistributionNotFound: The 'myapp' distribution was not found and is required by the application

main.pymain.spec 文件,这是我的 CLI 应用程序的入口点:

block_cipher = None

def Entrypoint(dist, group, name,
               scripts=None, pathex=None, hiddenimports=None,
               hookspath=None, excludes=None, runtime_hooks=None):
    import pkg_resources

    # get toplevel packages of distribution from metadata
    def get_toplevel(dist):
        distribution = pkg_resources.get_distribution(dist)
        if distribution.has_metadata('top_level.txt'):
            return list(distribution.get_metadata('top_level.txt').split())
        else:
            return []

    hiddenimports = hiddenimports or []
    packages = []
    for distribution in hiddenimports:
        packages += get_toplevel(distribution)

    scripts = scripts or []
    pathex = pathex or []
    # get the entry point
    ep = pkg_resources.get_entry_info(dist, group, name)
    # insert path of the egg at the verify front of the search path
    pathex = [ep.dist.location] + pathex
    # script name must not be a valid module name to avoid name clashes on import
    script_path = os.path.join(workpath, name + '-script.py')
    print ("creating script for entry point", dist, group, name)
    with open(script_path, 'w') as fh:
        print("import", ep.module_name, file=fh)
        print("%s.%s()" % (ep.module_name, '.'.join(ep.attrs)), file=fh)
        for package in packages:
            print ("import", package, file=fh)

    return Analysis([script_path] + scripts, pathex, hiddenimports, hookspath, excludes, runtime_hooks)

a = Entrypoint('myapp', 'console_scripts', 'myapp')

pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          exclude_binaries=True,
          name='main',
          debug=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               name='main')

我在虚拟环境中运行pip3 install --editable .时生成的myapp脚本的内容:

#!/some/path/to/myapp-cli/venv/bin/python3.6
# EASY-INSTALL-ENTRY-SCRIPT: 'myapp','console_scripts','myapp'
__requires__ = 'myapp'
import re
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(
        load_entry_point('myapp', 'console_scripts', 'myapp')()
    )

最后,我的存储库结构:

myapp-cli/
├── README.md
├── myapp
│   ├── __init__.py
│   ├── main.py
│   ├── main.spec
│   ├── resources
│   │   ├── __init__.py
│   │   └── functions.py
│   ├── subcommands
│   │   ├── __init__.py
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   └── cli.py
│   │   ├── create
│   │   │   ├── __init__.py
│   │   │   └── cli.py
│   │   ├── destroy
│   │   │   ├── __init__.py
│   │   │   └── cli.py
│   │   └── switch
│   │       ├── __init__.py
│   │       └── cli.py
│   └── variables.py
├── requirements.txt
└── setup.py

还有我的setup.py 文件:

from setuptools import find_packages
from setuptools import setup
import os

base_dir = os.path.dirname(__file__)

setup(
    entry_points = '''
        [console_scripts]
        myapp=myapp.main:entry_point
    ''',
    install_requires = [
        'packageone==1.0',
        'packagetwo==2.0',
    ],
    name = "myapp",
    packages=find_packages(),
    setup_requires="setuptools",
    version = "0.1",
)

【问题讨论】:

    标签: python python-3.x pyinstaller setuptools python-click


    【解决方案1】:

    这个错误:

    pkg_resources.DistributionNotFound:未找到“myapp”分发,应用程序需要此分发

    表示此包不在PYTHONPATH 上。我在 Windows 上修复了它:

    set PYTHONPATH=.
    

    根据您选择的操作系统进行调整。


    除了路径问题,还有:

    在 setup.py 中:

    setup(
        entry_points = '''
            [console_scripts]
            myapp=myapp.main:entry_point
        ''',
    

    在 main.spec 中:

    a = Entrypoint('myapp', 'console_scripts', 'myapp')
    

    根据 setup.py,您的入口点似乎是 myapp.main 而不是 myapp。所以你可能需要:

    a = Entrypoint('myapp', 'console_scripts', 'myapp.main')
    

    【讨论】:

      【解决方案2】:

      首先:我结合了斯蒂芬的答案和我自己的一些挖掘来找到答案。最后,斯蒂芬的第一部分成功了:手动添加/导出PYTHONPATH 变量。您实际上可以在 Entrypoint 函数中使用 pathex 指定它,如下所示:

      a = Entrypoint('myapp-cli',
          'console_scripts',
          'myapp',
          pathex=['/some/path/to/myapp-cli/myapp', '/some/path/to/myapp-cli']
      )
      

      毕竟我最终不需要myapp.main

      第二:我仍然遇到 PyInstaller 生成单个二进制文件的问题。对我来说,这成功了:

      • 最新版本的 PyInstaller 添加到您的 requirements.txt 或您的 install_requires setup.py: https://github.com/pyinstaller/pyinstaller/archive/develop.zip
      • 此外,您可以使用pyi-makespec 中的--onefile 选项创建.spec 文件,如下所示:pyi-makespec --onefile myapp.py。这将创建一个.spec 文件,确保您的所有包都编译到二进制文件中。

      最后,下面的规范文件成功了,我能够制作一个完全正常工作的二进制文件:

      # -*- mode: python -*-
      
      block_cipher = None
      
      def Entrypoint(dist, group, name,
                     scripts=None, pathex=None, hiddenimports=None,
                     hookspath=None, excludes=None, runtime_hooks=None):
          import pkg_resources
      
          # get toplevel packages of distribution from metadata
          def get_toplevel(dist):
              distribution = pkg_resources.get_distribution(dist)
              if distribution.has_metadata('top_level.txt'):
                  return list(distribution.get_metadata('top_level.txt').split())
              else:
                  return []
      
          hiddenimports = hiddenimports or []
          packages = []
          for distribution in hiddenimports:
              packages += get_toplevel(distribution)
      
          scripts = scripts or []
          pathex = pathex or []
          # get the entry point
          ep = pkg_resources.get_entry_info(dist, group, name)
          # insert path of the egg at the verify front of the search path
          pathex = [ep.dist.location] + pathex
          # script name must not be a valid module name to avoid name clashes on import
          script_path = os.path.join(workpath, name + '-script.py')
          print ("creating script for entry point", dist, group, name)
          with open(script_path, 'w') as fh:
              print("import", ep.module_name, file=fh)
              print("%s.%s()" % (ep.module_name, '.'.join(ep.attrs)), file=fh)
              for package in packages:
                  print ("import", package, file=fh)
      
          return Analysis([script_path] + scripts, pathex, hiddenimports, hookspath, excludes, runtime_hooks)
      
      a = Entrypoint('myapp-cli',
          'console_scripts',
          'myapp',
          pathex=['/some/path/to/myapp-cli/myapp', '/some/path/to/myapp-cli']
      )
      
      pyz = PYZ(a.pure, a.zipped_data,
                   cipher=block_cipher)
      exe = EXE(pyz,
                a.scripts,
                a.binaries,
                a.zipfiles,
                a.datas,
                name='myapp',
                debug=False,
                strip=False,
                upx=True,
                runtime_tmpdir=None,
                console=True )
      

      我认为最终使用 Cobra for Golang 之类的东西会更容易,因为 Golang 可以开箱即用地编译单文件二进制文件。但是,如果您更喜欢 Python,这应该可以解决问题。

      【讨论】:

        【解决方案3】:

        我注意到,一旦你按照 Scott Crooks 在ticked answer 中推荐的方式对入口点进行了猴子修补,typical way of adding a data file 就不起作用了。对我来说,我必须附加到 a.datas 数组。在 python3 中,这看起来像:

        ...
        a = Entrypoint(...)
        from pathlib import Path
        Path('/tmp/modulename/datafile.txt').write_text(Path('datafile.txt').read_text()))
        a.datas.append('datafile.txt', '/tmp/modulename/datafile.txt', 'DATA')
        
        pyz = PYZ(...)
        ...
        

        【讨论】:

          【解决方案4】:

          接受的答案对我不起作用。我必须通过.spec 文件添加egg-info 目录。

          我对@9​​87654323@ 函数的调用如下所示:

          a = Entrypoint(
                  'PrintIt',
                  'console_scripts',
                  'printit',
                  datas=[('plugins/*.egg', 'plugins/'),
                         ('../PrintIt.egg-info/*', 'PrintIt.egg-info/')])
          

          【讨论】:

            【解决方案5】:

            经过大量搜索,此错误通常是由于尝试访问您项目的包的元数据(即版本是主要的)。

            包元数据通常使用pkg_resources 或更旧的distutil 访问,要么显式地访问,要么经常隐藏在其他包中(通常尝试访问包版本)。从 Python v3.8 开始,它还可以在 importlib.metadata 内的 stdlib 中使用。

            如果是这种情况,您可能需要将部分或全部文件包含在 mypackage.egg-info 文件夹中,尤其是文件 PKG_INFO,但可能需要全部。

            有多种方法可以做到这一点,这里有几个我喜欢的:


            1。如果您使用的是 script.spec 文件,您可以更新 datas= 行以包含此信息,根据 Charles 的回答:

            a = Analysis(['myscript.py'],
                         pathex=['C:\\path\\to\\mypackage'],
                         binaries=[],
                         datas=[('mypackage.egg-info/*','mypackage.egg-info')],
            

            2。创建一个自定义的hook文件,放到一个目录下,在命令行中将该目录添加为自定义的hook目录。

            创建一个hook-mypackage.py 挂钩文件,其中包含以下非常简单且非常优雅的行:

            from PyInstaller.utils.hooks import copy_metadata
            
            datas = copy_metadata('md2mat')
            

            我将它放入我的根包/repo 文件夹中的一个新的hooks 文件夹中,然后将以下内容添加到我的 pyinstaller 命令中:

            pyinstaller -F -y --additional-hooks-dir=hooks myscript.py
            

            它运行良好,假设 copy_metadata 函数在我们从旧的元数据包切换到新的 importlib.metadata 时得到很好的维护,它应该在未来的 Python 更新中运行良好。


            3。直接在命令行中添加额外的数据文件

            这可能是我最喜欢的,如果我能让它工作的话......

            pyinstaller --add-data <SRC;DEST> myscript.py
            

            此选项--add-data 出现在帮助输出中(pyinstaller --help),并表示参数的格式应该是 SRC;Windows 的 DEST,所以我认为它必须匹配 @ 987654338@ 其他方法的格式,但我无法让它工作。

            我认为最接近正确格式的是:

            pyinstaller -F -y --add-data "mypackage.egg-info/*;mypackage.egg-info"
            pyinstaller -F -y --add-data="mypackage.egg-info/*;mypackage.egg-info"
            

            这些可以编译,但生成的 exe 会运行而没有输出。

            --add-data 选项在 PyInstaller Documentation 中缺失,但在运行 pyinstaller --help-commands 时显示。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2018-11-14
              • 1970-01-01
              • 1970-01-01
              • 2018-02-03
              • 2012-02-08
              • 1970-01-01
              • 2019-11-11
              相关资源
              最近更新 更多