Kivy を Pyinstaller で Windows Exe 化するときにした諸々
久々に Kivy でプログラムをつくったのですが.Exe するときにハマったのでメモ。自分の状況にピッタリマッチする記事がなかなかなかったので誰かの役にたつかも。
Kivy で UI をつくり .mp4 ファイルの生成するアプリを作成しました。 .py から import 部分を抜き出すとこんな感じになっています。
ソースは app.py という名前です。
ざっくりとしたモジュール構成は、
Kivy のいくつかの UI 部品 (ビデオ pane を含む) を配置し、 plyer でポップアップ通知がでるというものです。
pyinstaller のインストール方法や作りたい実行ファイルと同じ OS でやる必要があるとかそういったことは特に解説しません。
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.video import Video
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.popup import Popup
from kivy.uix.filechooser import FileChooserListView
from plyer import notification
# このへんは、 pyinstaller が自動的に "よしな" にししてくれます。
import os
import platform
from typing import Callable, Optional
from dataclasses import dataclass
import json
from pathlib import Path
from typing import Optional
from enum import Enum
pyinstaller 自体はとても簡単に使えるのでおむろに pyinstall --onefile app.py
してみます。app.exe と app.spec ファイルが生成されます。
が、 app.exe を実行してみるといろいろファイルが足りないようで追加していききます。
sdl2 と angle を取り込む
詳細な仕組みはよくわかっていませんが、Kivy で GUI を表示するとsdl2 か glew を使うものらしいです。先程つくった .exe は sdl を使っているようなのでそれを明示してやります。
import os
os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2'
from kivy_deps import sdl2, angle
deps = []
deps += [Tree(p) for p in sdl2.dep_bins]
deps += [Tree(p) for p in angle.dep_bins]
exe = EXE(
...
# [] のあたりに入れる
*deps,
...
)
app.py にも KIVY_GL_BACKEND = ‘angle_sdl2’ を設定してやります。
今度は、編集した app.spec を使って pyinstaller app.spec
を実行しします。 誤ってさっきと同じ app.py を指定するとせっかく変更した app.spec が上書きされるので注意してください。
gstreamer の取込み
app.exe を実行してみると、どうやら gstreamer 関係の初期化に失敗するようです。
なので app.spec に追記していきます。
ビデオ再生には、ffpyplayer が使われるという記述もみうけられるのですが、試した環境では gstreamer が使われていたので素直にそれに従います。
ffpyplayer の場合にも同じ様に import と deps に追加してやれば多分うまくいきます。
import os
os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2'
from kivy_deps import sdl2, angle
deps = []
deps += [Tree(p) for p in sdl2.dep_bins]
deps += [Tree(p) for p in angle.dep_bins]
deps += [Tree(p) for p in gstreamer.dep_bins]
exe = EXE(
...
# [] のあたりに入れる
*deps,
...
)
再度 pyinstaller app.spec
を実行する。
今度は、pywin32 のエラーが出た。pywin32 は おそらく app.py で明示的に Kivy のタイマー処理を使っている箇所がありそこで使われているっぽい。
pywin32 は今までと異なり pyinstaller から collect_all というモジュールをインポートしてこないといけないらしい。
from PyInstaller.utils.hooks import collect_all
pywin32_datas, pywin32_binaries, pywin32_hiddenimports = collect_all('pywin32')
a = Analysis(
...
hidenimports = [
'win32timezone',
'win32api',
'win32con'
] + pywin32_hiddenimports
...
)
でここまでの内容をまとめると app.spec はこうなった。
# -*- mode: python ; coding: utf-8 -*-
import os
os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2'
from kivy_deps import sdl2, angle, gstreamer
from PyInstaller.utils.hooks import collect_all
deps =[]
deps += [Tree(p) for p in sdl2.dep_bins]
deps += [Tree(p) for p in angle.dep_bins]
deps += [Tree(p) for p in gstreamer.dep_bins]
pywin32_datas, pywin32_binaries, pywin32_hiddenimports = collect_all('pywin32')
a = Analysis(
['app-v18.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[
'win32timezone',
'pywintypes',
'win32api',
'win32con',
'plyer',
'plyer.platforms',
'plyer.platforms.win',
'plyer.platforms.win.notification'
] + pywin32_hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
# set deps
*deps,
exclude_binaries=False,
name='app', # exe の名前なので好きに変えていい
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,
)
Kivy をパッキングするときは割とハマるので、次回はここをベースにしてなんとかしたい。
にしてもインストーラ系のビルドは、動作確認が面倒で、時間かかるのであまり好きにはなれない。