Pyinstaller on a setuptools package

First: I used a combination of Stephen's answer, and some digging of my own to find the answer. In the end, Stephen's first part did the trick: manually adding / exporting the PYTHONPATH variable. You can actually specify this using pathex in the Entrypoint function like so:

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

I didn't end up needing the myapp.main after all.

Second: I was still having issues with PyInstaller not producing a single binary. For me, this did the trick:

  • Add the latest version of PyInstaller to your requirements.txt or to your install_requires in setup.py: https://github.com/pyinstaller/pyinstaller/archive/develop.zip.
  • Also, you can make your .spec file with the --onefile option in pyi-makespec like so: pyi-makespec --onefile myapp.py. This will make a .spec file that ensures that all of your packages are compiled into the binary.

In the end, the following spec file did the trick, and I was able to make a fully working binary:

# -*- 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 )

I think in the end using something like Cobra for Golang would work easier since Golang compiles one-file binaries out of the box. However, if you prefer Python, this should do the trick.


This error:

pkg_resources.DistributionNotFound: The 'myapp' distribution was not found and is required by the application

indicates that this package is not on PYTHONPATH. I fixed it on Windows with:

set PYTHONPATH=.

adjust to your OS of choice.


In addition to the path problem, there is:

In setup.py:

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

In main.spec:

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

According to setup.py, it looks like your entry point is myapp.main not myapp. So you may need:

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

The accepted answer didn't work for me. I had to add the egg-info directory via the .spec file.

My call to the Entrypoint function looks like this:

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