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