Pyinstaller で実行ファイル化する時にやったこと

# はじめに

いつも何やってたか忘れてしまうので、よくやるだろう部分をメモして、将来の自分のために残したい。

## Pyinstaller について

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

# 前提

こちらの「前提」項目は、以下の大項目をまとめていく上での前提状態の事で、こちらが変更されると、まとめられた項目には多少の変化が生じる。

## パッケージ構成

```python
project_root
    |- .venv # virtualenv 環境
    |- sample
    |    |- statics # 画像などの静的アセット
    |    |    |- app_icon.png
    |    |    |- app_icon.ico
    |    |    |- right_arrow.svg
    |    |    |- left_arrow.svg
    |    |- ui
    |    |    |- main.ui # QtCreator/QtDesigner で作成する UI ファイル
    |    |- __init__.py
    |    |- __main__.py
    |    |- gui.py
    |    |- module_b.py
    |- tests
    |- build.spec
    |- requirements.txt # 依存モジュールの記載
```

## 成果物

得たい成果物は次の様な構成。
今回は、sample ディレクトリ以下のパッケージを、Python 環境が無いユーザーも含めて配布する場合を考える。

```markdown
- sample.exe - こちらに sample 内のファイルをすべて回収してきて起動ファイルとして固めたもの。
- sample_noconsole.exe  - コンソール出力がないモード。GUI だけの挙動で十分であればこちら。
```

## 開発環境

> Windows 10 / 11
> Python 3.7 / 3.9 / 3.10

# 基本的な考え方

ここが結構重要なところ。

Pyinstaller で Python 実行環境を固める方法として、ぐぐると大体は単純な1コマンドで完了させているケースを多く見かける。単純なファイル構成だけのスクリプトであればこれで問題はない。

しかしながら、それだけでは効率的に静的アセットファイルなどを取り込めなかったり、実行ファイルのアイコンなどはデフォルトのままになってしまったりする。
また、一つのプロジェクト環境で、出力する実行ファイルを同時に複数作成したりしたい場合もあったりする。
これらをコマンドで指定するのもいいが、内容は変動することも多く、そのたびに長いコマンドを書き換えて実行するのも煩わしい上、他の誰かがそのプロジェクトフォルダを開いた際にビルド環境がどういったものなのかが分からなくなってしまう。

なので、**[スペックファイル (.spec)](https://pyinstaller.org/en/stable/spec-files.html) を作成し、それを編集したうえでビルドを実行する**という事が基本になる。
スペックファイルは、**上記手筈を簡略化したり、ビルド環境のドキュメント代わりになったりもする**。

# 1, ビルド環境作成

まずは Python の仮想環境を整える。
ここれは色んなやり方があるので、ぶっちゃけなんでもいい。
いつもは、virtualenv を使用しているので、今回はこちらのやり方でまとめてみる。

:::note info
仮想環境の作り方はいろいろあるが、他のツールだと OS の問題に起因するエラーに直面したり、仮想環境の実行ファイルが固定されなかったり、また、不要になったらプロジェクトフォルダごと削除してしまえばいいという発想に加え、Python のバッテリーインクルードの仮想環境構築手法という事もあって、結局 virtualenv を使いがち。
:::

仮想環境の作成し、

```powershell
python -m venv .venv
.venv\Scripts\activate.bat
```

依存モジュールをインストールする。

```powershell
pip install -r requirements.txt
```

# 2, スペックファイルの作成

## とりあえず、実行環境を作りながら作成する

まだ Pyinstaller の何たるかを分かっていない場合や、設定方法を忘れている場合は、とりあえずこちらを実行して、スペックファイルの作成を行う。

```powershell
pyinstaller .\sample\__main__.py --one-file
```

## スペックファイルのみを作成

ある程度 spec ファイルの作成に慣れて、最初に設定が漏れている状態で Pyinstaller を実行させ、実行ファイル等の作成を行わない場合は、こちらでスペックファイルの作成を先に行い、その作成されたスペックファイルを編集していく。

```powershell
pyi-makespec .\sample\__init__.py --onefile
```

こちらは引数を加える事によって、以下のさらな spec ファイルの内容を書き換えた状態で作成してくれる。
詳しくは `pyi-makespec --help` で参照。

:::note info
スペックファイルの名前は、`--one-file` で作成した場合はその .py ファイルの名前だったりが適応されているくらいなので特に決まりはない。
しかしながら、明示的に「このプロジェクトのビルドスペックファイルはこれだよ。」というのを分かりやすくするため、**build.spec** という名前で作成する様に心がけている。
:::

作成される __ main __.spec ファイル。これを **build.spec** ファイルに名前を変更する。
中身は実質 Python ファイル。

```python:build.spec
# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(
    ['sample\\__init__.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    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='__init__',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
coll = COLLECT(
    exe,
    a.binaries,
    a.zipfiles,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name='__init__',
)
```

# 3, スペックファイルの編集

上記コマンドを一度実行する事で、pysintaller で処理されるためのスペックファイル (.spec) が作成される。
これを編集することによって、作成される実行ファイルの名前や内包するバイナリファイルやエクストラファイルなどを含める事ができる。

## 実行ファイル名の編集

exe ファイルの場合。
EXE クラスの `name` 引数に渡す値を変更する。

```python
exe = EXE(
    # ...
    name="sample"
    # ...
)
```

## 追加ファイルの編集

テキストデータや、画像データ、PySide の場合 .ui ファイルなどの、Python ファイル以外のものをパッケージに含む場合に設定する。
こちらの設定を簡素化したい場合は、以下の事を守る。

* なるべく目的に分けたディレクトリ構成にする。
* なるべくパッケージに不必要なファイルを、検索対象にするディレクトリ化に配置しない。

以下、該当部の切り出し。

```python
project_root
    |- sample
    |    |- statics # 画像などの静的アセット
    |    |    |- app_icon.png
    |    |    |- app_icon.ico
    |    |    |- right_arrow.svg
    |    |    |- left_arrow.svg
    |    |- ui
    |    |    |- main.ui # QtCreator/QtDesigner で作成する UI ファイル
```

spec ファイルの編集を行う。

```python
add_datas = [
    ('sample/ui/*', 'sample/ui'),
    ('sample/statics/*', 'sample/statics')
]

a = Analyze(
    datas=add_datas
)
```

## アイコンの付与

実行ファイルに対してアイコンを付与したい場合。`icon` 引数にパス文字列を追加する。

```python
exe = EXE(
    # ...
    icon="sample/statics/app_icon.ico"
    # ...
)
```

## コンソール出力を行わない

GUI だけの起動（ユーザービリティ的にコンソール出力を敢えて見せない）や、別途ログ出力を行っている際など、コンソール出力が必要ない場合がある。その際に設定する項目。

Exe の `console` 引数を `False` に設定する。


```python
exe = EXE(
    # ...
    console=False
    # ...
)
```

## 出力項目を増やす

例えば、CLI と GUI を兼ね備えたツールだが、普段ユーザーにコンソール表示を出したくない場合もある。両方を満たすため、それぞれの実行ファイルを作成する場合は両方とも出力するような設定に書き換える必要がある。

```python:build.spec
block_cipher = None


a = Analysis(
    ['sample\\__main__.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    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,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='sample_noconsole',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)

console_exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='sample',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
```

これをビルドすることによって、dist ディレクトリに以下の二つが作成される。

* sample_noconsole.exe
* sample.exe


# 4, スペックファイルを使ってビルド

スペックファイルを元にビルドを実行する。

```powershell
pyinstaller build.spec
```

### VSCode の設定

VSCode のタスク設定を次の様にしておくことで、`Ctrl + B` でのビルド実行がすぐに行える。便利。

```json:task.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Pyinstaller: Build",
      "type": "shell",
      "windows": {
        "command": ".venv\\Scripts\\pyinstaller.exe .\\build.spec",
      },
      "problemMatcher": [],
      "group": {
        "kind": "build",
        "isDefault": true
      }
    },
    {
      "label": "Pytest",
      "type": "shell",
      "windows": {
        "command": ".venv\\Scripts\\pytest.exe ."
      },
      "problemMatcher": [],
      "group": {
        "kind": "test"
      }
    }
  ]
}
```

# まとめ

大体のことは「基本的な考え方」の項目に書いが、様は「（ビルド）スペックファイル」を編集することが結構重要になってくるよ。といった感じ。公式ドキュメントにもまだまだ書かれてることもあるので、そちらを参照しながら更新していきたい。