62
69

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PyInstallerを使ってみた

Last updated at Posted at 2018-09-22

PythonのスクリプトをまとめてWindows用の実行ファイルに変換できる、PyInstallerを使ってみました。
いろいろと躓いたので忘れないうちにメモ書きです。

PyInstaller自体のインストール方法

とりあえず以下。

コマンド
  $ pip install pyinstaller

もしお使いのプロジェクトでpipenvを使っている場合は、以下です(なぜかは以下に書きました)

コマンド
  $ pipenv install -d pyinstaller

出遭ったトラブルとその対策

複数のファイルにまたがったプロジェクトをコンパイルするときは、どのファイルをターゲットにすれば良いの?

エントリポイントとなるファイル(mainがあるファイルなど)を引数に指定します。のこりの依存関係はPyInstallerが勝手に解析します

なんかログに大量のWARNINGが出る

Visual Cの開発用モジュール関連のようですが、とりあえず無視してOKです

Windows 10 Universal C Runtimeというもののようです1

とりあえずなくても動くようですが、気になる場合はc:\windows\WinSxS\x86_microsoft-windows-m..namespace-downlevel_***(環境ごとに異なる文字)というフォルダを参照パスに加えると消えます。

ただ、うちの環境ではWARNING: lib not found: api-ms-win-core-file-l2-1-0.dll dependency of c:\...\ucrtbase.dll というエラーだけが消えませんでした。

商用で使いたい

PyInstallerは商用利用OKのようです。2

__file__が正しい値を指してくれなくなった。

sys.argv[0]を使いましょう。ただし、一般的な構成の通りに「[プロジェクト名]\src」配下にファイルを配置して開発していた場合など、「親フォルダのパス」が想定しているパスと違ってくる場合があるので注意が必要です。
Path#suffixがexeかどうかなどで処理を分けるといいでしょう。

実装例
  def mypath():
    import sys
    from pathlib import Path
    p = Path(sys.argv[0])
    return p if p.suffix == ".exe" else p.parent

なお、PyInstallerを実行済みかどうか(実行ファイルかどうか)というのは、hasattr(sys, "frozen")でも確認可能なようです(exeになっているときはTrue)3

2018/10/19追記:unittestを使用していた場合

Pythonの標準テストフレームワーク、unittestを使っていた場合、上記の実装だとユニットテスト実行時に正しいパスが取得できない問題を確認しました。
そのため、以下のように書くのが恐らく安全です。

実装例
  def mypath():
    import sys
    from pathlib import Path
    return Path(sys.argv[0]) if hasattr(sys, "frozen") else \
      Path(__file__).parent

pipenvを使っていた場合

一番ハマりました。通常通りコンソールからpyinstallerを呼び出すと、通常通りexeは出力されますが、インポートしたライブラリを読み込んでくれません。

この辺はpipenvのGithubにも質問が上がっていますが4、pipコマンドでシステムグローバルにインストールしたPyInstallerでなく、pipenvで作った仮想環境内に入れたPyInstallerを使う必要があります。

そこで、pipenvコマンドでpyinstallerをインストールし、かならずpipenv shellを実行した環境から、PyInstallerを呼び出すようにしましょう。

コマンド
  $ pipenv install -d pyinstaller
  $ pipenv shell
  $ pyinstaller ...

インポートしたライブラリがPython以外のライブラリを使っていた場合

主にDLLなどです。この辺はコンパイルしたexeファイルを実行していると、そのDLLが必要になった時点で例外が発生するのでわかります(Tracebackに、どのDLLが必要なのか、どこに格納すべきかが表示されます)。
ライブラリが使用するDLLは大抵、インストールしたモジュールのsite-packagesフォルダに入っているので、PyInstallerの引数--add-binaryを使って実行ファイルに組み込むようにしましょう。5

##標準出力に出力していた内容をそれ以外の場所で使いたい
たとえばGUIのあるアプリなど、以前CUIに表示していた文言をわけあってGUIに出力したり、ログとして使いたい場合は、その出力の前に以下のコードを書きます。

追加コード
  import sys
  sys.stdout = open("file.log", "w")

GUIに出力する場合、writeメソッドを実装したオブジェクトをsys.stdoutに突っ込むことでとりあえずリダイレクトできるようです。わたしは面倒なのでまだやっていません(そのうちやるかも)6

また、標準出力を捨てたいときは、os.devnullを使います。

追加コード
  import sys
  import os
  sys.stdout = open(os.devnull, "w")

以上を踏まえて、コンパイル用のコードを書く

何度か試行錯誤することになると思いますので、バッチファイルなりなんなりを書いたほうが断然楽です。一次出力されるspecファイルをいじってもいいらしいのですが、よくわかりませんでした。

create_exe.ps1
if (Test-Path dist) {
  Remove-Item dist -Recurse | Out-Null
}
New-Item dist -ItemType Directory | Out-Null
$windll = "c:\windows\WinSxS\x86_microsoft-windows-m..namespace-downlevel_*"
$windllpath = "."
if(Test-Path $windll){
  $item = (Get-ChildItem $windll | Sort-Object LastWriteTime)[0]
  $windllpath = '"c:\windows\WinSxS\{0}"' -f $item.Name
}
pyinstaller `
  --onefile `
  --name Application `
  --path $windllpath `
  --specpath ./dist/ `
  --distpath ./dist/dist `
  --workpath ./dist/build `
  --add-binary "../.venv/Lib/site-packages/pyzbar/libzbar-32.dll;pyzbar/" `
  --add-binary "../.venv/Lib/site-packages/pyzbar/libiconv-2.dll;pyzbar/" `
  src\gui.py 
Copy-Item config -Recurse dist/dist/config
Copy-Item template -Recurse dist/dist/template

なお、GUIのアプリなので--windowedオプションもつけたかったのですが、つけて実行するとなぜかどこかで「OSError: [WinError 6] ハンドルが無効です。」というエラーが発生するため対処できていません。

PyInstallerの仕組み

PyInstallerは、Pythonおよび関連するファイル一式をまとめて実行ファイル形式に変換し、実行時に以下のフォルダに展開、実行しています。

%USERPROFILE%/AppData/Local/Temp/_[ランダムな英数字]/

このフォルダには、ファイルの実行に必要なpyd形式のPythonライブラリおよび、Pythonの実行エンジンとなるDLL、必要なライブラリ群、PyInstallerの--add-dataオプションや--add-binaryオプションでインクルードしたファイルなど一式が格納されます。

このファイルはアプリケーションの実行が終了すると自動的に削除されます(ただし、アプリケーションが異常終了したときなど消えずに残ってしまう場合があります。その場合いつ消えるのかは不明です)。

これでPyInstallerで作ったexeの実行がやたらと遅い理由がわかったね(Tempフォルダに全てのファイルを展開しているため。とはいえ容量の割には遅すぎると思いますので、今後のPyInstallerのアップデートで少し早くなる可能性はあります)。

それを知っていて嬉しいことって?

例えば内部でJinja2などのテンプレートエンジンを使っていて、テンプレートとなるHTMLファイルをexeに組み込みたいとか、PyInstallerで格納したアイコンファイルをアプリのGUIにアイコンとして表示したいとかいったときには有効です。

この展開されたフォルダのパスはsys.prefixで得ることができるので、Path(sys.prefix) / "template" / "main.jinja"などとやれば、テンプレートとして埋め込んだHTMLファイルを読み込むことが可能となります3

先にも書いたとおり、プログラムがPyInstallerによりexeにされているかどうかは、hasattr(sys, "frozen")またはPath(sys.argv[0]).prefixで知ることができるので、これを使ってアイコンファイルやテンプレートファイルの参照パスを切り替えれば、exeにしたときも、スクリプトのままでも、期待通りに動くプログラムを作成することができます。

ただし、先の通り、**exe実行時に作成されたフォルダは、アプリの終了と同時に削除されてしまいます。**そのため、ユーザーの設定・編集結果を含むファイルをここに配置しても、当然アプリの終了と同時に消されてしまうので注意が必要です。

  1. lib no found :api-ms-win-crt-*.dll · Issue #2068 · pyinstaller/pyinstaller

  2. FAQ · pyinstaller/pyinstaller Wiki

  3. Embed icon in python script - Stack Overflow ※ 一番下の回答でhasattr…およびsys.prefixが使われている 2

  4. ModuleNotFoundError when running installed packages · Issue #98 · pypa/pipenv

  5. Python 3.x - vlcとPyQt5を用いたプログラムのPyInstallerによるexe化がうまくいきません。(104186)|teratail(ただしここにある方法は使っていない)

  6. [Python] stdoutをTkinter Textウィジェットにリダイレクトする方法 Python-2.7 | CODE Q&A 問題解決 [日本語]

62
69
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
62
69

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?