pyinstaller によって Executable ファイルを生成すると、必要なモジュールやライブラリ以外にも生成物に含めてしまい、サイズが大きくなる傾向がある。そこで、仮想環境をつくって、そこで Executable 化対象となるスクリプトやアプリケーショを作成し、pyinstaller をインストールするのがよい。
$ python -m venv VENV
$ . VENV/bin/activate
(VENV) $ pip install pyinstaller
次のような単純なサンプルを作成した。
(VENV) $ tree -L 2
.
├── VENV
│ ├── bin
│ ├── include
│ ├── lib
│ └── pyvenv.cfg
├── app
│ └── addition.py
└── main.py
# main.py
from app.addition import add
print('5 + 4 =', add(5, 4))
# app/addition.py
def add(a, b):
return a + b
pyinstaller
に対し、アプリケーションのエントリポイントとなるモジュール(ここでは main.py
)を指定して実行する。
(VENV) $ pyinstaller main.py
すると、自分の環境では次のようなエラーが発生した。
3301 ERROR: Can not find path @executable_path/../Python3 (needed by /Users/kazu/tmp/VENV/bin/python3)
Traceback (most recent call last):
File "/Users/kazu/tmp/VENV/bin/pyinstaller", line 10, in <module>
sys.exit(run())
File "/Users/kazu/tmp/VENV/lib/python3.7/site-packages/PyInstaller/__main__.py", line 114, in run
run_build(pyi_config, spec_file, **vars(args))
File "/Users/kazu/tmp/VENV/lib/python3.7/site-packages/PyInstaller/__main__.py", line 65, in run_build
PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)
File "/Users/kazu/tmp/VENV/lib/python3.7/site-packages/PyInstaller/building/build_main.py", line 734, in main
build(specfile, kw.get('distpath'), kw.get('workpath'), kw.get('clean_build'))
File "/Users/kazu/tmp/VENV/lib/python3.7/site-packages/PyInstaller/building/build_main.py", line 681, in build
exec(code, spec_namespace)
File "/Users/kazu/tmp/main.spec", line 17, in <module>
noarchive=False)
File "/Users/kazu/tmp/VENV/lib/python3.7/site-packages/PyInstaller/building/build_main.py", line 244, in __init__
self.__postinit__()
File "/Users/kazu/tmp/VENV/lib/python3.7/site-packages/PyInstaller/building/datastruct.py", line 160, in __postinit__
self.assemble()
File "/Users/kazu/tmp/VENV/lib/python3.7/site-packages/PyInstaller/building/build_main.py", line 478, in assemble
self._check_python_library(self.binaries)
File "/Users/kazu/tmp/VENV/lib/python3.7/site-packages/PyInstaller/building/build_main.py", line 568, in _check_python_library
python_lib = bindepend.get_python_library_path()
File "/Users/kazu/tmp/VENV/lib/python3.7/site-packages/PyInstaller/depend/bindepend.py", line 945, in get_python_library_path
raise IOError(msg)
OSError: Python library not found: Python, .Python, libpython3.7m.dylib, libpython3.7.dylib
This would mean your Python installation doesn't come with proper library files.
This usually happens by missing development package, or unsuitable build parameters of Python installation.
* On Debian/Ubuntu, you would need to install Python development packages
* apt-get install python3-dev
* apt-get install python-dev
* If you're building Python by yourself, please rebuild your Python with `--enable-shared` (or, `--enable-framework` on Darwin)
仮想環境の Python はベースとなる元の環境の Python のライブラリを参照しているらしいのだが、その参照を pyinstaller がうまく処理できていないようだ。いろいろ試行錯誤した結果、結局、その参照を処理している pyinstaller のコードを修正することにした。
仮想環境内の VENV/lib/python3.7/site-packages/PyInstaller/depend/bindepend.py
の get_python_library_path
関数内で、ライブラリが見つからないという例外をスローしている箇所の直前に次のコードを書いた。
python_libname = _find_lib_in_libdirs('/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.7/lib')
if python_libname:
return python_libname
/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.7/lib
がベースとなる Python のライブラリが置いてあるディレクトリだ。これは、シンボリックリンクである VENV/bin/pyton
の参照先をたどっていけば推理できる。
このようにしてから、再度実行するとうまくいった。実行後、次のファイル、ディレクトリが作成される。
- build/
- dist/
- main.spec
dist/main/
フォルダに Executable ファイル main
とそれが必要とするリソースが格納されている。Executable ファイルはコンソールから実行できるし、GUI 上でダブルクリックして実行することもできる。
$ dist/main/main
5 + 4 = 9
dist/main/
フォルダ内のすべてで実行環境は完結しているので、これを他のマシン上にもっていっても実行できる。ただし、OS は共通でないといけない。PyInstaller はクロスコンパイラではないからだ。
GUI 上でダブルクリックして実行した場合、コンソールが開いて、そこで実行され、終了後すぐにコンソールが閉じてしまうので、コンソールアプリケーションの場合はダブルクリックでの実行は向かない。
main.spec
は生成物作成にあたっての仕様が出力されている。再度、同じやり方で Executable を生成したいなら、
(VENV) $ pyinstaller main.spec
のようにできる。Executable ファイル実行時にエラーが出て、生成方法に修正を加えたい場合、この spec ファイルを修正することになる。
実行環境をすべてひとつのファイルにまとめたいなら、--onefile
オプションをつけて実行する。
(VENV) $ pyinstaller main.py --onefile
Executable な dist/main
ファイルがひとつ作成される。
アプリケーション main.py
から外部のリソースへのアクセス、たとえば、assets/
フォルダ内のファイルを読み出し、tmp/
フォルダにファイルを書き出すとしよう。フォルダ構成は次のようにする。
(VENV) $ tree -L 2
.
├── VENV
│ ├── bin
│ ├── include
│ ├── lib
│ └── pyvenv.cfg
├── assets
│ └── hello.txt
├── main.py
└── tmp
main.py
を通常通りに実行した場合には、それと同じディレクトリにある assets/
, tmp/
を使用するようにする。
PyInstaller で Executable 化した場合、--onefile
をつけずに生成するのであれば、dist/main/
フォルダ内に assets/
をコピーし、tmp/
は dist/tmp/
を使用する。
└── dist
├── main
│ ├── main
│ ├── assets
│ │ └── hello.txt
│ │
│ │
│ 他多数のファイル
│ .....
│
└── tmp
--onefile
をつけて単一ファイルに生成した場合、assets/
は単一ファイル内に組み込んでしまい、tmp/
は、dist/tmp/
を使用する。
└── dist
├── main
└── tmp
この3つの状況すべてに対応できるようにするため、main.py
は次のように作成する。
"""
assets/hello.txt を tmp/new_hello.txt へとコピーする。
"""
# main.py
import sys
import os
import shutil
# pyinstaller により Excecutable 化されている場合、sys パッケージに
# frozen プロパティが追加される。そして、sys.executable は、Executable 化された
# 実行可能ファイルのパスを返し、sys._MEIPASS は、Executable ファイルが展開される
# フォルダのパスを返す。単一ファイルでない場合、sys._MEIPASS は Executable ファイルが
# 存在するフォルダのパスであり、単一ファイル化されている場合、それはシステム上のどこかのディレクトリに
# 一時的なフォルダをつくり、そこに展開されるので、その一時フォルダのパスが返る。
#
if getattr(sys, 'frozen', False):
print('sys.frozen:', sys.frozen)
print('sys.executable:', sys.executable)
print('sys._MEIPASS:', sys._MEIPASS)
folder_of_executable = os.path.dirname(sys.executable)
if os.path.samefile(folder_of_executable, sys._MEIPASS):
base_path = os.path.dirname(folder_of_executable)
else:
base_path = folder_of_executable
assets_path = os.path.join(sys._MEIPASS, 'assets')
else:
base_path = os.path.dirname(os.path.abspath(__file__))
assets_path = os.path.join(base_path, 'assets')
tmp_path = os.path.join(base_path, 'tmp')
print('base_path:', base_path)
print('assets_path:', assets_path)
print('tmp_path:', tmp_path)
shutil.copyfile(os.path.join(assets_path, 'hello.txt'), os.path.join(tmp_path, 'new_hello.txt'))
Executable 化するには、次のようにする。
(VENV) $ pyinstaller main.py --add-data assets:assets
または
(VENV) $ pyinstaller main.py --add-data assets:assets --onefile
--add-data {生成物に含めるファイル}:{生成物を展開時したフォルダ内での該当ファイルの相対パス}
を使用する。
3つの状況それぞれで実行した場合の出力を見てみる。
(VENV) $ ls tmp
(VENV) $ python main.py
base_path: /Users/kazu/tmp
assets_path: /Users/kazu/tmp/assets
tmp_path: /Users/kazu/tmp/tmp
(VENV) $ ls tmp
new_hello.txt
(VENV) $ pyinstaller main.py --add-data assets:assets
..........
..........
(VENV) $ mkdir dist/tmp
(VENV) $ dist/main/main
sys.frozen: True
sys.executable: /Users/kazu/tmp/dist/main/main
sys._MEIPASS: /Users/kazu/tmp/dist/main
base_path: /Users/kazu/tmp/dist
assets_path: /Users/kazu/tmp/dist/main/assets
tmp_path: /Users/kazu/tmp/dist/tmp
(VENV) $ ls dist/tmp
new_hello.txt
(VENV) $ pyinstaller main.py --add-data assets:assets --onefile
(VENV) $ mkdir dist/tmp
(VENV) $ dist/main
sys.frozen: True
sys.executable: /Users/kazu/tmp/dist/main
sys._MEIPASS: /var/folders/r_/ksrq32qs4vx_x4fpgdlzyjdr0000gn/T/_MEICYgreP
base_path: /Users/kazu/tmp/dist
assets_path: /var/folders/r_/ksrq32qs4vx_x4fpgdlzyjdr0000gn/T/_MEICYgreP/assets
tmp_path: /Users/kazu/tmp/dist/tmp
(VENV) $ ls dist/tmp
new_hello.txt
上の /var/folders/r_/ksrq32qs4vx_x4fpgdlzyjdr0000gn/T/_MEICYgreP
が、単一実行ファイルの main
が展開されたフォルダだ。これは終了後に自動で削除される。しかし、Ctl-C 等で終了すると残ってしまう。その場合でもシステム(Mac OS X)を再起動したら削除された。
なお、--add-data
オプションの効果は、spec ファイルに現れる。spec ファイル内の datas=[('assets', 'assets')]
というのがそれ。Executable ファイル実行時にエラーが出る場合、spec ファイルを編集して、エラーを解消することができる。
Dash フレームワーク と numpy
や pandas
, scikit-learn
等を使ったローカル実行用の Web アプリケーションを Executable 化したとき、Executable ファイルを実行時に Dash
フレームワークのファイルが見つからないというエラーが発生した。これを解消するには、spec ファイルの datas
に手動で必要データを追加してやらねばならない。実行時のエラーメッセージを確認し、spec ファイルを修正して、再度 Executable 化して実行し、エラーが起きるかを確認するということを繰り返していく。最終的に datas
は次のようになった。
a = Analysis(['index.py'],
pathex=['/Users/kazu/Works.localized/AISing/tosan-tool'],
binaries=[],
datas=[
('/Users/kazu/Works.localized/AISing/tosan-tool/VENV/lib/python3.7/site-packages/dash_core_components', 'dash_core_components'),
('/Users/kazu/Works.localized/AISing/tosan-tool/VENV/lib/python3.7/site-packages/dash_html_components', 'dash_html_components'),
('/Users/kazu/Works.localized/AISing/tosan-tool/VENV/lib/python3.7/site-packages/dash_table', 'dash_table'),
('/Users/kazu/Works.localized/AISing/tosan-tool/VENV/lib/python3.7/site-packages/dash_renderer', 'dash_renderer'),
('/Users/kazu/Works.localized/AISing/tosan-tool/VENV/lib/python3.7/site-packages/dash', 'dash'),
],
.....
.....)