在setuptools包上的Pyinstaller

Sco*_*oks 10 python setuptools pyinstaller python-3.x python-click

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

我的问题是:问题只是我的文件夹结构的问题吗?Recipe Setuptools入口点是否采用某种文件结构?

相关信息

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
Run Code Online (Sandbox Code Playgroud)

main.spec文件为main.py,我的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')
Run Code Online (Sandbox Code Playgroud)

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

#!/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')()
    )
Run Code Online (Sandbox Code Playgroud)

最后,我的存储库结构:

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
Run Code Online (Sandbox Code Playgroud)

我的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",
)
Run Code Online (Sandbox Code Playgroud)

Sco*_*oks 5

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

a = Entrypoint('myapp-cli',
    'console_scripts',
    'myapp',
    pathex=['/some/path/to/myapp-cli/myapp', '/some/path/to/myapp-cli']
)
Run Code Online (Sandbox Code Playgroud)

毕竟我最终不需要myapp.main

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

  • 最新版本的 PyInstaller 添加到您的requirements.txt或您的install_requiressetup.pyhttps: //github.com/pyinstaller/pyinstaller/archive/develop.zip
  • 另外,您可以使用如下选项创建.spec文件: 。这将创建一个文件,确保所有包都编译成二进制文件。--onefilepyi-makespecpyi-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 )
Run Code Online (Sandbox Code Playgroud)

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