pywry icon indicating copy to clipboard operation
pywry copied to clipboard

pyinstaller doesn't work

Open thewh1teagle opened this issue 2 years ago • 9 comments

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

thewh1teagle avatar Aug 22 '23 02:08 thewh1teagle

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

alitrack avatar Aug 23 '23 06:08 alitrack

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?

thewh1teagle avatar Aug 23 '23 09:08 thewh1teagle

Works on macOS but fails on Windows I compiled and install on another windows, and tried send_html.py, a window flashed and disappeared.

alitrack avatar Aug 23 '23 12:08 alitrack

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!

tehcoderer avatar Aug 27 '23 18:08 tehcoderer

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

thewh1teagle avatar Aug 27 '23 19:08 thewh1teagle

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🥲.

tehcoderer avatar Aug 27 '23 21:08 tehcoderer

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?

thewh1teagle avatar Aug 27 '23 22:08 thewh1teagle

🤦🏻‍♂️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! .🚀

tehcoderer avatar Aug 28 '23 21:08 tehcoderer

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=.

thewh1teagle avatar Aug 28 '23 23:08 thewh1teagle