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

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

---

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

```:exeファイル実行時のエラー
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://github.com/pygame/pygame/tree/main/src_py/__pyinstaller)

# ディレクトリ構造

ということで、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ディレクトリに配置。

```python:__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ファイルを作ります。
```python:__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に伝えます。
```toml:pyproject.toml
...

[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