Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pyinstaller doesn't work #83

Open
thewh1teagle opened this issue Aug 22, 2023 · 9 comments
Open

pyinstaller doesn't work #83

thewh1teagle opened this issue Aug 22, 2023 · 9 comments

Comments

@thewh1teagle
Copy link

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

@alitrack
Copy link

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

@thewh1teagle
Copy link
Author

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?

@alitrack
Copy link

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

@tehcoderer
Copy link
Collaborator

tehcoderer commented Aug 27, 2023

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!

@thewh1teagle
Copy link
Author

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

@tehcoderer
Copy link
Collaborator

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

@thewh1teagle
Copy link
Author

thewh1teagle commented Aug 27, 2023

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?

@tehcoderer
Copy link
Collaborator

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

@thewh1teagle
Copy link
Author

thewh1teagle commented Aug 28, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants