macOS Monterey (12.3)のM1 MacBookでPyInstallerを使って、Kivyで作ったアプリをappパッケージにするの巻

はじめに

Kivyで作成したアプリをPyInstallerでパッケージングして.app化したのですが、macOSでのPyinstallerの使い方の情報を探すのに難儀した挙句にどハマりまでしてしまったので、うまくいった方法の備忘録です。

実施環境

  • MacBook Pro, Apple M1 Proチップ
  • macOS Monterey バージョン12.3.1
  • Xcode Version 13.3.1 (13E500a)
  • Homebrew 3.4.7
  • Pyenv 2.2.5
  • Pipenv; version 2022.4.21
  • Python 3.10.3
  • Kivy 2.1.0
  • Pyinstaller 5.0
  • numpy 1.22.3
  • pandas 1.4.2 1.2.4の間違いでしたすみません(2022.6.12修正)
  • Cython 0.29.28

*Homebrew以下、全部ARM64環境で導入しています。

パッケージ化するKivyのアプリ

Kivy公式のFileChooserのページのサンプルのmain.pyとeditor.kvを使うことにします。

そして、main.pyの8行目に以下の記述を追記して、日本語フォントが使えるようにして、

使いませんけどnumpyとpandasを無駄に入れてみます(.appにした時の容量がどれくらいになるかみてみたいだけ)。

import numpy
import pandas
from kivy.core.text import LabelBase, DEFAULT_FONT
LabelBase.register(DEFAULT_FONT, 'ipaexg.ttf') 

ipaexg.ttfは https://moji.or.jp/ipafont/ipaex00401/ からダウンロードしてきました。

というわけで、今回、ファイルとしてはmain.py、editor.kv、ipaexg.ttfを準備しました。

*このままだとテキストボックスの日本語入力に不具合があるのですが、サンプルなのでそのまま放置します。

方法ざっくり

Kivy公式ページ; Creating packages for macOS の「Using PyInstaller and Homebrew」の項の手順をもとに、ARM64環境にてPipenvでPython仮想環境を作って作業をしました。

  1. Homebrewをインストール
  2. HomebrewでSDL2関連とPyenvをインストール
  3. PyenvでPythonをインストール
  4. Pipenvで仮想環境を構築
    1. 必要なライブラリ/PyInstallerをインストール
  5. 必要に応じて.pyファイルを修正
  6. PyInstallerを実行

ちなみに、最初は「Using PyInstaller without Homebrew」の項に従ってやろうとしたのですが、SDL2関連をインストールするところでエラーが出てしまって、その対処方法が分からず早々に躓いてしまいました。

なので、諦めてHomebrewを使ったこちらの方法(SDL2の導入があっさりできたので)にした、という経緯がありんすよ。

「Using the Kivy SDK」と「Using Buildozer」は試していません。

具体的な方法その1:PyInstaller実行環境を作る

公式マニュアルでは仮想環境上で実行していませんが、余計なライブラリなどが持ち込まれないよう、Pipenvによる仮想環境を作って実行することにします。

順番に見ていきましょう。(シェルはzshです。)

Homebrewのインストール

と、その前に、ARM64環境で実施していることを確認してみます。

uname -m

実行結果

arm64

はい、arm64になってます。よしよし。

Homebrewのホームページのインストールスクリプトをコピペしてきて実行

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

そうすると、次はこれをやってpathを通してちょうだいね、という指示が出ます。

そのまま実行したいところですが、

ここではそうせずに、ターミナルでx86_64環境とarm64環境を切り替えられるようにするために、~/.zshrcに以下を追記してHomebrewのpathの設定をすることにします。

(参考、というかそのまま使わせていただきました; M1 MacへのHomebrewの導入)

if (( $+commands[arch] )); then
  alias a64="exec arch -arch arm64e '$SHELL'"
  alias x64="exec arch -arch x86_64 '$SHELL'"
fi

function runs_on_ARM64() { [[ `uname -m` = "arm64" ]]; }
function runs_on_X86_64() { [[ `uname -m` = "x86_64" ]]; }

BREW_PATH_OPT="/opt/homebrew/bin"
BREW_PATH_LOCAL="/usr/local/bin"
function brew_exists_at_opt() { [[ -d ${BREW_PATH_OPT} ]]; }
function brew_exists_at_local() { [[ -d ${BREW_PATH_LOCAL} ]]; }

setopt no_global_rcs
typeset -U path PATH
path=($path /usr/sbin /sbin)

if runs_on_ARM64; then
  path=($BREW_PATH_OPT(N-/) $BREW_PATH_LOCAL(N-/) $path)
else
  path=($BREW_PATH_LOCAL(N-/) $path)
fi

(export PATH = “XXXXXXXXXXX” の記述の下側に追記しました。)

確認

brew -v

実行結果

Homebrew 3.4.7
Homebrew/homebrew-core (git revision 652cb3786e7; last commit 2022-04-24)

これでよしと。

補足

Homebrewは3.0.0からM1チップに対応したので、Rosetta2を使わずにインストールできるようになった、と。

SDL2, SDL2_image, SDL2_ttf, SDL2_mixerをインストール

Homebrewでインストールします。

brew reinstall --build-from-source sdl2 sdl2_image sdl2_ttf sdl2_mixer

あら、あっさり。

Pyenvのインストール

Homebrewでインストールします。

brew install pyenv

ここも、ターミナルでx86_64環境とarm64環境を切り替えられるようにするために、~/.zshrcに以下を追記してpyenvのpathの設定をします。

(参考、というかほぼそのまま使わせていただきました; Mac(M1 CPU)で、互換性のあるpyenv+pipenvの環境を作る

if [ "$(uname -m)" = "arm64" ]; then
  export PYENV_ROOT="$HOME/.pyenv_arm64"
  export PATH="$HOME/.pyenv_arm64/bin:$PATH"
else
  export PYENV_ROOT="$HOME/.pyenv_x86"
  export PATH="$HOME/.pyenv_x86/bin:$PATH"
fi
eval "$(pyenv init -)"
eval "$(pyenv init --path)"

これのexportとevalのところを、先ほどのHomebrewの設定の際に記載した記述に合体させて一体化することにします。

if (( $+commands[arch] )); then
  alias a64="exec arch -arch arm64e '$SHELL'"
  alias x64="exec arch -arch x86_64 '$SHELL'"
fi

function runs_on_ARM64() { [[ `uname -m` = "arm64" ]]; }
function runs_on_X86_64() { [[ `uname -m` = "x86_64" ]]; }

BREW_PATH_OPT="/opt/homebrew/bin"
BREW_PATH_LOCAL="/usr/local/bin"
function brew_exists_at_opt() { [[ -d ${BREW_PATH_OPT} ]]; }
function brew_exists_at_local() { [[ -d ${BREW_PATH_LOCAL} ]]; }

setopt no_global_rcs
typeset -U path PATH
path=($path /usr/sbin /sbin)

if runs_on_ARM64; then
  path=($BREW_PATH_OPT(N-/) $BREW_PATH_LOCAL(N-/) $path)
  export PYENV_ROOT="$HOME/.pyenv_arm64"     #合体
  export PATH="$HOME/.pyenv_arm64/bin:$PATH" #合体
else
  path=($BREW_PATH_LOCAL(N-/) $path)
  export PYENV_ROOT="$HOME/.pyenv_x86"       #合体
  export PATH="$HOME/.pyenv_x86/bin:$PATH"   #合体
fi
eval "$(pyenv init -)"                       #合体
eval "$(pyenv init --path)"                  #合体

これで、a64またはx64コマンドで切り替えたそれぞれのアーキテクチャに対応した、Homebrewまたはpipenvで導入したライブラリへのパスが通るようになるはず。

Pythonのインストール

–enable-frameworkをつけてフレームワークとしてインストールしておきます。

env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.10.3

ここではバージョン3.10.3をglobal設定にしてそのまま使っていくことにします。

pyenv global 3.10.3

確認

pyenv version

出力結果 (*****のところはユーザー名です)

3.10.3 (set by /Users/*****/.pyenv_arm64/version)

これでよし、と。

補足

–enable-frameworkにてインストールしておかないとPyinstallerでのappのビルド時にコケて結局このステップまで戻ってこないといけなくなります。

また、当初はこのM1 MacBookに買い換えるまでにIntel MacBookで使っていたバージョンであるPython 3.9.4をインストールしようとしたのですが、MakeFileが作られずインストールに失敗してしまいました。

↓エラー

BUILD FAILED (OS X 12.3.1 using python-build 20180424)
(中略)
configure: error: internal configure error for the platform triplet, please file a bug report
make: *** No targets specified and no makefile found.  Stop.

どうやら、2022年4月24日時点ではmacOS Monterey(12.3)上にインストールできるPythonのバージョンが限定されているようでした(Python 3.9.4はpyenv install –listで確認できたバージョンだったのですが。)

で、他のバージョンをいくつか試した中でPython 3.10.3がインストールできて、今回のKivyアプリも動作したので、Python 3.10.3を使うことにしました。

*Pythonのインストールのために何か他に細工をしたような気もするのですが、やったかどうかの記憶が飛んでしまってます、すみません。。。もしうまくインストールできなかったら調べてみてください。

Pipenvのインストールと仮想環境の構築

余計なものを取り込まないよう、仮想環境でappパッケージへのbuildを実施するためにPipenvを使います。Pipenvはpipで入れておきます。

pip install pipenv

仮想環境での作業用にディレクトリ「kivy_onefiletest」を作って、その中で実施していくことにします。

以下、kivy_onefiletestディレクトリ内にて

仮想環境を作ってから、

pipenv --python 3.10.3

仮想環境に入ります。

pipenv shell

はい、入れました。

(kivy_onefiletest) [kivy_onefiletest]$ 

補足

消したつもりの仮想環境がうっかり残っていてうまく行かないことがあるので、もし

pipenv -- python 3.10.3 

ERROR:: --system is intended to be used for pre-existing Pipfile installation, not installation of specific packages. Aborting.

が出る場合、

pipenv --rm

してから再度

pipenv -- python 3.10.3

をやるといいかも。

Cythonのインストール

公式マニュアルに記載のあるバージョンをインストールします。

pipenv install Cython==0.29.28

Kivyのインストール

公式マニュアルでは-Uがついてますが、この仮想環境に初めてインストールするので-Uオプションなしでインストールします。

pipenv install kivy

Pyinstallerのインストール

公式マニュアルでは-Uがついてますが、この仮想環境に初めてインストールするので-Uオプションなしでインストールします。

pipenv install pyinstaller

appに必要なその他のライブラリのインストール

ここではnumpyとpandasを無駄にインストール。使いませんけど最終的な容量がどれくらい大きくなるか見てみるために入れてみます。どきどき。

pipenv install numpy
pipenv install pandas

これでPyInstaller実行環境の準備ができました。

ふう、ちょっと休憩をば

ちょっと広告です
https://business.xserver.ne.jp/

https://www.xdomain.ne.jp/

★LOLIPOP★

.tokyo

MuuMuu Domain!

具体的な方法その2:PyInstallerの実行

と、その前に、

appパッケージにしたアプリを起動すると、appパッケージの中身をTempディレクトリに展開して実行する形になっているようなので、Tempディレクトリに展開された時のpathに合わせた形に.pyファイル中のpathの記述を修正してやる必要があるんですと。(修正理由の認識が間違ってたらすみません。appパッケージとした場合にはTempに展開しないかもしれないので。ここんとこ確認していません。)

外部ファイルを読み込むとき

次の記載内容を.pyファイルに追記します。

(参考;【Python】Pythonファイルを実行ファイルに変換する方法【exe化】

import os
import sys

def resource_path(filename):
    if hasattr(sys, '_MEIPASS'): 
        return os.path.join(sys._MEIPASS, filename) 
    return os.path.join(os.path.abspath("."), filename)

で、.pyファイル内で、外部ファイルのpathを記述していたところを次のように置き換えます。

resource_path("hogehoge.txt")

なので今回のmain.pyの6行目あたりを

  6 import os
  7 
  8 import sys
  9 import numpy
 10 import pandas
 11 from kivy.core.text import LabelBase, DEFAULT_FONT
 12 
 13 def resource_path(filename):
 14     if hasattr(sys, '_MEIPASS'): 
 15     ¦   return os.path.join(sys._MEIPASS, filename) 
 16     return os.path.join(os.path.abspath("."), filename) 
 17 
 18 LabelBase.register(DEFAULT_FONT, resource_path('ipaexg.ttf')) 

としました。

外部ファイルを読み込まないのであれば必要ありません(たぶん)。

FileChooserを使っているとき

Load/Saveダイアログがポップアップしたときに、デフォルトのディレクトリ場所をappパッケージが配置されているディレクトリになるように修正しておきます。

FileChooserでポップアップを開く時の関数(参考;Kivy公式のFileChooserのページ)において、

show_load/show_save関数のところに

次の1文を追記します(*****は、.kvファイル中でのLoadDialog/SaveDialogでのFileChooserのidに置き換えてください)

self._popup.content.ids.*****.path=os.path.join(os.path.dirname(sys.argv[0]), '../../../')

Load/Saveダイアログのポップアップが参照するpathを、Kivyの実行ファイルがある3つ上の階層のディレクトリのpathに設定することで、appパッケージが存在するディレクトリでpopupが開くようにできる、と。

で、こうして(Filechooserのidがfilechooserなので)、

def show_load(self):
    content = LoadDialog(load=self.load, cancel=self.dismiss_popup)
    self._popup = Popup(title="Load file", content=content,
                        size_hint=(0.9, 0.9))
    self._popup.content.ids.filechooser.path=os.path.join(os.path.dirname(sys.argv[0]), '../../../')
    self._popup.open()

こうしておく(Filechooserのidがfilechooserなので)、と。

    def show_save(self):
        content = SaveDialog(save=self.save, cancel=self.dismiss_popup)
        self._popup = Popup(title="Save file", content=content,
                            size_hint=(0.9, 0.9))
        self._popup.content.ids.filechooser.path=os.path.join(os.path.dirname(sys.argv[0]), '../../../')
        self._popup.open()

FileChooserを使っていない場合は必要ありません。また、設定しなくても動くので(その場合、 / ディレクトリが開きます)お好みで。

PyInstallerを実行

やっとPyInstallerの出番です。

公式マニュアルの記述をもとにして、次の内容でfirst_run_pyinstaller.txt を作りました。

pyinstaller -y --clean --windowed --name onefiletest \
  --exclude-module _tkinter \
  --exclude-module Tkinter \
  --exclude-module enchant \
  --exclude-module twisted \
  --add-data /*****/*****/*****/kivy_onefiletest/ipaexg.ttf:.\
  --add-data /*****/*****/*****/kivy_onefiletest/editor.kv:.\
  /*****/*****/*****/kivy_onefiletest/main.py 

–name にて、出来上がるものをonefiletestという名前にすることを指定しています。

この時点で–add-data オプションにて、アプリに必要なファイルを一つずつ全部指定しておきます。

このときに使っているデリミタは、macOSの場合は 「:」です。「;」ではなくて。

*****のところはそれぞれのファイルまでのパスに置き換えてください(もしかしたら絶対パスである必要はないかも)。

最後の行で.pyファイルを指定します。–add-dataオプションは付けません。

では走らせます。

source first_run_pyinstaller.txt

ログの最終部分を抜粋。

58946 INFO: checking BUNDLE
58946 INFO: Building BUNDLE because BUNDLE-00.toc is non existent
58946 INFO: Building BUNDLE BUNDLE-00.toc
60415 INFO: Moving BUNDLE data files to Resource directory
60604 INFO: Signing the BUNDLE...
61279 INFO: Building BUNDLE BUNDLE-00.toc completed successfully.

無事に終わると、distディレクトリ内にappパッケージができているのが確認できます。

ls dist 

出力結果

onefiletest	onefiletest.app

できてるっぽい。

補足

Kivy公式マニュアルの手順では、最初に必要ファイル群を指定せずにPyInstallerを走らせて、それによって生成された.specファイルにおいてappパッケージで使用するファイル群を取ってくるように編集して、仕上げに編集済みの.specファイルに対して再度PyInstallerを実行しています。

でもそうやろうとするとバンドルする段階でエラーが出てしまいます。o(`ω´ )o なんでや。

結局このエラーの回避方法が分からなかったので、一回目のPyInstallerの実行で完全体のappパッケージが得られるようにしました(今はappパッケージの生成に無事成功しているので.specファイルの正解の記述方法が分かりますけど。)

できてたonefiletext.specの中身です。(****はpathに置き換えて読んでください。)

# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(
    ['/****/*****/*****/kivy_onefiletest/main.py'],
    pathex=[],
    binaries=[],
    datas=[('/****/*****/*****/kivy_onefiletest/ipaexg.ttf', '.'),   
           ('/****/*****/*****/kivy_onefiletest/editor.kv', '.')],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=['_tkinter', 'Tkinter', 'enchant', 'twisted'],
    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='onefiletest',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    console=False,
    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='onefiletest',
)
app = BUNDLE(
    coll,
    name='onefiletest.app',
    icon=None,
    bundle_identifier=None,
)

codesignをする(必要に応じて)

参考;Signing QWebEngine for macOS notarization #6612

codesignがないからmanualでやりなはれ、と、ログの最後に次のwarningが出た場合、

WARNING: Error while signing the bundle:
(中略)
WARNING: You will need to sign the bundle manually!

これを実行します。

codesign --force --deep --verbose -s - dist/onefiletest.app

で、確認してみます。

codesign --verify --verbose --strict dist/onefiletest.app

出力結果

dist/onefiletest.app: valid on disk
dist/onefiletest.app: satisfies its Designated Requirement

これでOK。

補足

アプリを配布するならcodesignは必要な処理で、これに加えて公証(Notarization)手続きも必要です。が、ここでは触れません。(と、いうか理解が足りずに触れられない)

圧縮する(好みで)

zipで圧縮しました。cntrolキーを押しながらappパッケージをクリックして「”….”を圧縮」がお手軽で良いかと。

ええと、numpyとpandasをインクルードして(したつもりで)98MBでした。

これをzip圧縮したら39.2MBになりました。

ちなみにdmgを作ったら44.6MBでした。(dmgの作成方法は Creating packages for macOS をご参照ください)

まあ許容かなというところに収まったように思います。

配布するのならappパッケージのzipの状態にしておけばいいのかな、と。

起動するか確認

うむ、うまくいきました(日本語入力は不具合丸出しですね)。

FileChooserを開いた時に表示されるディレクトリの場所も、アプリを起動したディレクトリになっています。

おわりに

「ハマった。」という言葉がピッタリな状況を経て、Kivyで作成したアプリをmacOS/M1環境下のPyInstallerを使ってのappパッケージの生成ができたのでした。

インターネットで得られるPyInstallerの情報にはWindows用のものが多かったり、明示されていないために何のOS用なのか分かりにくいものがあったりと、macOS環境で使えそうな情報を得るのに苦労しました。で、公式マニュアルを基にやっていくのが一番回り道をしなくて済んだのね、という印象。

ところで、one-fileにした後の起動が遅い、ものすごい遅い。という情報があって気になってましたが、今回2秒ほどで起動できたので許容かなと。.pyファイルを使って実行していたときは起動するまで2秒ちょっと。あれ?速くなってる気がするぞ。何故だ。M1がすごいのか?いや、.appにするってことは結局one-directoryのようなものだからだなきっと(ちゃんと確かめてない)。

とにかくやり方が分かりましたので、今後はKivyで作ったアプリをぽちぽちっと起動して使えるようになりそうです。

one-fileの形にできたので(といっても.appの実体はディレクトリですが)配布するのも簡単になりますね。ただし、その場合はライセンスの明記をしておかないといけませんので、各種ライセンスを記述したファイルを同梱した形でzipファイルにして配布する形にすればいいのかなと考えています。

がしかし、配布した先でそのまま実行可能にするには、公証(Notarization)手続きを行う必要があるのでした。Developer IDを取得してないワタシはここまでですかね。Apple Developer Programへの登録すると99米ドル/年間必要になりますし。

まあ、なんか良さげなものを作れたら考えてみよう。

おしまい。

参考にしたサイトなど

Kivy公式; Creating packages for macOS

Homebrew

M1 Mac 向け Homebrew3.0.0 をインストールする

M1 MacへのHomebrewの導入

Apple Silicon(M1チップ)におけるpyenv+pipenvでのPython環境構築

Mac(M1 CPU)で、互換性のあるpyenv+pipenvの環境を作る

Mac環境でpyinstallerを使ってOSError: Python library not found: libpython3.9m.dylib, Python, Python3, libpython3.9.dylib,というエラーが出た場合の対処方法

pyenv で Framework / Shared library でインストール

【Python】Pythonファイルを実行ファイルに変換する方法【exe化】

Kivy公式のFileChooserのページ

Signing QWebEngine for macOS notarization #6612


ちょっと広告です
https://business.xserver.ne.jp/

https://www.xdomain.ne.jp/

★LOLIPOP★

.tokyo

MuuMuu Domain!