embeddableバージョンのPythonを利用したPythonの自作アプリの配布について

# 概要

Windowsユーザーへ自作のPythonアプリを配布する際，pyinstallerによるバイナリ化が一般的であると考える．しかし，これが何らかの理由により，多くの場合で失敗するため，これの代わりとなる方法として，embeddableバージョンのPythonを利用する方法を記す．

# 考慮している点

配布されるユーザーは，~~パソコンに関して疎く，~~プログラミングに対する知識が全くないと仮定し，彼らがなるべく簡単な操作でこれをできるようにする．

# 環境

`Windows 10 1903 64bit`で確認済み

# 実際にこの配布方法を利用した自作プロジェクト

[video_to_picture_everyseconds](https://github.com/dongsiku/video_to_picture_everyseconds)：開始時間と間隔の時間を設定すると，それに合わせて動画から静止画をキャプチャするプログラム~~（私の作ったプログラムの宣伝です）~~

# 方法

ディレクトリ構造は以下のように作成する．今回の例であるプロジェクト`my_project`では，プロジェクト名もアプリ名も`my_project`とする．

```:tree
my_project/
├── install.bat
├── my_project.bat
├── powershell
│   ├── install.ps1
│   └── my_project.ps1
├── requirements.txt
└── src
    └── my_project.py
```

## プログラム側とユーザー側がそれぞれやること

### プログラム側がやること

#### Pythonのインストール

1. [python.org](https://www.python.org/)から`Windows x86-64 embeddable zip file`をダウンロードし，これを展開する．（なお，厳密には，圧縮ファイルのダウンロードと展開をするだけですが，`Pythonをインストールする`との表現を用います．）
1. 展開したPythonに`pip`をインストールする．
1. `pip install -r requirements.txt`する．

#### 該当の自作アプリ`my_project`の起動

1. インストールしたPythonから，該当の自作アプリ`my_project`のPythonスクリプト`my_project.py`を起動する．

### ユーザー側がやること

- Pythonのインストール：プロジェクトフォルダ`my_project/`の中にある`install.bat`をクリックする．
- 該当の自作アプリ`my_project`の起動：プロジェクトフォルダ`my_project/`の中にある`my_project.bat`をクリックする．
- Pythonと該当の自作アプリ`my_project`のアンインストール：`my_project/`を削除するだけでよい．

アプリ`my_project`内での操作を除けば，ユーザーは，**クリック以外の操作は行わなくてよい**．よって，ユーザーには，`install.bat`と`my_project.bat`のみを操作し，他のファイルやフォルダは決して触らないように指示すればよい．

## それぞれのフォルダとスクリプトの詳細について

### それぞれのフォルダについて

#### my_project/src/

このフォルダには，自作アプリ`my_project`のPythonのスクリプトが入っている．この例では，メインとなるスクリプトを`my_project.py`とする．

#### my_project/powershell/

このフォルダには，Python本体のインストールやPythonのスクリプト`my_project/src/my_project.py`の実行に必要な，PowerShellのスクリプト`my_project/powershell/*.ps1`が入っている．これらのPowerShellのスクリプトはバッチファイル`my_project/*.bat`を介して実行する．また，このことからPowerShellのスクリプト内のパスは，`my_project/`がカレントディレクトリとして考えられている．

なお，バッチファイルを介さず，PowerShellファイルを直接実行しても問題はないが，これのダブルクリックによる実行は，デフォルトでは不可能であるため，（特にパソコンに疎い）ユーザーにとって困難となりうる[^1]．また，バッチファイルから直接Pythonを実行する場合，セキュリティの設定など，複雑な操作が困難であるため，バッチファイルからPowerShellを介した処理を行っている．

[^1]: [Powershellを楽に実行してもらうには](https://qiita.com/tomoko523/items/df8e384d32a377381ef9)

### それぞれのスクリプトについて

#### インストールのためのスクリプト

この例では，Pythonのバージョンが3.6.8，amd64となっているが，必要に応じて，変更してもかまわない．変更する部分は，`3.6.8`，`amd64`，`python36._pth`と書いてある部分である．

まずは，PowerShellのスクリプトである`my_project/powershell/install.ps1`を示す．

```powershell:my_project/powershell/install.ps1
# Download Python
if(!(Test-Path "python-3.6.8-embed-amd64.zip")){  
    wget "https://www.python.org/ftp/python/3.6.8/python-3.6.8-embed-amd64.zip" `
        -O "python-3.6.8-embed-amd64.zip"
}
## my_project/にPythonをダウンロードする．
## サーバーpython.orgに，不要な負荷をかけないため，すでにダウンロードされている場合は，ダウンロードしない．
Expand-Archive -Path python-3.6.8-embed-amd64.zip -DestinationPath python-3.6.8-embed-amd64 -Force  
## ダウンロードしたファイルを展開する．
## すでにある場合は，上書きする．

# Download get-pip
cd python-3.6.8-embed-amd64
## 展開したフォルダに入る．
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;
## wgetを使うため，セキュリティの設定を変更する．
## このスクリプトが終了したら，変更されたセキュリティの設定はスクリプトの実行前の状態に戻る．
wget "https://bootstrap.pypa.io/get-pip.py" -O "get-pip.py"
## get-pip.pyをダウンロードする．
Add-Content -Path ./python36._pth -Value "import site" -Encoding UTF8
## get-pip.pyを実行するため，python36._pthにimport siteを追記する．
## 編集前のpython36._pthでは，これがコメントアウトされている．
./python get-pip.py
## get-pip.pyを実行する

# Install additional packages for my_project.py
cd ../
./python-3.6.8-embed-amd64/python -m pip install -r requirements.txt
## pip install -r requirements.txtを行う．
```

次に，バッチファイルのスクリプトである`my_project/install.bat`をしめす．バッチファイルに日本語を表記すると，文字化け等の問題が生じる恐れがあるため，コメントアウトは英語で表記する．

```bat:my_project/install.bat
@echo off
@rem This makes the batch file not appear anything which is not needed for users.
echo Installing...
powershell -NoProfile -ExecutionPolicy Unrestricted .\powershell\install.ps1
@rem Chenge security setting to launch my_project/powershell/install.ps1.
@rem This setting return when this batch file is done.
echo Completed
pause
```

#### 自作アプリを実行するためのアプリ

まずは，PowerShellのスクリプトである`my_project/powershell/my_project.ps1`を示す．

```powershell:my_project/powershell/my_project.ps1
# Launch src/my_project.py
./python-3.6.8-embed-amd64/python ./src/my_project.py

```

次に，バッチファイルのスクリプトである`my_project/my_project.bat`をしめす．

```bat:my_project/my_project.bat
@echo off
powershell -NoProfile -ExecutionPolicy Unrestricted .\powershell\my_project.ps1
pause
```

# 自作アプリにてtkinterを使用した場合

上記の方法での一番の欠点は，[tkinter](https://docs.python.org/ja/3.6/library/tkinter.html)を利用できないことである．ただし，Open-file dialogのためだけにtkinterを用いる場合，`my_project/powershell/my_project.ps1`を改良することで，対処が可能である．

```powershell:my_project/powershell/my_project.ps1
[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
$dialog = New-Object System.Windows.Forms.OpenFileDialog
# おまじない
$dialog.Filter = "VIDEO FILE(*.MOV;*.MP4;*.AVI)|*.MOV;*.MP4;*.AVI"
# Open-file dialogにて，選択するファイルの種類を指定する．
$dialog.InitialDirectory = "~/Desktop"
# Open-file dialogがデフォルトで表示するフォルダを指定する．
$dialog.Title = "Choose the video file"
# Open-file dialogで表示するタイトルを指定する．

if($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK){
  ./python-3.6.8-embed-amd64/python ./src/my_project.py $dialog.FileName
  # Open-file dialogでファイル名を正常に取得した場合，my_project/src/my_project.pyを実行する．
}
```

なお，`my_project/src/my_project.py`では，以下に示すように，`sys.argv[1]`などで，コマンドライン引数を取得できるようにする．

```python:my_project/src/my_project.py
import sys

filename = sys.argv[1]
```

# 参考文献

[超軽量、超高速な配布用Python「embeddable python」](https://qiita.com/mm_sys/items/1fd3a50a930dac3db299)（このページに書かれているものは，左のページに載っているものを自動化したものです．）