macOS 上でポータブルな Python 環境を作成する

数年前、次のような記事を書きました。

https://qiita.com/doraTeX/items/f66e80ace5ed5c899799

上記記事を最新状況を踏まえて更新した、新バージョンの記事が本記事です。

# 最新の macOS には Python 環境はデフォルトでは同梱されていない

2022年の macOS Monterey 12.3 Update より、macOS には標準で同梱されないようになりました。よって、Pythonを使用するアプリケーションやスクリプトを配布する開発者は、

* ユーザが各自で Python 環境をインストールすることを要求する
* Python環境を丸ごと添付して配布する

といういずれかの手段を選ぶ必要があります。

# ユーザにインストールさせる場合

ユーザが Python をインストールする方法としては、

* Xcode Command Line Tools
* Homebrew
* MacPorts
* pyenv
* [Python の公式 pkg](https://www.python.org/downloads/macos/)
* Anaconda / Miniconda

などの手が考えられます。このうち、最も簡便なのは `Xcode Command Line Tools` でしょう。

```sh
xcode-select --install
```

としてインストールすると、

```
/Library/Developer/CommandLineTools/usr/bin/python3
```

が使えるようになります。

## 初回起動時の自動インストール

実は、現在の macOS でも

```
/usr/bin/python3
```

というコマンドはデフォルトで用意されていますが、これは“スタブ”と呼ぶべきもので、

* Xcode Command Line Tools がインストールされていなければインストールを促すダイアログを表示する
* インストールされていれば `/Library/Developer/CommandLineTools/usr/bin/python3` を起動する

という挙動をします。

# ポータブルな Python 環境を用意したい！

## ユーザにターミナル操作をさせることは避けたい
上記の「ユーザ各自にPython環境をインストールさせる」という方法の場合、「ユーザにターミナル操作をさせる」という手間が生じます。ですが、これはユーザ側にとって大きな負担になります。一般ユーザにとって「ターミナルにコマンドを打ち込む」という操作は、プログラマが想像するよりもはるかに忌避感の強い行為であり、猛烈に嫌がられます。「ターミナルにコマンドを打ち込む」ことを要求するアプリは、それが普及の妨げとなりえます。自分はアプリ作成において **「ユーザにターミナル操作を要求したら負け」** と考え、ターミナル操作を要求しない形で配布するように心がけています。

## ポータブルな Python 環境を同梱して配布すると受け入れられやすい

そこで、Python環境を丸ごとパッケージ化して、それをアプリに同梱して配布する方法が使えます。`.app` バンドルのアプリの場合、そのバンドル内にPython環境を封じ込めて(`APPNAME.app/Contents/Frameworks/Python.framework`)配布し、アプリ内からはその中の Python バイナリ (`APPNAME.app/Contents/Frameworks/Python.framework/Versions/Current/bin/python3`) を起動する、という形で使えば、ユーザにPython環境を用意させる手間が省けます。 

# ポータブルな Python 環境のビルド法

macOSで配布するためのポータブルな Python 環境のビルドには、[relocatable-python](https://github.com/gregneagle/relocatable-python)というツールを使うのが便利です。

## Step 0: Xcode Command Line Tools のインストール（確認）

まず、開発者自身のMac環境には、Xcode Command Line Tools がインストールされている必要があります。

```sh
xcode-select --install
```


## Step 1: `relocatable-python` レポジトリの clone

```sh
mkdir ~/WORKDIR
cd ~/WORKDIR
git clone https://github.com/gregneagle/relocatable-python
cd relocatable-python
```

## Step 2: Python の最新バージョンを確認

Pythonの最新の正式リリースバージョンを確認します。本記事執筆時点では `3.13.3` でした。そこで、[`https://www.python.org/ftp/python/3.13.3/`](https://www.python.org/ftp/python/3.13.3/) にアクセスします。すると、その中に `python-3.13.3-macos11.pkg` というファイル名が確認できます。この `3.13.3` と `11` という数値を記録しておきます。

## Step 3: `requirements.txt` を用意

これから作成する Portable Python Framework にデフォルトで同梱する pip ライブラリを記載した `requirements.txt` を作成します。

なお、`relocatable-python` レポジトリにはデフォルトで `requirements_python3_recommended.txt` というファイルが用意されていますが、これには古い `pyobjc` が指定されており、これを同梱しようとするとビルドが失敗してしまいます。よって、**用意されている `requirements_python3_recommended.txt` は使わず、自分で必要な `requirements.txt` を用意しましょう。**

例えば次のように指定します。

```python:requirements.txt
numpy             # 配列・行列計算の基盤
scipy             # 数値解析・統計・最適化
matplotlib        # 可視化
pandas            # データフレームによるデータ処理
requests          # HTTP通信の標準的ライブラリ
```

## Step 4: ビルド

次のようにビルドコマンドを実行します。オプションは以下の通りです。

* `--python-version`: Step 2 で確認したPythonのバージョン番号を指定
* `--os-version`: Step 2 で確認したOSのバージョン番号を指定
* `--pip-requirements`: Step 3 で作成したファイルを指定

```sh
./make_relocatable_python_framework.py --python-version 3.13.3 --os-version 11 --upgrade-pip --pip-requirements=requirements.txt
```

これにより、`Python.framework` が生成されます。

## Step 5: 署名

配布するには、Apple Developer Program に登録された正当な署名をしておくことが望ましいです。

```sh
codesign --force -s "Developer ID Application: <キーチェーンに登録した証明書名>" Python.framework
```

署名を終えたら、その署名が有効かどうか、

```
codesign --verify --verbose=4 Python.framework
```

として確認しましょう。


## Step 6: バイナリの対応CPUアーキテクチャ確認

この方法で生成された Python バイナリ自体は、Intel Mac (`x86_64`) / Apple Silicon (`arm64`) の両方に対応した Universal Binary となっています。しかし、`pip` でインストールされる周辺ライブラリは、ホストマシンのネイティブなバイナリしかインストールされないことが普通です。それでは、完全なポータビリティが実現できたとは言えません。

どのバイナリが Universal Binary になっているか否かを確認するには、

```sh
./python_universal_tester.sh 3.13.3
```

とするとよいです。`Python.framework` 内の各バイナリが、`x86_64` / `arm64` のどちらに対応しているかを確認できます。

Pythonそのものだけではなく pip ライブラリを添付して配布する場合、完全なポータビリティを実現するには、Intel Mac と Apple Silicon Mac の両方で `Python.framework` を生成して両方を同梱し、自分のアプリからは、`uname -m` の実行結果に応じてどちらを呼び出すかを切り替える、などといった措置を講じる必要があるでしょう。

## Step 7: 配布

app bundle として配布する場合、`APPNAME.app/Contents/Frameworks/Python.framework` に同梱しておき、`APPNAME.app/Contents/Frameworks/Python.framework/Versions/Current/bin/python3` を起動させるとよいでしょう。シェルスクリプトから Python インタプリタを起動させる場合、

```sh
export PATH=/path/to/APPNAME.app/Contents/Frameworks/Python.framework/Versions/Current/bin:$PATH
```

と冒頭で宣言して起動させるとよいでしょう。

### zipで包む場合の注意

生成された `Python.framework` を zip 圧縮して配布する場合、内部のシンボリックリンク構造を保って zip 化するため，`-y` オプションを付けることを忘れぬようご注意ください。

```sh
zip -rqy PythonFramework.zip Python.framework
```