Pythonでスタンドアローンアプリの配布--PyInstallerとInnoSetupを使用するときの注意点

# 前書き
Pythonで作成したWindows上で使用するアプリケーションを配布するため、PyInstallerでスタンドアローンの実行形式にビルドして、その成果物をInnoSetupによってインストーラにまとめています。
この一連の作業で躓いた注意点を書いておこうと思います。

# 使用環境
OS:Windows10 pro(64bit)
IDE:VScode
言語:python3.7.7→3.9.6
PyInstaller　Ver.3.6→4.5→5.7
InnoSetup　Ver.6.0.4

# 参考
[PyInstallerのHomepage](http://www.pyinstaller.org/ "PyInstallerのHomepage")
[InnoSetupのHomepage](https://jrsoftware.org/isinfo.php "InnoSetup　のHomepage")

# スタンドアローン化

PythonコードをWindows用スタンドアローン実行形式にするツールとしては、以下のツールが有名だそうです。

* PyInstaller
* cx_Freeze
* py2exe
この中で、py2exeは2014年以降更新されておらず、これからの利用は避けたほうがよいかもしれません。

PyInstallerのほうが単一ソースコードならば簡単に作れたため、PyInstallerを使うようになりました。

# PyInstallerの問題点
PyInstallerは、多くのサイトで取り上げられていますが、特に3つの点に注意が必要だと思います。

* ~~Python3.7までしかサポートしていない。（3.8以降では正常に動作しません。）~~Ver4.3（2021/4/17）にてPython3.9までサポートされました。（~~ただし、私は動作確認はしておりません。~~Python3.9に移行し、動作確認しました。）
* 依存関係に注意しないと、やたらと大きい容量になってしまう。（importするライブラリやpipでインストールしているパッケージを整理しましょう）
* onefileオプション（実行形式ファイルを一つにまとめる）が正しく機能しない場合がある。
* onefileオプションで作成した実行ファイルを起動すると、起動が遅い（数秒から10数秒）。

**【アンチウイルスソフトによる異常動作】2022年12月26日追記**
アンチウィルスソフトの影響により、pip install でバージョンアップデートを実施したPyInstallerでのBuildがうまくいかない事象が発生しました。
**事象**
consoleオプションをFalseにしてbuildすると、以下のようなエラーメッセージが出てしまいます。
```
PyInstaller does not include a pre-compiled bootloader for your platform
```
ファイルの改ざんを検出するアンチウィルスソフトを使用している場合、PyInstallerのアップデート時に不具合を生じます。
**対策**
アンチウィルスソフトを無効にしたうえで、PyInstallerを一旦アンインストールし、再インストールします。

# InnoSetupの問題点
 InnoSetupはライセンスやインストール前後にドキュメント（テキスト）を表示して、ライセンス条項などを表示できますが、~~日本語表示ができません。~~　
→　（2020年11月16日訂正）
日本語表示ができることを確認しました。ただし、文字化けする現象も確認しています。切り分けができておらず現時点では原因不明です。
→　（2022年10月24日訂正）
テキストファイルの文字コードをUTF-8 BOMとすることで、日本語表示が可能でした。気づかずにテキストファイルをSJISやUTF-8などで保存していると文字化けしてしまします。


# PyInstallerの経験上の注意点
### (1)実行するときは「PyInstaller」の大文字小文字を判別します。
「pyinstaller」ではだめです。


### (2)specファイルの上書きに注意  
 複数ソースやフォルダを跨ぐようなプログラムでは、specファイルに詳細を記述します。specファイルのテンプレートは、*.pyファイルを単独でビルドすることで得られます。
例えば以下のように一度実行すると、Main.specというファイルが生成されます。

``` 
 python -m PyInstaller Main.py
```

　以降、Main.specを利用する場合、リネームして利用するか同名のMain.pyをビルドしないように注意する必要があります。Main.specが上書きされてしまいます。

### (3)スクリプトファイルで自動化  
 ソースコードだけでなく、iconファイルや画像ファイルなど参照することも多いと思います。この場合、PyInstallerをビルドすると生成されるdistフォルダ内のプロジェクトフォルダ名以下に必要なファイル、フォルダを複製しましょう。
　こういったときはバッチファイル等のスクリプトファイルで、PyInstallerのビルドからファイルの複製、さらにはInnoSetupの実行まで自動化しておいたほうが良いです。

### (4)ビルド結果を試しておく
  最終的にInnoSetupでインストーラにまとめ上げるのですが、インストーラを実行する前に、distフォルダ内の実行ファイルで動作するか確認しておきましょう。このためにも上記のスクリプトで必要ファイルをフォルダ内に複製されていることが望ましいです。
　**consoleオプションで動作が異なるパッケージ【2022年12月27日追記】**
　FastAPIでは、consoleオプションによりエラーが発生します。これはFastAPIがデフォルトで標準出力するためconsoleがないとエラーが発生してしまうためです。このようなパッケージがあるため、想定される本番オプションで試すことも必要です。
　スタックオーバーフローにFastAPIでの問題/解決策を投稿しておきました。
 [FaseAPI+Uvicornの環境をPyInstallerでビルドしたとき、console=Falseとするとエラーが発生してしまう](https://ja.stackoverflow.com/questions/93030/faseapiuvicorn%e3%81%ae%e7%92%b0%e5%a2%83%e3%82%92pyinstaller%e3%81%a7%e3%83%93%e3%83%ab%e3%83%89%e3%81%97%e3%81%9f%e3%81%a8%e3%81%8d-console-false%e3%81%a8%e3%81%99%e3%82%8b%e3%81%a8%e3%82%a8%e3%83%a9%e3%83%bc%e3%81%8c%e7%99%ba%e7%94%9f%e3%81%97%e3%81%a6%e3%81%97%e3%81%be%e3%81%86/93031#93031)

### (5)ファイルパスに注意
  開発環境と本番実行環境のディレクトリ構造に注意する必要があります。
　ソースコードをモジュールごとに分割し、モジュールテストのためにソースコードごとのテストのため、ファイルパスを判断している場合に注意が必要がです。
　アプリ全体の実行するカレントフォルダではなく、子フォルダに格納されている場合には、子フォルダ内のソースをテスト実行する際は、実行カレントが異なります。こういうケースに備えて、ファイルパスを自動的に判断させる記述の場合に、スタンドアロン実行形式にしたときに矛盾するパスになっていると実行エラーとなります。

### (6)ソースコードを整理する
  PyInstallerの問題点に記載した通り、注意しないとやたらと大きな実行ファイルを生成します。
　importする内容を精査し、よりシンプルな依存関係に整理しましょう。
　私の場合、最大600MBまで膨れ上がったのですが、80MBまでは落とせました。

### (7)pipでインストールしたパッケージを整理
  いろいろなパッケージを試して開発しており、不要となったパッケージも上記(6)の容量にも影響しないようだったのでそのままにしてありました。
　ところが、久しぶりにインストールパッケージまとめようとしたところ、実行エラーが発生しました。gitでクローンを作っていた別の環境では問題なくインストールパッケージが作れます。上記(4)ビルド結果ですでに動作していませんでした。
　失敗する開発環境と成功する環境でpip listを比較し、uninstallして同じパッケージに揃えても依然として失敗してしまいます。
　まずは、Python自体を再インストールしましたが、本体の再インストールにかかわらず、pipでインストールしたパッケージはそのまま残っていました。
　そこで、パッケージのインストール先を手動で削除することにしました。この削除後にPython本体を再インストールし、必要なパッケージをインストールしなおして、PyInstallerをビルドすると正常に動きました。
　<2021年08月17日追記>
　インストールしたパッケージ整理は、モジュールのライセンス問題が絡んできます。PyInstallerはインストールしてあるパッケージをアプリケーションで使用していなくてもビルドしてしまいます。できる限り最小限のモジュール構成にすべきです。開発環境は必要に応じてvenvなどで仮想化して、パッケージを他の環境から分離することで、余計なビルドやライセンス問題を回避します。

#### pipパッケージの手動削除方法
まず、インストールされているパッケージをリスト表示します。  

```
python -m pip list
```
パッケージ名が分かったところでインストール先を調べます。  

```
> python -m pip show Pyinstaller
Name: PyInstaller
Version: 3.6
Summary: PyInstaller bundles a Python application and all its dependencies into a single package.
Home-page: http://www.pyinstaller.org
Author: Giovanni Bajo, Hartmut Goebel, David Vierra, David Cortesi, Martin Zibricky
Author-email: pyinstaller@googlegroups.com
License: GPL license with a special exception which allows to use PyInstaller to build and distribute non-free programs (including commercial ones)
Location: c:\users\ユーザーアカウント名\appdata\local\programs\python\python37\lib\site-packages
Requires: altgraph, setuptools, pefile, pywin32-ctypes
Required-by:

```
Location情報をもとにlibフォルダ以下をずばっと削除します。

### (8)新しく採用するパッケージはテストコードでビルドしてみる
　新たな機能のため、採用しようとしているパッケージがあれば、そのパッケージの使い方を習得するために、最小限動作するテストモジュールを作成すると思います。
　この最小限動作し、依存関係がシンプルな状態の時に、PyInstallerでビルドして正常に動作することを確認しておきましょう。
　本番コードに組み込んでビルド失敗してから切り分けしていたら時間がかかりすぎます。

### (9)Gitでこまめにコミットしておく
　いったいいつからビルドできなくなったのか、どんな機能を追加したときに問題となったのかを切り分けるため、Gitでこまめにコミットし影響する部分を切り分けられるようにしておくとよいでしょう。
　Git Historyを利用すると、前回コミットからの差分をファイルごとに確認できるので便利です。問題発生した前後のバージョンで、差分箇所をひとずずつ反映していくことで切り分けできます。そのためには変更点が少なくこまめにコミットしてあると良いのです。

### (10)使用しているモジュールのライセンスを把握しておく
　<2021年08月17日追記>
 PyInstaller自体のソースコードは、GPLライセンスですが、PyInstallerでビルドしたバイナリ形式の実行ファイルは、任意のライセンスを付与することができます。
　これは以下のURLに記述がありますので、参考にしてください。
　[PyInstallerのFAQ](https://github.com/pyinstaller/pyinstaller/wiki/FAQ "PyInstallerのFAQ")

　しかし、組み込んでいるモジュールについては、そのライセンスの制約を受けますので、注意が必要です。
　ソースコードの配布を前提にしているのであれば問題はありませんが、ソースコード配布したくない場合には問題になります。
　問題となる主要なライセンス種別
　・GPL
　・LGPL

　GPLはソースコード開示を逃れることが不可能なので、GPLライセンスではない代替パッケージを探す必要があります。
　LGPLは以下の対策が考えられます。
　➀ライセンスの異なる代替パッケージに置き換える。
　➁動的リンクにして、ユーザーに必要なパッケージを個別にインストールしてもらう。
　➂該当パッケージとその周辺機能を分離し、個別のバイナリ化したうえで、主体のアプリケーションから動的リンクで動作させ、該当パッケージのバイナリ部分だけソースコードを開示する。
　
#### LGPLライセンス対策について
　(1)ライセンスの異なる代替パッケージに置き換える。
　　代替パッケージの探索、テストにかなりの時間を費やすことになります。難なく見つかればよいのですが、見つけられない場合には終わりの見えない作業となる恐れがあります。必要に応じて、パッケージではなく自作モジュールの構築も考えたほうが良いかもしれません。
　(2)動的リンクにして、ユーザーに必要なパッケージを個別にインストールしてもらう。
　　私自身は、動的リンクにしたビルドに挑戦していません。ユーザー環境により様々なトラブルを引き起こす可能性があり、不特定多数向けには不安な方法だと思います。
　(3)該当パッケージのバイナリ部分だけソースコードを開示する。
　　扱いなれたパッケージであれば、この手段を考えても良いかもしれません。アプリケーション本体と追加バイナリ（LGPLライセンス）の二つに分かれてしまいますが、一つの手段だと思います。
　　パッケージの機能に依存しすぎている場合には、難しい手段ですが、サードパーティパッケージではごく一部の処理だけ任せるのがほとんどだと思います。
　　「動的リンク」手段を明確に（例えばAPI）しておきさえすれば、ユーザーが自由に追加バイナリ部分の改造が自由になり、LGPLライセンスの意図を汲み取っていると思います。
　 
### (11)明示的にexit()で終了するときは、sys.exit()を使う。
Pythonスクリプトでは、exit()のみで機能しますが、PyInstallerでbuildした実行ファイルではエラーが発生します。
import sysをしたうえで、sys.exit()と記述してください。
→　（2022年12月26日追記）

# InnoSetupの経験上の注意点
### (1)issファイルの準備
  InnoSetupでFile->NewからScriptWizardを起動し、テンプレートを作成しておきます。
　ScriptWizardの中で、Application Filesのところで、依存ファイル/フォルダを追加する項目があります。ここで、必要なファイル/フォルダを加えましょう。PyInstallerのビルド結果が出力されるフォルダそのまま加えます。

→　（2022年10月24日追記）
ISSファイルの保存フォルダがInnosetupでのカレントフォルダとなるので、相対パスに留意する必要があります。
.\や..\が使用できます。


### (2)バージョン記述の自動書き換え
  Pythonのスクリプトファイルで、バージョン記述モジュールを準備しておきます。たとえばRewriteVersion_iss.pyなどとしておきます。PyInstallerの経験上の注意点(3)スクリプトファイルで自動化の中で、このスクリプトによりissファイル(text)のバージョン記述部分のみ書き換えからInnoSetupのビルドを実施します。インストーラに記述されるバージョンとアプリケーションのバージョンが自動的に一致します。

### (3)Pythonコード上のコメント記述に注意
　PyInstallerの経験上の注意点(4)ビルド結果を試しておく　でビルド結果が正常に動作したものの、インストーラでインストールした実行ファイルはエラーが出て動作しません。
　この時の原因が以下のようなコメント記述をしてしまったことです。

```Python
  temp_list = [1,2,3,'''4''']
```
 あとから再利用するかもしれないと思い、一行中に''' '''で囲い込んでおきました。
　これがインストーラを経由すると動かなくなります。

```Python
  temp_list = [1,2,3]
```
　このように記述しなおすと、動作します。

---
以上、現在までに経験した注意点を記述しました。