Python 実行形式をアイコン付きインストーラにする（Scratch 2 拡張ヘルパーを例に）

# はじめに

Python で作成したプログラムを実行形式にしてインストーラにしておくと、Python のインストールされていない環境で簡単に使えます。できれば、分かりやすいアイコンをデスクトップにでも置いておきたいです。
そこで、アイコン付き実行形式のインストーラの作り方を調べてみたので、忘れないようにまとておきます。

なお、ここではウィンドウが開かない（CUI の）プログラムを扱います。
アイコンをクリックするとコマンドプロンプトが立ち上がって実行される感じです。ただ、GUIでもインストーラの作成はおおよそ同様の流れになります。

ここでは例として、[Scratch 2 オフラインに拡張ブロックを加えるためのヘルパー](https://qiita.com/memakura/items/fb48d7f6fb6b4b88b5bb)をアイコン付きインストーラにしてみます。
ファイル一式は [GitHub](https://github.com/memakura/scratch2-extension) に置きます。
    
# 準備

1. cx_Freeze を `pip install cx_Freeze` などでインストール
1. Python で作成したプログラムが、`if __name__ == '__main__':` で始まるように変更
1. コマンドラインで実行して動くことを確認
1. アイコンを用意
1. UUID を取得

2以降については、以下で実際の流れを説明していきます。

## テスト用プログラムの説明

Scratch 2 のヘルパーに特化した内容も多いため、流れだけ見て飛ばしても結構です。

今回用いる Scratch 2 拡張用ヘルパーのコード [(s2extest.py)](https://github.com/memakura/scratch2-extension/blob/master/s2extest.py) を以下に示します。（リンク先と同じものですが、修正があれば随時リンク先の方を更新するかもしれません。）

  - [元の解説ページ](https://qiita.com/memakura/items/fb48d7f6fb6b4b88b5bb)のコードそのままではなく、クラスにしてあります。
  - `s2extest.main()` で Scratch 2 からのリクエストを受け取るための HTTP サーバが立ち上がるようになっています。
  - （注）変数は `volume` となっていますが、特にボリュームを調整する機能があるわけではありません。


```py3:s2extest.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
 s2extest.py
"""
from aiohttp import web


class S2EXTEST:
    """ Scratch 2 offline extension example """

    def __init__(self):
        self.volume = 0

    async def handle_poll(self, request):
        """ Handle polling from Scratch """
        text = "volume " + str(self.volume) + "\n"
        return web.Response(text=text)

    async def handle_beep(self, request):
        """ Handle beep request from Scratch """
        print("play beep!")
        print("\007")
        return web.Response(text="OK")

    async def handle_setvolume(self, request):
        """ Handle set volume request from Scratch """
        tmp_volume = int(request.match_info['vol'])
        if tmp_volume >= 0 and tmp_volume <= 10:
            self.volume = tmp_volume
            print("set volume= " + str(self.volume))
        else:
            print("out of range: " + str(tmp_volume))
        return web.Response(text="OK")

    def main(self):
        """ Main routine """
        app = web.Application()
        app.router.add_get('/poll', self.handle_poll)
        app.router.add_get('/playBeep', self.handle_beep)
        app.router.add_get('/setVolume/{vol}', self.handle_setvolume)

        web.run_app(app, host='127.0.0.1', port=12345)

if __name__ == '__main__':
    s2extest = S2EXTEST()
    s2extest.main()
```

このプログラムでは aiohttp を使っているので、`pip install aiohttp` としておいてから `python s2extest.py` でテストできます。

使い方はおおよそ以下の流れです。詳細は[こちらの解説](https://qiita.com/memakura/items/fb48d7f6fb6b4b88b5bb)を確認してください。
    1. [Scratch 2 オフライン版](https://scratch.mit.edu/download)をインストールします。
    2. シフトを押しながら [ファイル] を選んで [実験的なHTTP拡張を読み込み] を表示させます。
    3. そこから [JSON 形式のブロック定義ファイル (test.s2e)](https://github.com/memakura/scratch2-extension/blob/master/test.s2e) を開きます。
    4. `python s2extest.py` を立ち上げておけば、Scratch 側の[その他] のブロック（たとえば `beep`）でテストできます。


## アイコンの用意

表示サイズが変わってもアイコンがきれいに表示されるようにするには、複数の解像度の画像がまとまったアイコン画像ファイル（.ico 形式）を用意しておきます。

1. 256x256のカラーの画像 (8bitぐらいでよい) を用意もしくは作成します。(今回は png 形式で用意しました。)
    - 試しに作ってみるとこんな感じです。<img alt="s2extest.png" src="https://qiita-image-store.s3.amazonaws.com/0/55780/e33d9f81-393a-d520-b78c-008f834e5a9e.png" width="128">
    - どこかで見たような気がします（汗）
2. 適当なアイコン作成ツールで読み込んで .ico 形式で保存します。
    - 今回は Greenfish Icon Editor Pro を使ってみました。
    - [ファイル] > [ファイルを開く] で上のカラー画像を読み込みます
    - [アイコン] > [イメージからWindows用のアイコンを作成] を選び作成したい形式はデフォルトのままとします。
    - [ファイル] > [名前を付けて保存] で保存します。（今回は `icon_256x256.ico` としました。
）
3. icons というフォルダを作り、その下に作成した .ico 形式のファイルを入れておきます。

## UUID の取得

UUID (GUID) を生成してくれるページはいろいろあります。Python でも `import uuid` とすれば[簡単に生成できるようです](http://maku77.github.io/python/numstr/uuid.html)。
ただし、一度だけ取得しておくことに注意しましょう。毎回生成してはだめです。別のアプリケーションだと認識され、バージョンアップなどがうまくできなくなります。


# インストーラの生成

cx_Freeze でインストーラを作るために、セットアップ用のファイル (setup.py) を作成します。



## セットアップ用ファイルの用意

以下のようなファイル (setup.py) を用意します。取得した UUID は `upgrade_code` に入れておきます。
アイコンのフォルダやファイル名が違う場合は変更しておきます。ここでは `icons/icon_256x256.ico` とします。
`name`, `version`, `description`, `author`, `url` などを書いておきます。
`packages` には、必要に応じて Python のモジュールを入れます。実行時にすぐエラーで終了する（立ち上がったコマンドプロンプトが閉じてしまう）場合はここを見直すとよいかもしれません。（後述の「トラブルシューティング」も参考に。）

```py3:setup.py
# -*- coding: utf-8 -*-

from cx_Freeze import setup, Executable


name = "s2extest"
version = "1.0.0"
description = "Scratch 2 extension test"
author = "memakura"
url = "https://github.com/memakura/scratch2-extension"

# UUIDは一度決めたら変更しない
upgrade_code = "{7A37B0C0-64E1-479D-A64C-6CCFCB2E1149}"

# ----------------------------------------------------------------
# セットアップ
# ----------------------------------------------------------------
shortcut_table = [
    ('DesktopShortcut',        # Shortcut
     'DesktopFolder',          # Directory_
     "s2extest",               # Name
     'TARGETDIR',              # Component_
     '[TARGETDIR]s2extest.exe',   # Target
     None,                     # Arguments
     None,                     # Description
     None,                     # Hotkey
     None,                     # Icon
     None,                     # IconIndex
     None,                     # ShowCmd
     'TARGETDIR',              # WkDir
    )
    ]

# Table dictionary
msi_data = {'Shortcut': shortcut_table}

# 追加モジュールで必要なものを packages に入れる
build_exe_options = {'packages': ["asyncio"],
                     'excludes': [],
                     'includes': [],
                     'include_files': ["icons/"]
}

bdist_msi_options = {'upgrade_code': upgrade_code,
                     'add_to_path': False,
                     'data': msi_data
}

options = {
    'build_exe': build_exe_options,
    'bdist_msi': bdist_msi_options
}

# CUI : None
base = None 
# GUI :  'Win32GUI' if sys.platform == 'win32' else None

icon = "icons/icon_256x256.ico"

# exe にしたい python ファイルを指定
exe = Executable(script="s2extest.py",
                 targetName="s2extest.exe",
                 base=base,
                 icon=icon
                 )

# セットアップ
setup(name=name,
      version=version,
      author=author,
      url=url,
      description=description,
      options=options,
      executables=[exe]
      )
```

## インストーラの生成

1. ビルドします。以下のコマンドを実行すると、`dist` フォルダの下に `.msi` 形式のインストーラが出来上がります。

    ```
    > python setup.py bdist_msi
    ```
2. 実行してみると、`Program Files` の下に `s2extest` というフォルダができます。(通常は `C:\Program Files\s2extest` になります。)
3. デスクトップにもアイコンが出来上がります。
 
 ![desktopshortcut-image.png](https://qiita-image-store.s3.amazonaws.com/0/55780/01b470e1-fac4-c90f-fb12-fcafd4521ff7.png)
（右が今回作成したヘルパーのアイコンです。Scratch 2 のアイコンも左に並べてみます。）

## 設定ファイルやデモ用ファイルなど

インストール時に設定ファイルやデモ用ファイルを一緒に配布したい場合は、setup.py の `build_exe_options` にある `include_files` を書き換えます。

- デモ用ファイルとして、拡張ブロック定義ファイル [(test.s2e)](https://github.com/memakura/scratch2-extension/blob/master/test.s2e) をいったん Scratch 2 で読み込んで保存したプロジェクトファイル [(s2extest.sb2)](https://github.com/memakura/scratch2-extension/blob/master/s2extest.sb2) を用意します。
   - 本来はブロックの使い方のデモを少し入れておく方がよいかもしれませんが、今回はブロック定義ファイルを読み込む手間を省くためのプロジェクトファイルということで。
- `include_files` を以下のようにすれば、test.s2e と s2extest.sb2 が `Program Files\s2extest\00scratch` の下に一緒にインストールされます。

```setup.py
                     'include_files': [
                         "icons/",
                         ("test.s2e", "00scratch/test.s2e"),
                         ("s2extest.sb2", "00scratch/s2extest.sb2")
                         ]
```

# インストール後の実行方法

1. デスクトップにできたアイコンから s2extest.exe を実行すると、ヘルパー（HTTPサーバ）がコマンドプロンプトで立ち上がります。
1. （今回の例に特化した話ですが）インストール後に `Program Files\s2extest\00scratch\s2extest.sb2` のショートカットをデスクトップに作っておけば、Scratch 2 のプロジェクトが立ち上がって、拡張ブロックをすぐ使用できます。

# トラブルシューティング
- インストール先のPCにて実行してもすぐに閉じてしまう場合：ショートカットのリンク先の先頭に `cmd /k` をつけるとコマンドプロンプトが閉じなくなります。これでデバグできるかもしれません。


## KeyError: 'TCL_LIBRARY'

ビルドする際に `KeyError: 'TCL_LIBRARY'` が出る場合は、Pythonと一緒にインストールされている Tcl/Tk が見つからないことが原因のようです。

https://stackoverflow.com/questions/35533803/keyerror-tcl-library-when-i-use-cx-freeze

を参考に、setup.py へ以下を追加しておきます。

```setup.py
import os.path

PYTHON_INSTALL_DIR = os.path.dirname(os.path.dirname(os.__file__))
os.environ['TCL_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tcl8.6')
os.environ['TK_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tk8.6')
```


# 参考ページ
- [cx_Freeze のマニュアル](http://cx-freeze.readthedocs.io/en/latest/index.html)
- https://github.com/makewitharduino/Scratio : アイコン付き setup.py の例
- http://www.lisz-works.com/entry/python-cx-freeze : GUI と CUI の切り替え