How to tidy up a packaged Python app with pyinstaller? - python

So, let's say I got a simple pyqt app main.py:
import sys
from PyQt5 import QtWidgets
def main():
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
w.resize(250, 150)
w.move(300, 300)
w.setWindowTitle('Simple')
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
And then i got a main.spec which packages the app in one folder:
# -*- mode: python -*-
block_cipher = None
import inspect, os
current_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
a = Analysis(['main.py'],
pathex=[current_path],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
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=False,
console=True )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=False,
name='main')
The outcome of doing pyinstaller main.spec will be a working pyqt app which has a mess of files:
So, here's the thing, I don't like the current outcome and I also don't like the option of using --onefile (the idea of extracting files into a temp directory is not my cup of tea).
Now, I've found this interesting article which presents a solution to this problem and I was trying to reproduce it here with this simple mcve but for some reason I've got stuck at certain point. Here's the steps i've followed:
1) I've created a file pyinstaller\use_lib.py:
import sys
import os
sys.path.append(os.path.join(os.path.dirname(sys.argv[0]), "lib"))
2) I've replaced runtime_hooks=[] by runtime_hooks=['.\\pyinstaller\\use_lib.py'] in the above main.spec file
3) I've rerun pyinstaller main.spec, which has generated the bunch of messy files like the above screenshot
4) I've moved manually all dependency files to a lib directory and the results is this:
PROBLEM: When i try to run the app will crash:
Why do you think it's crashing? At which step I've messed up? Could you please explain me how to fix it?

You can't move those dlls away from exe (into another directory). Those dlls are linked statically and should be placed in the same directory as exe.
Anyway. Look into some application folders inside of your C:\Program Files. There are big bunch of files inside each of those directories. It is just the way it goes. And no one cares because users does not look into these folders.
If you want to distribute your application you should act as all other developers. Folder state after using PyInstaller is not the final form of your application but only an initial form: any C/C++ application will start its way to the users from this exact form.
So if you want to distribute your application to the users you should use one of the installer tools. The best form of installation packet for the Windows platform is msi packet (made for "Windows Installer"). To build your msi packet you may use WiX Installer (the simplest way to create msi packets) or MS Visual Studio. Also there is a plenty amount of installation tools which will generate exe form of installation packets (and usually they are much more simple to use than msi-tools): NSIS, Inno Setup, InstallShield (paid!), etc. Also you may search names of this installers through https://pypi.python.org/pypi database: there are some special Python packets which you can use to manage some of this installation tools.

Related

How to package python and kivy application for Raspbian Buster

I am currently writing an application for a raspberry pi 3 using python3.7 and kivy1.11.1. I am trying to package a test application to ensure that it will work. I have created a simple grid with 6 buttons. When you click on a button it prints "button pressed" to the screen. I am using a separate kv file to lay this out as this will be more practical as the application develops. So I have two files, "test.py" and "Test.kv".
I have been using pyinstaller to package my app but it is not working. It runs and creates the executable file just fine with no errors. I then drag my executable file out to my main folder from the "dist" folder as recommended in this video.
My command is:
pyinstaller --onefile --add-data "Test.kv:." test.py
I have also tried:
pyinstaller --onedir --add-data "Test.kv:." test.py
which does the same: blank window no widgets, no errors.
When I try to execute it nothing happens. I don't know what is wrong because I'm not getting any errors when I try to execute it from the terminal. When I do the exact same process on windows it works so its not the code. On occasion, I think it does eventually run but I just get a blank screen and again no error. This would leave me to believe it's an issue with reading the .kv file.
Perhaps this process doesn't work with kivy and I need to use some other method to package my application. However I have also tried this kind of approach but it has left me with errors. I much prefer the pyinstaller method however I don't know if it is going to work as it doesn't seem to pick up the .kv file.
Any help/advice on this issue would be greatly appreciated?
My test.spec file:
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['test.py'],
pathex=['/home/pi/Desktop/WinTest'],
binaries=[],
datas=[('Test.kv', '.')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='test',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False )
The following picture is my program folder (sorry for the poor quality). My library files are in venv/Lib/site-packages. This is where my kivy/kivy_deps folders are. Could the issue be that the program can't find my library files? But surely if that was the issue it would throw a "file not found" error? And as previously stated, I am not receiving any errors.
Found this in my warn-text.txt file
missing module named kivy.lib.vidcore_lite.egl - imported by kivy.lib.vidcore_lite (top-level), kivy.core.window.window_egl_rpi (top-level)
missing module named kivy.lib.vidcore_lite.bcm - imported by kivy.lib.vidcore_lite (top-level), kivy.core.window.window_egl_rpi (top-level)

How do I resolve a missing utility module preventing my executable from running when I already installed the parent module?

I've written a few Python scripts to create a tkinter GUI for a machine learning algorithm process. I originally coded everything in PyCharm, but I'd really like to put everything together into a stand-alone executable. I've moved my main script and its .py dependencies into their own directory and tested it out using the Command Prompt, and it works great. However, when I run pyinstaller, the executable is created but fails on startup.
The program is made up of three files, with GUI.py being the main script. As mentioned above, I moved the dependent files into a new directory and tested GUI.py in the Command Prompt, and it worked great. Executable is created (albeit with a lot of warnings about missing 'api-ms-win-crt' files) but can't be run.
I created the executable using the command:
pyinstaller --onefile GUI.py
When the executable is run from the command line after creation, I get a big long traceback ending in the following:
File "site-packages\sklearn\metrics\pairwise.py", line 32, in <module>
File "sklearn\metrics\pairwise_fast.pyx", line 1, in init
sklearn.metrics.pairwise_fast
ModuleNotFoundError: No module named 'sklearn.utils._cython_blas'
[3372] Failed to execute script GUI
I know I've already explicitly imported sklearn through the command prompt, but from the traceback, it seems I'm missing a utility module somewhere. I tried to import the missing module specifically, but I got an error that no distributed module was available.
I don't have much experience with pyinstaller, and I have no idea where to go from here. I'm using Windows 10 and Python 3.7.3.
It seems that Pyinstaller can't resolve sklearn import. So one easy way is to just bring the whole module directory which located in <path_to_python>/Lib/site-packages/sklearn/ with executable output. So use below spec file to generate your executable:
# -*- mode: python -*-
block_cipher = None
a = Analysis(['test.py'],
pathex=['<path to root of your project>'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
a.datas += Tree('<path_to_sklearn_in_python_dir>', prefix='sklearn')
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='test',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
runtime_tmpdir=None,
console=True )
Finally generate your executable with
pyinstaller test.spec
This should resolve import errors for sklearn but if you face other NotFound imports add them like above to spec file.
Building up on M.R.'s answer, you can directly include the path to sklearn in your original pyinstaller command:
pyinstaller --onefile GUI.py --add-data "<path-to-python>\Lib\site-packages\sklearn;sklearn"
which results in the following line of code being added inside a = Analysis() in the automatically-generated GUI.spec file:
datas=[('<path-to-python>\\Lib\\site-packages\\sklearn', 'sklearn')]
Note that the --onefile option will result in an executable that is slower to start up than the default one-folder bundle (based on both the pyinstaller documentation and my own experience bundling up sklearn):
pyinstaller GUI.py --add-data "<path-to-python>\Lib\site-packages\sklearn;sklearn"

Pyinstaller exe conversion fail - using lightgbm and sklearn

I am trying to convert my code into an exe using pyinstaller spec. Pyinstaller initially failed with the following reason:
main__.PyInstallerImportError: Failed to load dynlib/dll
'C:\\Users\\...\\lightgbm\\../compile\\lib_lightgbm.dll'. Most probably this
dynlib/dll was not found when the application was frozen.
I tried to correct it by adding the following line to my pathex list in the spec:
'myenv\\lib\\site-packages\\lightgbm'
Note: myenv is my virtualenv created for this project.
Which led to ImportError for sklearn. I added sklearn to hidden imports. This is my final spec fie:
# -*- mode: python -*-
import sys
sys.setrecursionlimit(5000)
block_cipher = None
a = Analysis(['myscript.py'],
pathex=['C:\\project_folder', 'myenv\\lib\\site-packages\\lightgbm'],
binaries=[],
datas=[('lib_lightgbm.dll', '.')],
hiddenimports=['cython', 'sklearn', 'sklearn.feature_extraction','sklearn.pipeline', 'sklearn.ensemble', 'sklearn.neighbors.typedefs', 'sklearn.neighbors.quad_tree', 'sklearn.tree._utils'],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='myscript',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='myscript')
This leads to an error I find very weird:
File "myenv\lib\site-packages\lightgbm\sklearn.py", line 9, in <module>
ImportError: attempted relative import with no known parent package
[12692] Failed to execute script myscript
I have no idea why it would be searching for sklearn.py inside lightgbm?
There is sklearn.py inside lightgbm, but I am not sure how to fix this error. Also, there is no sklearn folder in the dist/myscript folder. Ideally it should have been present. I tried to manually copy the sklearn folder, although, as suspected, it made no difference. Could someone please let me know where the spec file is wrong?
Let's go back to the first error you had. That error suggests lib_lightgbm.dll was not seen when your application was frozen.
Assuming you run pyinstaller from Windows cmd.exe, you can fix this by passing a hook for lightgbm to pyinstaller, so that it knows where to get it from, e.g.
pyinstaller --additional-hooks-dir dir_with_lightgbm_hook --hidden-import lightgbm myscript.py
The name of the hook should be hook-lightgbm.py and for its contents, you can look here: https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/hooks/hook-numpy.py , it should be something similar. Just make sure that the library is added to the binaries list, i.e.
binaries.append((os.path.join(os.path.dirname(
get_module_file_attribute('lightgbm')), "lib_lightgbm.dll"), "lightgbm"))
The second "lightgbm" argument is the location where the .dll will be copied, relative to your distribution directory, so in your case it should end up as dist\myscript\lightgbm\lib_lightgbm.dll.
Note: Adding lib_lightgbm.dll through a hook will make pyinstaller add the dependencies for it to the distribution, so it is better to do it like this than just copying it manually.
Second note: When copying your distribution package to another machine, you might need to install the appropriate Visual Studio redistributable used by the library (e.g. VS 2015 redistributable).
EDIT: Forgot to mention that if you do it like this, you can remove the path you added to your pathex.

Python: how to specify output folders in Pyinstaller .spec file

I am using python 3.5 and pyinstaller version 3.1.1. I have specified a .spec file, called SCADAsync_spec.spec, as follows:
block_cipher = None
a = Analysis(['SCADAsync.py'],
pathex=['C:\\repo\\analysis\\trunk\\source\\python\\functions', 'C:\\repo\\analysis\\trunk\\source\\python\\Executables'],
binaries=None,
datas=[('figs\\ROMO_icon.ico','figs'),('figs\\OpenFile2.gif','figs'),('figs\\ROMOWind_Logo2015_CMYK.png','figs')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='SCADAsync',
debug=True,
strip=False,
upx=True,
console=True
)
That works fine when executed with
pyinstaller SCADAsync_spec.spec
Now that creates two large folders (dist and build) , which I would prefer to store elsewhere than in the default directory. Does anyone know how to set the location of those folders in the spec file? I'd like to keep my command line command as simple as possible, i.e. the .exe should build just by typing
pyinstaller SCADAsync_spec.spec
From the Pyinstaller manual it seems I can specify globals called 'DISTPATH' and 'workpath' to the spec file (https://pythonhosted.org/PyInstaller/spec-files.html). But I cannot really figure out how to do that.
Any help would be greatly appreciated!
Nick
Pyinstaller spec/build/dist location paths can be configured as part of pyinstaller command. Refer below example
pyinstaller --specpath /opt/bk/spec --distpath /opt/bk/dist --workpath /opt/bk/build testscript.py
As Jonas Gröger pointed out you can indeed do:
import PyInstaller.config
PyInstaller.config.CONF['workpath'] = "./my_build_directory"
# ... rest of spec file
However the documentation in the config module says that it works only on a limited number of variable:
This module holds run-time PyInstaller configuration.
Variable CONF is a dict() with all configuration options that are necessary
for the build phase. Build phase is done by passing .spec file to exec()
function. CONF variable is the only way how to pass arguments to exec() and
how to avoid using 'global' variables.
NOTE: Having 'global' variables does not play well with the test suite
because it does not provide isolated environments for tests. Some tests might
fail in this case.
NOTE: The 'CONF' dict() is cleaned after building phase to not interfere with
any other possible test.
To pass any arguments to build phase, just do:
from PyInstaller.config import CONF
CONF['my_var_name'] = my_value
And to use this variable in the build phase:
from PyInstaller.config import CONF
foo = CONF['my_var_name']
This is the list of known variables. (Please update it if necessary.)
cachedir
hasUPX
hiddenimports
noconfirm
pathex
ui_admin
ui_access
upx_dir
workpath
If you use "DISTPATH" in capital the trick will not work (Pyinstaller 3.3.1 and python 2.7.13) but if you set it lower case:
import PyInstaller.config
PyInstaller.config.CONF['distpath'] = "./my_app_directory"
# ... rest of spec file
That will work... :)

How to compile python script to binary executable

I need to convert a Python script to a Windows executable.
I have Python 2.6 installed to python26.
I have created one script and kept it in C:\pythonscript. Inside this folder there are two files
Setup.py and oldlogs.py (this file need coversion)
setup.py code is
from distutils.core import setup
import py2exe
setup(console=['oldlogs.py'])
How can I convert oldlogs.py to an exe file?
Or use PyInstaller as an alternative to py2exe. Here is a good starting point. PyInstaller also lets you create executables for linux and mac...
Here is how one could fairly easily use PyInstaller to solve the issue at hand:
pyinstaller oldlogs.py
From the tool's documentation:
PyInstaller analyzes myscript.py and:
Writes myscript.spec in the same folder as the script.
Creates a folder build in the same folder as the script if it does not exist.
Writes some log files and working files in the build folder.
Creates a folder dist in the same folder as the script if it does not exist.
Writes the myscript executable folder in the dist folder.
In the dist folder you find the bundled app you distribute to your users.
I recommend PyInstaller, a simple python script can be converted to an exe with the following commands:
utils/Makespec.py [--onefile] oldlogs.py
which creates a yourprogram.spec file which is a configuration for building the final exe. Next command builds the exe from the configuration file:
utils/Build.py oldlogs.spec
More can be found here
Since other SO answers link to this question it's worth noting that there is another option now in PyOxidizer.
It's a rust utility which works in some of the same ways as pyinstaller, however has some additional features detailed here, to summarize the key ones:
Single binary of all packages by default with the ability to do a zero-copy load of modules into memory, vs pyinstaller extracting them to a temporary directory when using onefile mode
Ability to produce a static linked binary
(One other advantage of pyoxidizer is that it does not seem to suffer from the GLIBC_X.XX not found problem that can crop up with pyinstaller if you've created your binary on a system that has a glibc version newer than the target system).
Overall pyinstaller is much simpler to use than PyOxidizer, which often requires some complexity in the configuration file, and it's less Pythony since it's written in Rust and uses a configuration file format not very familiar in the Python world, but PyOxidizer does some more advanced stuff, especially if you are looking to produce single binaries (which is not pyinstaller's default).
# -*- mode: python -*-
block_cipher = None
a = Analysis(['SCRIPT.py'],
pathex=[
'folder path',
'C:\\Windows\\WinSxS\\x86_microsoft-windows-m..namespace-downlevel_31bf3856ad364e35_10.0.17134.1_none_50c6cb8431e7428f',
'C:\\Windows\\WinSxS\\x86_microsoft-windows-m..namespace-downlevel_31bf3856ad364e35_10.0.17134.1_none_c4f50889467f081d'
],
binaries=[(''C:\\Users\\chromedriver.exe'')],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='NAME OF YOUR EXE',
debug=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=True )

Categories

Resources