From 6257b498fec3e7ae111a1f3b3032c78fd9160608 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 6 Nov 2024 12:09:59 +0100 Subject: [PATCH] Add support for pyinstaller (#8) --- .github/workflows/ci.yml | 2 +- pyproject.toml | 2 +- rendercanvas/__pyinstaller/__init__.py | 12 +++++ rendercanvas/__pyinstaller/conftest.py | 1 + .../__pyinstaller/hook-rendercanvas.py | 22 +++++++++ .../__pyinstaller/test_rendercanvas.py | 47 +++++++++++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 rendercanvas/__pyinstaller/__init__.py create mode 100644 rendercanvas/__pyinstaller/conftest.py create mode 100644 rendercanvas/__pyinstaller/hook-rendercanvas.py create mode 100644 rendercanvas/__pyinstaller/test_rendercanvas.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e30f1fa..eeef250 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,7 +108,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -U . - pip install pytest pyinstaller + pip install pytest pyinstaller psutil glfw - name: Unit tests run: | pushd $HOME diff --git a/pyproject.toml b/pyproject.toml index 91e48b3..3f27d0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ glfw = ["glfw>=1.9"] lint = ["ruff", "pre-commit"] examples = ["numpy", "wgpu", "glfw", "pyside6"] docs = ["sphinx>7.2", "sphinx_rtd_theme", "sphinx-gallery"] -tests = ["pytest", "numpy", "psutil", "wgpu", "glfw"] +tests = ["pytest", "numpy", "wgpu", "glfw"] dev = ["rendercanvas[lint,tests,examples,docs]"] [project.entry-points."pyinstaller40"] diff --git a/rendercanvas/__pyinstaller/__init__.py b/rendercanvas/__pyinstaller/__init__.py new file mode 100644 index 0000000..c27432f --- /dev/null +++ b/rendercanvas/__pyinstaller/__init__.py @@ -0,0 +1,12 @@ +from os.path import dirname + + +HERE = dirname(__file__) + + +def get_hook_dirs(): + return [HERE] + + +def get_test_dirs(): + return [HERE] diff --git a/rendercanvas/__pyinstaller/conftest.py b/rendercanvas/__pyinstaller/conftest.py new file mode 100644 index 0000000..1fb31cf --- /dev/null +++ b/rendercanvas/__pyinstaller/conftest.py @@ -0,0 +1 @@ +from PyInstaller.utils.conftest import * # noqa: F403 diff --git a/rendercanvas/__pyinstaller/hook-rendercanvas.py b/rendercanvas/__pyinstaller/hook-rendercanvas.py new file mode 100644 index 0000000..04579aa --- /dev/null +++ b/rendercanvas/__pyinstaller/hook-rendercanvas.py @@ -0,0 +1,22 @@ +# ruff: noqa: N999 + +from PyInstaller.utils.hooks import collect_dynamic_libs + +# Init variables that PyInstaller will pick up. +hiddenimports = [] +datas = [] +binaries = [] + +# Add modules that are safe to add, i.e. don't pull in dependencies that we don't want. +hiddenimports += ["rendercanvas.offscreen"] + +# Since glfw does not have a hook like this, it does not include the glfw binary +# when freezing. We can solve this with the code below. Makes the binary a bit +# larger, but only marginally (less than 300kb). +try: + import glfw # noqa: F401 +except ImportError: + pass +else: + hiddenimports += ["rendercanvas.glfw"] + binaries += collect_dynamic_libs("glfw") diff --git a/rendercanvas/__pyinstaller/test_rendercanvas.py b/rendercanvas/__pyinstaller/test_rendercanvas.py new file mode 100644 index 0000000..308c685 --- /dev/null +++ b/rendercanvas/__pyinstaller/test_rendercanvas.py @@ -0,0 +1,47 @@ +script = """ +# The script part +import sys +import importlib + +from rendercanvas.auto import RenderCanvas + +if "glfw" not in RenderCanvas.__name__.lower(): + raise RuntimeError(f"Expected a glfw canvas, got {RenderCanvas.__name__}") + +# The test part +if "is_test" in sys.argv: + included_modules = [ + "rendercanvas.glfw", + "rendercanvas.offscreen", + "glfw", + ] + excluded_modules = [ + "PySide6.QtGui", + "PyQt6.QtGui", + ] + for module_name in included_modules: + importlib.import_module(module_name) + for module_name in excluded_modules: + try: + importlib.import_module(module_name) + except ModuleNotFoundError: + continue + raise RuntimeError(module_name + " is not supposed to be importable.") +""" + + +def test_pyi_rendercanvas(pyi_builder): + pyi_builder.test_source(script, app_args=["is_test"]) + + +# We could also test the script below, but it's not that interesting since it uses direct imports. +# To safe CI-time we don't actively test it. +script_qt = """ +import sys +import importlib + +import PySide6 +from rendercanvas.qt import RenderCanvas + +assert "qt" in RenderCanvas.__name__.lower() +"""