LoginSignup
3
4

More than 3 years have passed since last update.

【自分用備忘録】初めてPyinstallerを使って躓いたから解決策を書く

Last updated at Posted at 2020-12-16

はじめに

最近Python が気になっていてexe化を実行しようと思ったのですが、めちゃくちゃ躓いて時間ばかり使ったので、初めて使う人が引っかかりやすい(自分が引っかかった)所を少しでもお助けできたらなと思います。
国語が苦手で、文章が分かりにくいかもしれないです。。。(自分用なのでご容赦ください(´・_・`))

pyinstallerのインストール

インストールは超カンタンで、コマンドプロンプトで以下のコマンドを打つだけで出来ました。

pip install pyinstaller

サンプルコード

僕はPython初心者なので、コードが雑なところがあると思いますが、今回は以下のコードをexe化したいと思います。(適宜numpyをインストールしてください。)コードは出来るだけ行数を減らすようにしているので読みにくいと思います。
filetree.jpg

qiita_tutorial.py
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()
import_file.py
def add(a, b):
    return a + b

※補足※
2つファイルがあったり、GUI作ったり、変なimportをしてるのは説明のときに使うためです。
iconsフォルダには icon.icoというアイコンファイルが入っています。
iconフォルダ内のicon.icoqiita_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化する時の設定を変更しやすくする為のファイルです。

qiita_tutorial.spec(一部縮小)
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 は何故か読み込まれてました)

qiita_tutorial.spec
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実行のときは普通にパスを取得できるようにする関数です。

resource_path()
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行目を以下のように変更します。

qiita_tutorial.py
root.wm_iconbitmap(resource_path("icons/icon.ico"))

そうすればexeファイル単体でもGUIのアイコンが設定されると思います。

subprocess が動かない!

noconsoleのオプション(SPECファイルではconsole=False)を、つけてsubprocessを実行すると、コンソールが起動せずコマンドが実行されないみたいです。
調べたところ回避策があったので、以下の関数をsubprocessの引数にアスタリスクでくっつけると良いっぽい。この関数は stdin, stderr, startupinfo, env を返り値として待ってるので、それらをオプションに付けるのは出来ないです。(cwd とかは指定できます。)

subprocess_args()
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_args() 参照元

subprocess
subprocess.run(cmd, **subprocess_args(True)) # cmd = コマンドプロンプトで実行するコマンド(string)
3
4
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
3
4