pywry
pywry copied to clipboard
pyinstaller doesn't work
When compiling the example to exe using pyinstaller
pyinstaller --onedir main.py
and then opening the compiled exe file the app starts but no window is opened and no error
if you just run
python main.py
does it work?
I tried on my PC, just got
ModuleNotFoundError: No module named 'pywry'
I can make sure I installed pywry, but maybe need more dynamic library
No, you error simply means you don't have the package at all. Refradless of dynamic library. Naybe you have python and python3? Which OS?
Works on macOS but fails on Windows
I compiled and install on another windows, and tried send_html.py, a window flashed and disappeared.
Since the PyWry backend runs in a subprocess the main.exe is sys.executable so calling [sys.executable, "-m", "pywry.backend", "--start"] ends up calling main.exe.
To fix this and get it working with pyinstaller you'd create a spec file and do a folder build
# -*- mode: python ; coding: utf-8 -*- # noqa
import os
import sys
from pathlib import Path
from PyInstaller.building.api import COLLECT, EXE, PYZ
from PyInstaller.building.build_main import Analysis
from PyInstaller.compat import is_darwin
NAME = "Executable Name" # Change this to the name of your executable
cwd_path = Path(os.getcwd()).resolve()
# Local python environment packages folder
venv_path = Path(sys.executable).parent.parent.resolve()
# Check if we are running in a conda environment
if is_darwin:
pathex = os.path.join(os.path.dirname(os.__file__), "site-packages")
elif "site-packages" in list(venv_path.iterdir()):
pathex = str(venv_path / "site-packages")
else:
pathex = str(venv_path / "lib" / "site-packages")
pathex = Path(pathex).resolve()
# Files that are explicitly pulled into the bundle
added_files = [
(str(cwd_path / "folder_path"), "folder_path"),
]
# Python libraries that are explicitly pulled into the bundle
hidden_imports = ["pywry.pywry"]
# Entry point
analysis_kwargs = dict(
scripts=[str(cwd_path / "main.py")],
pathex=[str(pathex), "."],
binaries=[],
datas=added_files,
hiddenimports=hidden_imports,
hooksconfig={},
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
a = Analysis(**analysis_kwargs)
pyz = PYZ(a.pure, a.zipped_data, cipher=analysis_kwargs["cipher"])
block_cipher = None
# PyWry
pywry_a = Analysis(
[str(pathex / "pywry/backend.py")],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pywry_pyz = PYZ(pywry_a.pure, pywry_a.zipped_data, cipher=block_cipher)
# PyWry EXE
pywry_exe = EXE(
pywry_pyz,
pywry_a.scripts,
[],
exclude_binaries=True,
name="PyWry",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
console=True,
disable_windowed_traceback=False,
target_arch="x86_64",
codesign_identity=None,
entitlements_file=None,
)
exe_args = [
pyz,
a.scripts,
[],
]
exe_kwargs = dict(
name=NAME,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
upx_exclude=[],
console=True,
disable_windowed_traceback=False,
target_arch="x86_64",
codesign_identity=None,
entitlements_file=None,
)
# Packaging settings
exe_kwargs["exclude_binaries"] = True
collect_args = [
a.binaries,
a.zipfiles,
a.datas,
]
collect_kwargs = dict(
strip=False,
upx=True,
upx_exclude=[],
name=NAME,
)
if is_darwin:
exe_kwargs["argv_emulation"] = True
exe = EXE(*exe_args, **exe_kwargs)
pywry_collect_args = [
pywry_a.binaries,
pywry_a.zipfiles,
pywry_a.datas,
]
coll = COLLECT(
*([exe] + collect_args + [pywry_exe] + pywry_collect_args),
**collect_kwargs,
)
If you change the PyWry exe name, In your main.py include this before pywry import
import os
os.environ["PYWRY_EXECUTABLE"] = "NewName"
Then in cmdline call pyinstaller filename.spec --clean -y
Hope that helps!
Oh, I see why it didn't worked
Is there a way to simplify the way you run it instead of sys.executable?
Do you have to run it in separate process and not just another thread?
Becasue I think that using sys.executable for running the subprocess will end with another issues
We run it as a subprocess due to Wry needing to run in main thread on the rust side, and that created issues of blocking python GUI when we initially started PyWry development🥲.
If you already create subprocess for that, why don't you use it as stand alone program just as webview cli controller?
it can get commands from stdin and output to stdout and you can simply run the binary
and what about running wry from main thread in Python in non blocking mode? is there a way?
🤦🏻♂️you're right and got me thinking why we kept the pyo3 extension if we weren't even using it anymore 🤣
So I went ahead and got rid of it and now we use pure compiled binary . Here's how to test
pip install pywry-nightly
And the updated pyinstaller spec file
# -*- mode: python ; coding: utf-8 -*- # noqa
import os
from shutil import which
import sys
from pathlib import Path
from PyInstaller.building.api import COLLECT, EXE, PYZ
from PyInstaller.building.build_main import Analysis
from PyInstaller.compat import is_darwin
NAME = "Executable Name" # Change this to the name of your executable
cwd_path = Path(os.getcwd()).resolve()
# Local python environment packages folder
venv_path = Path(sys.executable).parent.parent.resolve()
# Check if we are running in a conda environment
if is_darwin:
pathex = os.path.join(os.path.dirname(os.__file__), "site-packages")
elif "site-packages" in list(venv_path.iterdir()):
pathex = str(venv_path / "site-packages")
else:
pathex = str(venv_path / "lib" / "site-packages")
pathex = Path(pathex).resolve()
# Files that are explicitly pulled into the bundle
added_files = [
(str(cwd_path / "folder_path"), "folder_path"),
(which("pywry"), "."),
]
# Python libraries that are explicitly pulled into the bundle
hidden_imports = ["pywry"]
# Entry point
analysis_kwargs = dict(
scripts=[str(cwd_path / "main.py")],
pathex=[str(pathex), "."],
binaries=[],
datas=added_files,
hiddenimports=hidden_imports,
hooksconfig={},
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
a = Analysis(**analysis_kwargs)
pyz = PYZ(a.pure, a.zipped_data, cipher=analysis_kwargs["cipher"])
exe_args = [
pyz,
a.scripts,
[],
]
exe_kwargs = dict(
name=NAME,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
upx_exclude=[],
console=True,
disable_windowed_traceback=False,
target_arch="x86_64",
codesign_identity=None,
entitlements_file=None,
)
# Packaging settings
exe_kwargs["exclude_binaries"] = True
collect_args = [
a.binaries,
a.zipfiles,
a.datas,
]
collect_kwargs = dict(
strip=False,
upx=True,
upx_exclude=[],
name=NAME,
)
if is_darwin:
exe_kwargs["argv_emulation"] = True
exe = EXE(*exe_args, **exe_kwargs)
coll = COLLECT(
*([exe] + collect_args),
**collect_kwargs,
)
Thank you for the suggestion and wake up call haha! .🚀
Thanks. It's much simpler :)
If you want to make it easy to use with pyinstaller without this special spec file,
the way to do that is to make special PR to Pyinstaller repo, to add custom hook for pywry.
That's how another popular frameworks make it easy to bundle their apps using Pyinstaller
see this PR for example
I tested your nighly build, it works. the only thing which needs to be copied to the compiled python program is pywry.exe
Update
I created the hook:
hook-pywry.py
import ctypes
from os.path import join, exists
from PyInstaller.compat import is_win, getsitepackages
name = 'pywry.exe' if is_win else 'pywry'
binary = ctypes.util.find_library(name)
datas = []
if binary:
datas = [(binary, '.')]
else:
for sitepack in getsitepackages():
library = join(sitepack, 'lib', binary)
if exists(library):
datas = [(library, '.')]
if not datas:
raise Exception(binary + ' not found')
then just run
pyinstaller --onefile main.py --additional-hooks-dir=.