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

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

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

```
pip install pyinstaller
```
# サンプルコード
僕はPython初心者なので、コードが雑なところがあると思いますが、今回は以下のコードをexe化したいと思います。(適宜numpyをインストールしてください。)コードは出来るだけ行数を減らすようにしているので読みにくいと思います。
![filetree.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620977/393ea00c-37f0-1018-722f-e2704c812185.jpeg)


```python: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()
```
```python:import_file.py
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化する時の設定を変更しやすくする為のファイルです。

```python: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` は何故か読み込まれてました)

```python:qiita_tutorial.spec
hiddenimports=["numpy"],
```
このように、配列に追加してあげると上手くいくみたいです。
自動追加してくれるやつの基準が分らないので、「なんか起動しねぇ！」ってときは試してみてください。

(numpyインストール時に躓いたので[この記事](https://qiita.com/bear_montblanc/items/b4b75dfd77da98076da5)を読んで解決しました。)


# どうやってファイルを埋め込むの？
アイコンとか画像とかは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実行のときは普通にパスを取得できるようにする関数です。

```python: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行目を以下のように変更します。

```python: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` とかは指定できます。)

```python: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() 参照元](https://github.com/pyinstaller/pyinstaller/wiki/Recipe-subprocess)


```python:subprocess
subprocess.run(cmd, **subprocess_args(True)) # cmd = コマンドプロンプトで実行するコマンド(string)
```