Skip to content

Instantly share code, notes, and snippets.

@fortune
Created April 24, 2020 10:22
Show Gist options
  • Save fortune/35282362a7bdcf500f08912b5ed39ff2 to your computer and use it in GitHub Desktop.
Save fortune/35282362a7bdcf500f08912b5ed39ff2 to your computer and use it in GitHub Desktop.
PyInstaller による Executable ファイル生成

PyInstaller による Executable ファイル生成

インストールと環境構築

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

Executable ファイルの生成

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.pyget_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 ファイルを修正することになる。

単一ファイルでの Executable ファイルの生成

実行環境をすべてひとつのファイルにまとめたいなら、--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 アプリケーションの Executable 化

Dash フレームワークnumpypandas, 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'),
             ],
             .....
             .....)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment