tlambert03 / napari-pyinstaller

minimal example of bundling napari with pyinstaller
BSD 3-Clause "New" or "Revised" License
0 stars 1 forks source link

Created app fails with <No module named 'napari._event_loop'> #2

Open cudmore opened 2 years ago

cudmore commented 2 years ago

Trying to get this to work. For my users I really need to hand them a working app. They can not deal with a command line (not today, not ever).

My build system:

Still trying to figure out how to set up an easy to use Python installation on the M1 (arm64) chip. I currently can't get PyQt5 to install via pip so need to use conda.

Here are my steps to creating a conda env

conda create -y -n napari-env python=3.9
conda activate napari-env    

# pip install PyQt5 fails on arm64 architecture
# need to use conda and for some reason pyqt rather than PyQt5?
conda install pyqt

# manually remove PyQt5 from requirements.txt
pip install -r requirements.txt

chmod a+x ./build.sh
./build.sh

When I run the napari-app.app, I get this?

Last login: Fri Jun 10 15:53:05 on ttys003
/Users/cudmore/Desktop/napari-app.app/Contents/MacOS/napari-app ; exit;
(base) cudmore@Roberts-Mac-Studio ~ % /Users/cudmore/Desktop/napari-app.app/Contents/MacOS/napari-app ; exit;
Traceback (most recent call last):
  File "main.py", line 3, in <module>
  File "napari/__main__.py", line 446, in main
  File "napari/__main__.py", line 228, in _run
  File "napari/_lazy.py", line 47, in __getattr__
  File "importlib/__init__.py", line 127, in import_module
ModuleNotFoundError: No module named 'napari._event_loop'
[31031] Failed to execute script 'main' due to unhandled exception: No module named 'napari._event_loop'
[31031] Traceback:
Traceback (most recent call last):
  File "main.py", line 3, in <module>
  File "napari/__main__.py", line 446, in main
  File "napari/__main__.py", line 228, in _run
  File "napari/_lazy.py", line 47, in __getattr__
  File "importlib/__init__.py", line 127, in import_module
ModuleNotFoundError: No module named 'napari._event_loop'

Saving session...
...copying shared history...
...saving history...truncating history files...
...completed.
Deleting expired sessions...none found.
tlambert03 commented 2 years ago

For my users I really need to hand them a working app.

just to confirm, you can't use the bundles here right? https://github.com/napari/napari/releases (and if so, curious why not)

ModuleNotFoundError: No module named 'napari._event_loop'

Pyinstaller definitely takes some experimentation to get right, and the biggest issue is hidden imports. If you're committed to using pyinsatller, and definitely can't use our bundle. Then read through the pyinstaller docs, specifically this page: https://pyinstaller.org/en/stable/when-things-go-wrong.html, (and this section). I can't give you an immediate answer, since it just took a lot of trial and error for me in the first place.

You'll likely need to modify the hook_napari.py file...

but ultimately, the bundled app we intend to provide support for is the ones on our release page, not this one.

cudmore commented 2 years ago

Thanks for your always prompt and thorough response!

My motive here is I want to create a bundled installer for a desktop app that uses Napari as a viewer. With this I can rapidly extend Napari functionality while I wait for my proper plugins to mature.

On the flip-side, I am actively writing proper plugins for Napari. AS these become mature, my users can use the awesome prebuilt Napari installers.

Can I get a link to a description of how Napari is building it's one-click install bundles? Would love to add that to my work-flow.

tlambert03 commented 2 years ago

we have two methods. one based on briefcase: https://github.com/napari/napari/blob/main/bundle.py and one based on conda: https://github.com/napari/napari/blob/main/bundle_conda.py

you can see how they are used on GitHub actions here: https://github.com/napari/napari/blob/main/.github/workflows/make_bundle.yml and here: https://github.com/napari/napari/blob/main/.github/workflows/make_bundle_conda.yml

Modjular commented 4 months ago

For posterity, here's a --onefile spec that worked for me; part of the solution was to include napari._event_loop in the hiddenimports list

python: 3.10
napari: 0.4.19
pyinstaller: 6.8.0
# -*- mode: python ; coding: utf-8 -*-
import sys
from PyInstaller.building.api import COLLECT, EXE, MERGE, PYZ
from PyInstaller.building.build_main import Analysis

import napari

sys.modules["FixTk"] = None

NAME = "my-napari-app"
WINDOWED = True
DEBUG = True
UPX = False

def get_icon():
    logo_file = "logo.ico" if sys.platform.startswith("win") else "logo.icns"
    return logo_file

def get_version():
    if sys.platform != "win32":
        return None

    from PyInstaller.utils.win32 import versioninfo as vi

    ver_str = napari.__version__
    version = ver_str.replace("+", ".").split(".")
    version = [int(x) for x in version if x.isnumeric()]
    version += [0] * (4 - len(version))
    version = tuple(version)[:4]
    return vi.VSVersionInfo(
        ffi=vi.FixedFileInfo(filevers=version, prodvers=version),
        kids=[
            vi.StringFileInfo(
                [
                    vi.StringTable(
                        "000004b0",
                        [
                            vi.StringStruct("CompanyName", NAME),
                            vi.StringStruct("FileDescription", NAME),
                            vi.StringStruct("FileVersion", ver_str),
                            vi.StringStruct("LegalCopyright", ""),
                            vi.StringStruct("OriginalFileName", NAME + ".exe"),
                            vi.StringStruct("ProductName", NAME),
                            vi.StringStruct("ProductVersion", ver_str),
                        ],
                    )
                ]
            ),
            vi.VarFileInfo([vi.VarStruct("Translation", [0, 1200])]),
        ],
    )

a = Analysis(
    ["main.py"],
    hookspath=["hooks"],
    hiddenimports=[
        'napari._event_loop',
    ],
    excludes=[
        "PyQt5",
        # "PyQt6",
        "FixTk",
        "tcl",
        "tk",
        "_tkinter",
        "tkinter",
        "Tkinter",
        "matplotlib",
    ],
)

pyz = PYZ(a.pure)

## --onefile
exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name=NAME,
    debug=DEBUG,
    upx=UPX,
    icon=get_icon(),
    version=get_version(),
    bootloader_ignore_signals=False,
    strip=False,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=(not WINDOWED),
    disable_windowed_traceback=False,
    argv_emulation=True,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)