dashで作成したアプリをpyinstallerでexe化する

# はじめに

dashで作成したアプリをpyinstallerを使ってPythonファイルをexe化しようとしたとき、画像リソースや依存パッケージが読み込まれなかったため手動でspecファイルを編集したときの備忘録です。

#### dashとは・・・
Pythonだけで良い感じにかっこいいダッシュボードが作れるライブラリです。
プロトタイプ開発には非常に適していると思います。
[Dash Enterprise App Gallery](https://dash-gallery.plotly.host/Portal/)

サーバーを公開して使用してもらうのが一般的な使い方ですが、今回はテスト環境をプログラマではない人に配布する目的でexe化しました。


# 手順

## step1. minimumな仮想環境を作成する

pyinstallerは導入しているライブラリをすべて合わせてexe化しようとするので最初に環境をキレイにします。導入するライブラリはアプリで使用する最低限のものにしてします。ここでいらないライブラリまで仮想環境に含めるとアプリの容量が無駄に大きくなります。

このとき、仮想環境はアプリが置かれたディレクトリと同じ階層に作成しておくとspecファイル内部で相対ファイルパス指定が楽にできます。

```
python3 -m venv python37
```

```:windows
.\python37\Scripts\activate
```

```:mac
. python37/bin/activate
```


## step2. 必要なライブラリ及びpyinstallerをインストールする

作成した仮想環境に必要なライブラリ及びpyinstallerをインストールします。

```
pip install 必要なライブラリ
pip install pyinstaller
```

## step3. specファイルを作成する

dashアプリの起動ファイルとなるapp.pyと同じ階層にspecファイルを用意します。
pyinstallerにコマンドライン引数としてspecファイルを渡すため、名前はなんでも良いです。（仮にdashapp.specとします。）

> `pyinstaller -D app.py`を実行することにより、たたき台となるspecファイルを自動的に生成することもできます。
ただしdashアプリの場合は依存モジュールの設定が自動では含まれないためbuildには失敗します。
(-Dオプションはonedirモードでexeを作成する場合に指定する。onefileモードであれば不要。）

``` :dashapp.spec
block_cipher = None

a = Analysis(['app.py'],
             pathex=['.'],
             binaries=[],
             datas=[
                 ('./image','image'),
                 ('./python37/lib/python3.7/site-packages/dash_core_components', 'dash_core_components'),
                 ('./python37/lib/python3.7/site-packages/dash_html_components', 'dash_html_components'),
                 ('./python37/lib/python3.7/site-packages/dash_renderer', 'dash_renderer'),
                 ('./python37/lib/python3.7/site-packages/dash_table', 'dash_table'),
                 ('./python37/lib/python3.7/site-packages/dash_bootstrap_components', 'dash_bootstrap_components'),
                 ('./python37/lib/python3.7/site-packages/plotly', 'plotly'),
			 ],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='dashapp',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='dashapp')
# OSX用
# app = BUNDLE(coll,
#          name='dashapp.app',
#          icon=None,
#          bundle_identifier=None)
```

**ポイント**
前述したとおり、`pyinstaller -D app.py`でexeを作成すると不足しているモジュールがあると怒られます。
自動的にパスを取得できない依存ライブラリがある旨のエラーが出力されるため、これらを手動でdatasに記述していきます。
step2で作成した仮想環境に不足しているライブラリが存在するため、相対パスで指定します。

私の環境で不足していたライブラリは以下でした。
- dash_core_components
- dash_html_components
- dash_renderer
- dash_table
- dash_bootstrap_components
- plotly

また、画像ファイル等、リソースファイルも手動で追加する必要があります。
画像ファイルを配置しているディレクトリごと相対パスで指定します。

## 4. pyinstallerを実行する

```python
pyinstaller dashapp.spec
```

specファイルが適切に作成されていれば、distファイル下にdashappフォルダが出来上がります。
windows環境の場合はdashappフォルダ下にexeファイルが存在します。

Mac環境の場合は`app = BUNDLE...`以下のコメントを外して実行してください。
dashapp.appが出来上がります。 

## 余談
### onedirモードで実行する理由

ネットで検索するとonefileモードで実行している人が多いです。onefileモードはすべてのバイナリをexeでパッケージ化して使用するモードで、1ファイルにまとまるため見た目がすっきりしますが起動時にすべてのバイナリをtempファイルに展開するため起動が非常に遅くなります。
特にdashアプリは使用しているライブラリの数も多いため、onefileモードは不向きなように思います。

補足ですが、onefileモードで作成する場合は`exclude_binaries=True,`あたりの記述が変わるため、一度`pyinstaller app.py`でspecファイルを作成し直してから編集すると良いと思います。

### 実行ファイルのカレントディレクトリ

exe化したファイルを直接実行した場合、Windows環境とMac環境では実行時のカレントディレクトリが異なるため注意が必要です。

- Windowsはexeを実行すると、exeファイルが置かれているフォルダがカレントになります。
- Macは出力されたUnix実行ファイル(.app)を実行すると、HOMEディレクトリがカレントになります。

クロスプラットフォーム対応をする場合は気を付けてください。

# 参考URL
- [公式ページ](https://pyinstaller.readthedocs.io/en/stable/spec-files.html)
- [不足しているファイルをdatasとして追加する](https://stackoverflow.com/questions/59914106/pyinstaller-error-when-executing-plotly-dash-exec-file)
- [MacのカレントディレクトリがHOMEになる問題の解決策](https://stackoverflow.com/questions/54837659/python-pyinstaller-on-mac-current-directory-problem)