Sometimes your project has useful internal tools, but building them can bit cumbersome for other teams which can lead to time being wasted trying to instruct how to get build environment setup and the tool compiled. It would be nice to distribute these internal tools as self contained executables for other teams. PyInstaller is a Python package that can create Linux and Windows binaries that extract the package contents and run a script/entrypoint. Here is a short tutorial how to package internal tools with PyInstaller.
If the tool you are packaging is a simple script that does not have external dependencies (for example non Python executables), packaging can be as easy as:
pyinstaller --onefile tool.py --name tool
But many times the tool uses some binaries that have been compiled and need to be added to the package. We can add extra files into to the package with --add-data
:
# pyinstaller --onefile tool.py --name tool --add-data '<source>:<destination in package>'
pyinstaller --onefile tool.py --name tool --add-data 'file.txt:.'
Fixing paths
Above solution works quite well, but your tool will not automatically have access to the binary/file you included, as it most likely can not find the file.
To fix this, we need to add a wrapper script that modifies PATH
environment variable and adds the extracted package path (or even sub directories) to the PATH
.
When pyinstaller
extracts the package content, it uses the tmp
system directory and creates a random sub directory. Unfortunately __file__
global variable will not point to the correct location, but we need to check the correct location of the wrapper script with a small function:
import os
import sys
def get_script_location():
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
# Running from PyInstaller
# Return location of the extracted package
location = os.path.realpath(sys._MEIPASS)
else:
# Normal python run
# When running PyInstaller package this would incorrectly return
# current working directory
location = os.path.dirname(os.path.realpath(__file__))
return location
We can now add the script path to the PATH
:
def add_to_path(p):
os.environ['PATH'] = p + os.pathsep + os.environ['PATH']
add_to_path(get_script_location())
and then run the original script:
import tool
tool.main()
Check out https://github.com/buq2/pyinstaller_example for full example and how to cross compile the packages.