はじめに
最近Python が気になっていてexe化を実行しようと思ったのですが、めちゃくちゃ躓いて時間ばかり使ったので、初めて使う人が引っかかりやすい(自分が引っかかった)所を少しでもお助けできたらなと思います。
国語が苦手で、文章が分かりにくいかもしれないです。。。(自分用なのでご容赦ください(´・_・`))
pyinstallerのインストール
インストールは超カンタンで、コマンドプロンプトで以下のコマンドを打つだけで出来ました。
pip install pyinstaller
サンプルコード
僕はPython初心者なので、コードが雑なところがあると思いますが、今回は以下のコードをexe化したいと思います。(適宜numpyをインストールしてください。)コードは出来るだけ行数を減らすようにしているので読みにくいと思います。
import tkinter, os, sys, numpy, import_file
print(numpy.array([1, 2, 3]))
print(import_file.add(3, 5))
root = tkinter.Tk()
root.wm_iconbitmap("icons/icon.ico")
root.mainloop()
def add(a, b):
return a + b
※補足※
2つファイルがあったり、GUI作ったり、変なimportをしてるのは説明のときに使うためです。
iconsフォルダには icon.ico
というアイコンファイルが入っています。
iconフォルダ内のicon.ico
はqiita_tutorial.py
を実行するときに使用します。画像にあるicon.ico
ファイルはpyinstaller
でexeファイルに埋め込むときに使います。
SPECファイルってなんやねん
ファイルの準備が終わったので、さっそくPyinstallerを使ってexe化してみたいと思います。
pyinstaller qiita_tutorial.py --onefile --icon=icon.ico
カレントディレクトリを合わせて、このコマンドを実行するとdistフォルダの中にqiita_tutorial.exe
が生成されていると思います。とりあえずこれでexe化は出来ました。
それに合わせてqiita_tutorial.py
と同じ階層にqiita_tutorial.spec
というファイルが生成されています。SPECファイルというのは簡単に言うと、exe化する時の設定を変更しやすくする為のファイルです。
block_cipher = None
a = Analysis(['qiita_tutorial.py'],
pathex=['G:\\ProgrammingFiles\\VScode\\Python\\qiita_tutorial'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
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,
a.binaries,
a.zipfiles,
a.datas,
[],
name='qiita_tutorial',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=True , icon='icon.ico')
これをテキストエディタで編集する事で、任意のファイルを埋め込んだり、アイコンを設定したりできるようになります。(コマンドのオプションで解決できるものもありますが、テキストエディタで編集する方が個人的には楽です。)
設定例として console=False
にすればコンソール(黒い画面)を非表示にすることができます。
あとSPECファイルを有効にするにはpyinstaller実行時の引数にSPECファイルを指定してあげないと設定が反映されないので、実行する時pythonファイルを指定しないように気をつけてください。
pyinstaller qiita_tutorial.spec
以下のお話はSPECファイルのお話です。
外部ライブラリの埋め込み
自分の環境ではPythonの標準ライブラリ(tkinter
, os
)などは埋め込まなくても動きました。
うまく動かないときは、SPECファイルのhiddenimportの場所に名前を入力すると埋め込む事ができました。
SPECファイルに設定しなくても上手くいく奴もあるっぽいのですが、 (import_file.py
は何故か読み込まれてました)
hiddenimports=["numpy"],
このように、配列に追加してあげると上手くいくみたいです。
自動追加してくれるやつの基準が分らないので、「なんか起動しねぇ!」ってときは試してみてください。
(numpyインストール時に躓いたのでこの記事を読んで解決しました。)
どうやってファイルを埋め込むの?
アイコンとか画像とかはdataの配列に追加すると埋め込む事ができるみたい。
tkinterのGUIアイコンはpyinstallerのアイコンを変更してもうまく行かないので、exeに埋め込む必要があります。
datas=[],
の所を datas=[('icon.ico', 'icons')],
に変更すると icon.ico
ファイルが埋め込まれます。タプルは ("ファイル名", "フォルダ名")
という感じに指定するとうまくいきます。フォルダ名は必須みたいで空欄にすることは出来ませんでした。
埋め込んだファイルは、exeが起動しているときに、C:\Users\<ユーザー名>\AppData\Local\Temp\_MEI<数字>\icons\icon.ico
に展開されます。(exeを終了すると削除されます。)
埋め込んだファイルは以下の関数を通して取得することが出来ます。
exeのときはTempファイルから、Debug実行のときは普通にパスを取得できるようにする関数です。
def resource_path(relative_path):
if hasattr(sys, '_MEIPASS'):
return sys._MEIPASS + "\\" + relative_path
return os.path.join(os.path.abspath("."), relative_path)
以上の関数を qiita_tutorial.py
に追加して、下から2行目を以下のように変更します。
root.wm_iconbitmap(resource_path("icons/icon.ico"))
そうすればexeファイル単体でもGUIのアイコンが設定されると思います。
subprocess が動かない!
noconsoleのオプション(SPECファイルではconsole=False
)を、つけてsubprocessを実行すると、コンソールが起動せずコマンドが実行されないみたいです。
調べたところ回避策があったので、以下の関数をsubprocessの引数にアスタリスクでくっつけると良いっぽい。この関数は stdin, stderr, startupinfo, env
を返り値として待ってるので、それらをオプションに付けるのは出来ないです。(cwd
とかは指定できます。)
def subprocess_args(include_stdout=True):
# The following is true only on Windows.
if hasattr(subprocess, 'STARTUPINFO'):
# Windowsでは、PyInstallerから「--noconsole」オプションを指定して実行すると、
# サブプロセス呼び出しはデフォルトでコマンドウィンドウをポップアップします。
# この動作を回避しましょう。
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
# Windowsはデフォルトではパスを検索しません。環境変数を渡してください。
env = os.environ
else:
si = None
env = None
# subprocess.check_output()では、「stdout」を指定できません。
#
# Traceback (most recent call last):
# File "test_subprocess.py", line 58, in <module>
# **subprocess_args(stdout=None))
# File "C:Python27libsubprocess.py", line 567, in check_output
# raise ValueError('stdout argument not allowed, it will be overridden.')
# ValueError: stdout argument not allowed, it will be overridden.
#
# したがって、必要な場合にのみ追加してください。
if include_stdout:
ret = {'stdout': subprocess.PIPE}
else:
ret = {}
# Windowsでは、「--noconsole」オプションを使用してPyInstallerによって
# 生成されたバイナリからこれを実行するには、
# OSError例外「[エラー6]ハンドルが無効です」を回避するために
# すべて(stdin、stdout、stderr)をリダイレクトする必要があります。
ret.update({'stdin': subprocess.PIPE,
'stderr': subprocess.PIPE,
'startupinfo': si,
'env': env })
return ret
subprocess.run(cmd, **subprocess_args(True)) # cmd = コマンドプロンプトで実行するコマンド(string)