Search code examples
pythonpython-3.xpyinstaller

Pyinstaller ignores dll which is part of a dependency


I am creating a slim Python project which is wrapping a native dll, so that other Python developers can use the functionality of this dll without bothering about C-internals. This project can be build into a wheel, it can be pip installed, and it can be successfully used within scripts. However, when bundled with pyinstaller, the dll is ignored, so the bundled program crashes.

I know there is a workaround by explicitly telling pyinstaller to include the dll: pyinstaller --add-binary="source/path/mylib.dll:destination/path ./main.py. However, this somewhat defies the purpose of such a wrapper which is to obfuscate the dll's existence to the user. I would like to know if there is a way to improve my wrapper's project structure so that pyinstaller will find the dll automatically.


This is how my wrapper project is structured:

wrapper
|-- wrapper
|   |-- __init__.py
|   |-- mylib.dll
|-- pyproject.toml

pyproject.toml:

[tool.poetry]
name = "wrapper"
version = "0.1.0"
description = ""
authors = []

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

__init__.py:

import ctypes
import importlib.resources as res

lib = None
with res.path("wrapper", "mylib.dll") as dll_path:
  lib = ctypes.CDLL(str(dll_path))

# now defining several python functions accessing lib

As mentioned above, this project can be successfully build to a wheel by running poetry build, and it also can be installed by pip install <wheel name>. In both cases, the dll gets recognized and copied to the proper direction (like e.g. site-packages/wrapper). Having it installed, the following script runs successfully:

main.py:

import wrapper

# calling arbitrary functions from the wrapper module

However, when running pyinstaller ./main.py, the library mylib.dll is ignored, and the bundled executable crashes with a PyInstallerImportError.


Added later: Running pyinstaller --collect-binaries wrapper .\main.py actually works fine. This is a solution which isn't too bad for my taste. As a library developer however, I would still prefer a solution which does not require users to set nonstandard flags to be able to use my library. So I am looking forward for better ideas.


Solution

  • The PyInstaller team was leading me to PyInstaller hooks, and this is exactly what I needed. There is a very general example demonstrating many features of those hooks. In my case, I needed to change my project structure in the following way:

    wrapper
    |-- wrapper
    |   |-- __init__.py
    |   |-- mylib.dll
    |   |-- __pyinstaller
    |       |-- __init__.py
    |       |-- hook-wrapper.py
    |-- pyproject.toml
    

    The newly added files look like that:

    __init__.py:

    import os
    
    def get_hook_dirs():
        return [os.path.dirname(__file__)]
    

    hook-wrapper.py:

    from PyInstaller.utils.hooks import collect_data_files
    datas = collect_data_files('wrapper', excludes=['__pyinstaller'])
    

    I also had to add one extra section to my pyproject.toml:

    [tool.poetry]
    name = "wrapper"
    version = "0.1.0"
    description = ""
    authors = []
    
    [build-system]
    requires = ["poetry-core"]
    build-backend = "poetry.core.masonry.api"
    
    [tool.poetry.plugins.pyinstaller40]
    hook-dirs = "wrapper.__pyinstaller:get_hook_dirs"
    

    Now any script using my wrapper library can be bundled just by running pyinstaller <script name>.