ビルドした自作のpythonパッケージをPyInstallerに対応させよう

qlog公式からのメッセージ:ログインをしていなくてもいいねを押すことができます!

(動的インポートや画像ファイルなどを使用せず、モジュール(.pyファイル)のみで動かせるパッケージの場合は本記事の設定は必要ありません)


自分用にpygameを使ったゲームフレームワークのpythonパッケージを作っている https://github.com/Eleven-junichi2/auraboros のですが、
PyInstallerでそのパッケージを使ったスクリプトのWindows環境の実行ファイル化に躓いたので、解決に至るまでを共有します。

PyInstaller --windowed 実行ファイル化するスクリプト.py

Traceback (most recent call last):
  File "実行ファイル化するスクリプト.py", line 1, in <module>
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "PyInstaller\loader\pyimod02_importers.py", line 352, in exec_module
  File "auraboros\engine.py", line 9, in <module>
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "PyInstaller\loader\pyimod02_importers.py", line 352, in exec_module
  File "auraboros\shader.py", line 12, in <module>
FileNotFoundError: [Errno 2] No such file or directory: '~\\...\\スクリプトのディレクトリ\\dist\\実行ファイルを置くディレクトリの名前\\auraboros\\default.vert <- パッケージが使用しているfile

Tracebackからわかること

  • インストールしたパッケージのモジュール(pyファイル)自体は読み込めている
  • どうやら、PyInstallerがdefault.vertを見つけられていない?
  • site-packagesを見ると、きちんとpipインストールしたパッケージにはdefault.vertは含まれているので、パッケージのビルド時の不具合ではない

原因と解決策

https://pyinstaller.org/en/stable/hooks.html

ドキュメントによると、どうやらパッケージにモジュール以外の画像等のファイルを含めたい場合、hookファイルで指定しないとPyInstallerが実行ファイル化時にそのファイルを同梱してくれない模様。

例えばpygameのリポジトリを見てみると、実際に__pyinstaller/hook-pygame.pyを設定してPyInstallerにフォントファイルやdllを同梱させるようになっています。

ディレクトリ構造

ということで、https://pyinstaller-sample-hook.readthedocs.io/en/latest/ を参考に必要な設定をしてみます。

作っているパッケージはsrcレイアウトを採用していて、以下のようなディレクトリ構造。
ビルドに必要なメタデータはpyproject.tomlで管理する方式です。

プロジェクトのrootディレクトリ
├─src
│  └─auraboros
│      ├─__pyinstaller *
│      │ ├─___init__.py *
│      │ └─hook-auraboros.py *
│      │ (その他パッケージ本体の.pyファイル)
│      ├─default.vert <-今回含めたいファイル
├─pyproject.toml *
(説明に必要な部分のみ抜粋)

*が更新・新しく作成するファイル・ディレクトリ。


hookファイルを作成する

PyInstallerは実行ファイルを作成する際、pyproject.tomlやsetup.cfg(setup.py)で設定をしておくことで、"site-packages/必要な依存関係のパッケージのディレクトリ"を見に行って、そこに含まれているhook- .pyファイルを参照します。
hook-
.pyファイルに処理を記述しておくことで、実行ファイル化の際必要なファイルを同梱してくれるので、今回は以下の用に記述し、サンプルに習って作成した__pyinstallerディレクトリに配置。

```__pyinstaller/hook-auraboros.py
from PyInstaller.utils.hooks import collect_data_files

collect_data_file()でパッケージ内のファイルのパスのリストを作成し、

datas変数でPyInstallerに知らせる

datas = collect_data_files('auraboros', excludes=['__pyinstaller'])

第一引数にプロジェクトのパッケージ名、excludesに含めたくないディレクトリ・ファイルを記述


また、PyInstallerがhookファイルの場所を見つけられるように、同じく__pyinstallerディレクトリに__init__.pyファイルを作ります。
```__pyinstaller/__init__.py
import os

# hookファイルのディレクトリのリストを返し、hookファイルの場所をPyInstallerに知らせます
# 今回はhookファイルを__pyinstaller/__init__.pyと同じディレクトリに置いてい
# るので、 __init__.pyファイル自身の親ディレクトリを返す
def get_hook_dirs():
    return [os.path.dirname(__file__)]

pyproject.tomlを設定

(メタデータにsetup.cfg, setup.pyを使っている場合でも対応した形で記述して設定できます)

エントリポイントを設定して、pyinstaller/ __init .pyのget_hook_dirs()をPyInstallerに伝えます。

...

[project.entry-points.pyinstaller40]
hook-dirs = "auraboros.__pyinstaller:get_hook_dirs"

これでhookファイルの設定は完了です。後は通常通りパッケージをビルドしましょう。
ビルドしたwhlを実際にインストールして、パッケージをimportしたスクリプトを実行ファイル化する際の出力に

INFO: Loading module hook '(作成したhookファイルの名前).py'

の表示があれば、きちんとhookファイルがPyInstallerに認識されています。

その他の参考

https://hatch.pypa.io/latest/config/metadata/#entry-points
https://github.com/pypa/packaging-problems/issues/634
https://github.com/MatthewRalston/kmerdb/blob/master/pyproject.toml
https://github.com/pyinstaller/pyinstaller-hooks-contrib
https://github.com/pyinstaller/pyinstaller/issues/2294
https://github.com/jmoiron/humanize/issues/105


elevenjunichi2