enaml icon indicating copy to clipboard operation
enaml copied to clipboard

How do you package/distribute an enaml application?

Open nicksspirit opened this issue 3 years ago • 9 comments

I built a simple app using enaml called date range chunker. I want to share it with others who won't have python installed in their system.

What is the recommended approach to packaging/distributing my application to people using windows. I tried to make use of pyinstaller but every time I try to start the app it fails. It is not able to execute the dynamic import of the .enaml files via the enaml.imports() context manager.

nicksspirit avatar Sep 12 '20 23:09 nicksspirit

This is not something I ever investigated myself. The topic was discussed a couple of times on the mailing list though. In particular one person provided a build script for pyinstaller listing a number of hidden imports he needed to add to get things working. It may help, note however that it was quite some time ago so some imports may not be correct anymore due to some internal reorganization.

https://github.com/jminardi/syncnet/blob/master/build_app.sh

MatthieuDartiailh avatar Sep 14 '20 02:09 MatthieuDartiailh

So I tried to follow the code in the build_app.sh. I noticed that the script was targeted for MacOS but I am running windows.

Either way I tried to add the lines regarding hidden import

...
PACKAGE_NAME = "enaml_demo"
EXECUTABLE_NAME = "person"
ENTRYPOINT = BASE_DIR / PACKAGE_NAME / "person.py"
ASSETS = BASE_DIR / PACKAGE_NAME / "assets"


def resolve(s: Path) -> str:
    return str(s.resolve())


def build():
    pyinstaller = import_module("PyInstaller.__main__")

    pyinstaller.run(
        [
            "-y",
            f"--name={EXECUTABLE_NAME}",
            "--onedir",
            '--console',
            "--clean",
            '--hidden-import="enaml.core.parse_tab.lextab"',
            '--hidden-import="enaml.core.compiler_helpers"',
            '--hidden-import="enaml.core.compiler_nodes"',
            '--hidden-import="enaml.core.enamldef_meta"',
            '--hidden-import="enaml.core.template"',
            '--hidden-import="enaml.widgets.api"',
            '--hidden-import="enaml.widgets.form"',
            '--hidden-import="enaml.layout.api"',
            resolve(ENTRYPOINT),
        ]
    )
...

But still no success, once the app is built it won't run because of the import of the enaml file.

nicksspirit avatar Sep 14 '20 22:09 nicksspirit

Could you share the kind of error you get ?

MatthieuDartiailh avatar Sep 14 '20 22:09 MatthieuDartiailh

Looking briefly through PyInstaller docs, I feel like you need to help PyInstaller realize it needs to package the enaml files (as data files).

MatthieuDartiailh avatar Sep 14 '20 22:09 MatthieuDartiailh

But data files are not imported into python via the import system. They are usually accessed through some IO method like open()

nicksspirit avatar Sep 14 '20 22:09 nicksspirit

Yeah but .enaml files cannot be identified by pyinstaller as being needed since for an import it is only gonna look for a matching .py which does not exist. In that respect .enaml files are data files. We hook into the Python import system to load them actually.

MatthieuDartiailh avatar Sep 14 '20 22:09 MatthieuDartiailh

Using the following I was able to package the employee example:

pyinstaller employee/employee.py --hiddenimport "enaml.core.compiler_helpers" --hiddenimport "atom.api" --add-data employee/employee_view.enaml:. --hiddenimport "enaml.layout.api" --hiddenimport "enaml.core.enamldef_meta" --hiddenimport "enaml.widgets.api" --hiddenimport "phone_validator"

It is ugly and we could do much better, but it is a start.

MatthieuDartiailh avatar Sep 14 '20 23:09 MatthieuDartiailh

To do a better job, we will need a fairly complex hook to inspect all the .enaml files of the packaged application to find missing imports, to package the lextab and parsetab for the relevant Python version for enaml, to package all the relevant enaml files and if they exists the matching enamlc files (the hook may want to force the compilation of enaml files to be able to bundle the enamlc file to avoid the parsing/compilation cost each time the app is run). Based on PyInstaller recommendation enaml could distribute its own hook. I would be happy to do so but may not have much time to work on the hook itself. Is it something you would have time/be interested in working on @OdinTech3 (once we get your use case working)?

MatthieuDartiailh avatar Sep 14 '20 23:09 MatthieuDartiailh

I was able to get this working by creating a special hook for Enaml. It's a bit crude, but it does make sure the *.enaml files get included as data. Future revisions should probably include the *.enamlc files instead (not sure if the *.enaml files are needed as well). However, I won't have further time to explore this for a bit. Here's the hook code:

from PyInstaller.utils.hooks import collect_data_files
datas = collect_data_files('enaml', True)

Really simple and it works with PyInstaller. See the full repository at https://github.com/NCRAR/ncrar-abr-installer for the process to create a stand-alone installer for a program. For example, see how to include the hooks in your spec file for PyInstaller.

bburan avatar Sep 07 '23 14:09 bburan