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 yourinstall_requires
insetup.py
: https://github.com/pyinstaller/pyinstaller/archive/develop.zip. - Also, you can make your
.spec
file with the--onefile
option inpyi-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/')])