背景
streamlit のアプリを .exe にしてぺろっと配布できるようにしたい...
pyinstaller を使い, .py から steramlit run
を動かすという形で対応します.
手順
ありがとうございます. 基本はこれを周到すればよいです. 以下は差分的な感じです.
2022 年 11 月時点で, streamlit.web.cli
がすでに用意されているので, site-packages のほうに cli.py を作る必要がなくなりました.
にあるようにエントリとなる .py はこんな感じになります.
import streamlit.web.cli as stcli
import sys
def streamlit_run():
sys.argv=["streamlit", "run", "main.py", "--global.developmentMode=false"]
sys.exit(stcli.main())
if __name__ == "__main__":
streamlit_run()
.streamlit/config.toml
は不要かもしれません.
とりあえずなくても cli コマンド引数で対応できるためです.
spec ファイル
一度コマンドラインで pyinstaller 実行し, spec ファイルが作られるのでこれを編集します.
asset file
実行時に streamlit のサイトテンプレートのファイルなどが必要になります(streamlit/static/index.html
など)
datas にパス(元ディレクトリと, コピー先)を指定します.
site.getsitepackages()
で現在(pyinstaller 動かす環境)の python site-packages からコピるようにするとよいでしょう.
import site
import os
block_cipher = None
assert len(site.getsitepackages()) > 0
# Choose path containing "site-packages"
package_path = site.getsitepackages()[0]
for p in site.getsitepackages():
if "site-package" in p:
package_path = p
break
a = Analysis(
['cli_main.py'],
pathex=[],
binaries=[],
# copy streamlit files
datas=[(os.path.join(package_path, "altair/vegalite/v4/schema/vega-lite-schema.json"),
"./altair/vegalite/v4/schema/"),
(os.path.join(package_path, "streamlit/static"),"./streamlit/static")
],
hiddenimports=[],
hookspath=['./hooks'],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
streamlit で利用する .py 関連(multipage 利用時は pages
フォルダの .py
なども)は datas
に記載しましょう.
hooks
hooks スクリプトが必要になります. これを行わないと, 実行時 import streamlit
で streamlit
フォルダを import するようになり, しかしパッケージには static html のアセットファイルしかないため import error
importlib_metadata.PackageNotFoundError: No package metadata was found for streamlit
が生じてしまいます.
# hook-streamlit.py
from PyInstaller.utils.hooks import copy_metadata
datas = copy_metadata('streamlit')
な hoo-streamlit.py ファイルを作り,
↑での hookspath
に hooks スクリプトのフォルダを記載します.
hook 用のファイル名は hook-IMPORTNAME.py
である必要があります.
streamlit 1.14 あたりから, 実行時 `'No module named streamlit.runtime.scriptrunner.magic_funcs' エラーがでるるようになります.
streamlit.runtime
は実行時に評価されるモジュールのため, .spec での datas に streamlit.runtime
のフォルダもコピーするように追加するか, エントリポイントの .py で import して pyinstaller が見つけられるようにしておくとよいでしょう.
other python modules
cv2 など, streamlit に直接関係しない module は pyinstaller が自動では見つけてくれないので,
(.exe にしたあと .py を評価しないとわからないため)
適当に requirements.txt
などの python module をエントリポイントの .py で import しておくか,
.spec に記述しておきましょう
パッケージング
あとは pyinstaller で .spec ファイルを指定してパッケージングするだけです!
お好みで --onefile
などして.
問題点
streamlit が web サーバプロセスとして動くので, port 解放とか必要になります.
Electron で Standalone という手もありますが, 完全に処理部分を以下の stlite(python module が全部 WASM 化されている場合)に置き換えられる場合に限られます.
(ローカルファイル扱っていたり, FastAPI や pytorch つかっていたりしたら結局はサーバプロセス + Electron 等の GUI の組み合わせとなってしまう. FastAPI については OpenSSL 部分含め WASM モジュール化できればワンチャンあるかもしれませんが...)
.py がいじれる
起動させる .py はユーザーがいじれてしまいます.
ソースコード公開したくないやソースいじられたくない場合, なにか方法を考える必要があります(cli 起動側で main.py 部分を import させて .exe に取り込むなどできるかも?)
パッケージが大きい, 起動が遅い
pytorch とか使っているとパッケージが 4 GB とかになります. 大きめの python プロジェクトだと起動も遅くなります...
このあたりはどうしようもないですね...
python-build-standalone で Python 環境を self-contained で作り, PyOxidizer でバイナリ化(Python 環境 embed)がいいかもしれません.
stlite を使う(制約あり)
pyodide で WASM 化された Python で pure HTML(WASM)で動かす
C++ native module などを含んだものは対応できません(のはず)
(C++ -> wasm コンパイルすればいけるだろうがめんどい)
pytorch とかつかっておらず, numpy あたりだけ使っているのであれば, これと nativefier を組み合わせすれば行けそうなきもします!(未検証) ただ, 大きめのプロジェクトだと numpy とかの js のロードで起動遅くなるやも?
TODO
main.py
など .spec に記載し自動でパッケージされるようにする.
- electron-builder を試す https://qiita.com/tetutaro/items/ac04b4dca12fbbaf55dc
- PyOxidizer を試す