gradioを使用したPythonアプリをpyinstallerで実行ファイル化する手順

# gradio
[gradio](https://www.gradio.app/)というPythonモジュールがあります。

これを利用すると、AI Webアプリを簡単に実装でき、WebブラウザをフロントエンドにするローカルAIアプリも容易に実装できます。実際、[Stable Diffusion Web UI AUTOMATIC1111](https://github.com/AUTOMATIC1111/stable-diffusion-webui)などさまざまなAIアプリで活用されています。

![AUTOMATIC1111の例](https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/master/screenshot.png?raw=true)

gradioを利用する場合、Webアプリなら開発者が[Hugging Face spaces](https://huggingface.co/spaces)等のサイトでアプリを構築すればユーザは簡単に利用できます。

ですが、Pythonアプリを配布して、ローカルでPythonアプリを実行させたい場合、ユーザーにPython本体をインストールさせた上でアプリを実行させる必要があります。コマンドラインに慣れている多くのLinuxユーザは別ですが、一般的なWindowsユーザやmacOSユーザにとっては使いやすくはないでしょう。

Pythonにはスクリプトを実行ファイルにするアプリやモジュールがいくつかあります。その一つ[Pyinstaller](https://pyinstaller.org/en/stable/)を使うと1ファイルの実行ファイルにまとめることができます。

ただ、gradioを利用したPythonスクリプトは単にコマンドを実行するだけでは実行ファイル化できなかったので、[こちら](https://github.com/pyinstaller/pyinstaller/issues/8108)や[こちら](https://x.com/tori29umai/status/1923215729698144632)を参考に、1ファイル化する具体的な手順を解説します。また、macOSではPyinstaller(またはgradio)にバグがあり、うまく動かない場合があるので、代替手法も説明します。

## Pyinstallerでの1ファイル化

### サンプルアプリ作成
[Python公式サイト](https://www.python.org/)のなどを参照し、Pythonをインストールして実行できるようにしておいてください。

適当な作業用フォルダに移動しvenvで仮想環境を構築します(pythonではなくpython3の場合もあります)。

```
python -m venv venv
```

仮想環境を有効にします。

#### Windowsの場合
```
.\venv\Scripts\Activate.ps1  
```

#### macOSやLinuxの場合
```
. venv/bin/activate
```

gradioを`pip`でインストールします(`pip`ではなく`pip3`の場合もあります)。

```
pip install gradio
```


gradioのサイトのトップの例を自動でブラウザが立ち上がるよう修正したスクリプトを作成します。次の内容の`test.py`を適当なエディタで作成してください。

```
import gradio as gr

def greet(name):
    return "Hello " + name + "!"

demo = gr.Interface(fn=greet, inputs="text", outputs="text")
demo.launch(inbrowser=True)   
```

動作確認します。

```
python test.py
```


自動的にブラウザが起動します。

![tmp1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/882900/c9f3c88c-8f7e-498b-a086-3fb36867aa4a.png)

name欄に名前を入れて送信ボタンを押せば、outputにメッセージが表示されます。

![tmp2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/882900/0034b729-c601-4da5-b71b-632d241e8d0b.png)

一旦終了してください。

### Pyinstallerによる実行ファイル化
Pyinstallerを`pip`でインストールします。

```
pip install pyinstaller
```

`pyinstaller`で1ファイルとして実行ファイル化してみます。

```
pyinstaller --onefile test.py
```

distフォルダ以下に`test.exe`(Windowsの場合)または`test`(macOSやLinux場合)が生成されるので、一見正しく動作しているように見えるのですが、実行してみるとうまく動きません。

#### Windowsの場合

```
dist\test.exe
...
  File "gradio_client\serializing.py", line 16, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\asfdwe\\AppData\\Local\\Temp\\_MEI127082\\gradio_client\\types.json'
[PYI-5196:ERROR] Failed to execute script 'test' due to unhandled exception!
```

#### macOSの場合
```
dist/test
...
  File "gradio_client/serializing.py", line 16, in <module>
FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/lc/q4l6040s4n709pxrsg3jx7d40000gp/T/_MEI0cSdr3/gradio_client/types.json'
[PYI-14006:ERROR] Failed to execute script 'test' due to unhandled exception!
```

#### Linuxの場合
```
dist/test
...
  File "gradio_client/serializing.py", line 16, in <module>
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/_MEIrMnl9c/gradio_client/types.json'
[PYI-358283:ERROR] Failed to execute script 'test' due to unhandled exception!
```

エラーの原因は`gradio_client`モジュールのファイル不足のようです。

`--collect-data` で`gradio_client`を指定します。もう一度`pyinstaller`を実行します。

```
pyinstaller --onefile --collect-data gradio_client test.py
```

```
dist\test.exe
...
  File "safehttpx\__init__.py", line 12, in get_version
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\asfdrwe\\AppData\\Local\\Temp\\_MEI74442\\safehttpx\\version.txt'
[PYI-3580:ERROR] Failed to execute script 'test' due to unhandled exception!
```

`safehttpx`モジュールのファイルが足らないので追加してもう一度`pyinstaller`を実行します。

```
pyinstaller --onefile --collect-data gradio_client --collect-data safehttpx test.py
```

```
dist\test.exe
...
  File "groovy\__init__.py", line 5, in <module>
  File "pathlib.py", line 1134, in read_text
  File "pathlib.py", line 1119, in open
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\asfdrwe\\AppData\\Local\\Temp\\_MEI146082\\groovy\\version.txt'
[PYI-6776:ERROR] Failed to execute script 'test' due to unhandled exception!
```

`groovy`モジュールを追加します。

```
pyinstaller --onefile --collect-data gradio_client --collect-data safehttpx --collect-data groovy  test.py
```

```
dist\test.exe
...
  File "gradio\component_meta.py", line 111, in create_or_modify_pyi
  File "pathlib.py", line 1134, in read_text
  File "pathlib.py", line 1119, in open
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\asfdrwe\\AppData\\Local\\Temp\\_MEI81842\\gradio\\blocks_events.pyc'
[PYI-3820:ERROR] Failed to execute script 'test' due to unhandled exception!
```

まだダメです。`gradio`モジュールを追加します。

```
pyinstaller --onefile --collect-data gradio_client --collect-data safehttpx --collect-data groovy --collect-data gradio test.py
```

```
dist\test.exe
...
  File "gradio\blocks_events.py", line 20, in __new__
  File "gradio\component_meta.py", line 111, in create_or_modify_pyi
  File "pathlib.py", line 1134, in read_text
  File "pathlib.py", line 1119, in open
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\asfdrwe\\AppData\\Local\\Temp\\_MEI38162\\gradio\\blocks_events.pyc'
[PYI-11504:ERROR] Failed to execute script 'test' due to unhandled exception!
```

`blocks_events.pyc`の扱いが問題のようです。specファイルを編集してgradio内のファイルの扱いを変更します。

`pyinstaller`はpythonスクリプトを解析し、specファイルを出力し、specファイル内容に基づいて実行ファイル化します。`pyi-makespec`コマンドで一旦specファイルを出力させたあと、specファイルを修正した上で、specファイルに基づいて実行ファイル化するようにします。

`pyi-makespec`コマンドを`pyinstaller`を同じ引数で実行します。

```
pyi-makespec --onefile --collect-data gradio_client --collect-data safehttpx --collect-data groovy --collect-data gradio test.py
```

生成された`test.spec`をテキストエディタで開いてください。

```
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_data_files

datas = []
datas += collect_data_files('gradio_client')
datas += collect_data_files('safehttpx')
datas += collect_data_files('groovy')
datas += collect_data_files('gradio')


a = Analysis(
    ['test.py'],
    pathex=[],
    binaries=[],
    datas=datas,
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
    optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='test',
    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,
)
```

上の方に`--collection-data`で追加した内容が書かれています。gradioに対するmodule_collection_modeを変更するために、中央付近の`optimize=0`あたりを次のように修正して保存します。

```
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_data_files

datas = []
datas += collect_data_files('gradio_client')
datas += collect_data_files('safehttpx')
datas += collect_data_files('groovy')
datas += collect_data_files('gradio')


a = Analysis(
    ['test.py'],
    pathex=[],
    binaries=[],
    datas=datas,
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
    optimize=0,
    module_collection_mode={
        'gradio': 'py',  # Collect gradio package as source .py files
    },
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='test',
    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,
)
```

`pyinstaller`をspecファイルに対して実行します。

```
pyinstaller test.spec
```

これでWindowsやLinuxでは正常に実行できるようになります。

#### Windowsの場合
```
dist\test.exe
```

`test.exe`ファイルをどこかに移動させて、エクスプローラーでダブルクリックしても自動的にターミナルとブラウザが起動して正常に動作するはずです。

#### Linuxの場合
```
dist/test
```

同様にうまく動くはずです。

#### macOSの場合
```
dist/test
```

とすると、python3.9(コマンドライン版xcodeでインストールされるものやbrew python@3.9でインストールされるもの両方)は正常に動くはずです。

python3.10~3.13の場合、

```
* Running on local URL:  http://127.0.0.1:7860
* To create a public link, set `share=True` in `launch()`.
* Running on local URL:  http://127.0.0.1:7863
* To create a public link, set `share=True` in `launch()`.
* Running on local URL:  http://127.0.0.1:7864
* To create a public link, set `share=True` in `launch()`.
* Running on local URL:  http://127.0.0.1:7865
* To create a public link, set `share=True` in `launch()`.
* Running on local URL:  http://127.0.0.1:7866
* To create a public link, set `share=True` in `launch()`.
* Running on local URL:  http://127.0.0.1:7867
* To create a public link, set `share=True` in `launch()`.
* Running on local URL:  http://127.0.0.1:7868
...
```
と、ブラウザを何度の起動するバグにぶつかります。

python3.9で問題ないならばpython3.9で実行ファイル化してください。

## macOSでのPyinstaller以外の手段

他にもやり方はあるのかもしれませんが、python実行環境をどのフォルダに移動させても実行できるポータブルアプリ化した上でアプリに組み込み、その実行環境を使用するようにしたcommandファイルを作成すれば、ユーザにpythonを意識させずにpythonアプリを利用させることができます。

通常のmacOS用のPython実行環境はそのままでは移動できないので、[この記事](https://qiita.com/doraTeX/items/87302e4f01ee00bb6e5e)が参考にpython 3.12.10をrelocateable-pythonツールを使ってポータブル化してみましょう。

今までのPython仮想環境に入っている場合抜けてください。
```
deactivate
```

test.pyのあるフォルダで作業を続けます。`git`でrelocatable-pythonをインストールします。

```
git clone https://github.com/gregneagle/relocatable-python
cd relocatable-python
./make_relocatable_python_framework.py --python-version 3.12.10 --os-version 11 --upgrade-pip
```

`Python.framework`フォルダがポータブル実行環境です。このフォルダはどこに移動させても動作します。`python3`や`pip3`などの実行ファイルは`Python.framework/Versions/Current/bin`以下に存在します。

test.pyのあるフォルダに戻り、`Python.framework`フォルダ移動させます。

```
mv Python.framework ..
cd ..
```

relocatable-pythonはもういらないので削除します。

```
rm -rf relocatable-python
```

ポータブルPython環境にパスを通します。
```
export PATH=Python.framework/Versions/Current/bin:$PATH
hash -r
```

gradioモジュールをインストールしてtest.pyを動かせるようにします。

```
pip3 install gradio
```

test.pyがこの環境で動くか確認します。

```
python3 test.py
```

次の内容でtest.commandを作成してください。

```
#!/bin/sh
cd "$(dirname "$0")"
exec ./Python.framework/Versions/Current/bin/python3 test.py
```

test.commandに実行属性をつけます。

```
chmod +x test.command
```

こうすれば、test.pyのあるフォルダをどこに移動させてもFinderでtest.commandをダブルクリックすることで問題なく実行することができます。

あとは、[この記事](https://qiita.com/soh19/items/7b3393266ec74378b58a)などを参考に、一般的なmacOSアプリ形式にすれば、gradioを使用したmacOSアプリを一般ユーザでも扱いやすい形で配布できると思います。