pyenv によるPythonインストールと落とし穴 (Mac, Ubuntu, CentOS)

# pyenv
[pyenv](https://github.com/pyenv/pyenv) はさまざまなバージョン・実装の Python のインストールとバージョン切り替えを行うための shell scripts 製のソフトウェアです.
Python はソースのダウンロードからビルド・インストールまでを自動で行ってくれる一方, 一部のOSではコンパイルに必要なライブラリのバージョン不整合でインストールに失敗してしまうケースもあります(CentOS7, Debian7など)
pyenv の使い方とインストールにハマったときの対処を記述します.


## とりあえずチートシート
- `pyenv versions` -- インストール済みのバージョン一覧
- `pyenv shell <version>` -- シェルセッションでバージョン切り替え
- `pyenv local <version>` -- 現在のディレクトリ以下でバージョン切り替え
- `pyenv global <version>` -- グローバルのバージョンの切り替え
- `pyenv install --list` -- インストール可能なバージョン一覧
- `pyenv install <version>` -- python のインストール
- `pyenv which python` -- pythonのフルパスの確認

### 使用例
```bash
$ pyenv install --list | grep 3.10
  3.10.0
  3.10-dev
  3.10.1
  3.10.2
  3.10.3
  3.10.4
  3.10.5
  3.10.6
  3.10.7
  mambaforge-4.10.3-10
  miniconda-3.10.1
  miniconda3-3.10.1
  miniforge3-4.10.3-10

$ pyenv install 3.10.7
python-build: use openssl@1.1 from homebrew
python-build: use readline from homebrew
Downloading Python-3.10.7.tar.xz...
...

$ pyenv versions
  system
  3.9.14
* 3.10.7 (set by ~/.pyenv/version)

$ pyenv global 3.10.7
```


## pyenv のインストール
### Linux
```bash
git clone https://github.com/pyenv/pyenv.git ~/.pyenv
cd ~/.pyenv && src/configure && make -C src
```
二行目は高速化のために Bash拡張のコンパイルをしています. 環境によっては失敗しますが, その場合でも pyenv は正常に動作します

### MacOS
```zsh
brew update
brew install pyenv
```

### (OS共通) ログインシェルの設定ファイルに追記
- Bash
```bash
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
```
- Zsh
```zsh
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(pyenv init -)"' >> ~/.zshrc
```

## python ランタイムのインストール

### インストール可能な Python バージョンの確認
- `pyenv install --list`


### 各Pythonバージョンのインストール
- `pyenv install <version>`
    - [python-build](https://github.com/pyenv/pyenv/tree/master/plugins/python-build) により, Python の各バージョンのソースダウンロード・ビルド・インストールをしてくれる
    - `-v` オプションで詳細表示するとコンパイルログが見れる
    - マシンの環境によってはビルド失敗・Warningがでる場合がある. その場合は下記のように必要なパッケージを入れてインストールしてください.

ビルド失敗例: [Python3.10 のビルドには OpenSSL1.1.1 以降が必須](https://www.python.jp/news/wnpython310/require-openssl11.html) なので, CentOS7 や Debian7 (OpenSSL 1.0.2) ではビルドに失敗する. 
```
ERROR: The Python ssl extension was not compiled. Missing the OpenSSL lib?

Please consult to the Wiki page to fix the problem.
https://github.com/pyenv/pyenv/wiki/Common-build-problems


BUILD FAILED (CentOS Linux 7 using python-build 2.3.5-2-g03a5d65)
```
#### インストールに失敗するとき
Python のビルドに必要なパッケージのインストールをする.
##### Debian系(Ubuntu など)
```bash
sudo apt update
sudo apt install build-essential libbz2-dev libdb-dev \
  libreadline-dev libffi-dev libgdbm-dev liblzma-dev \
  libncursesw5-dev libsqlite3-dev libssl-dev \
  zlib1g-dev uuid-dev tk-dev
```
##### CentOS
```bash
sudo yum groupinstall "development tools"
sudo yum install bzip2-devel gdbm-devel libffi-devel \
  libuuid-devel ncurses-devel openssl-devel readline-devel \
  sqlite-devel xz-devel zlib-devel tk-devel
```
###### CentOS7
Pyhton3.10 以降のコンパイルには OpenSSL1.1以降のインストールが必要です. 手間に感じる場合はコンテナイメージ内での開発も視野に入れると良いでしょう.

OpenSSL1.1.1 のインストールと penv による Python3.10.x のインストール

a. OpenSSL のインストールに必要なパッケージのインストール
```
sudo yum install -y zlib-devel perl-core make gcc
```
b. OpenSSL のソースダウンロードとインストール
```bash
sudo curl https://www.openssl.org/source/openssl-1.1.1.tar.gz -o /usr/local/src/openssl-1.1.1.tar.gz
cd /usr/local/src
sudo tar xvzf openssl-1.1.1.tar.gz
cd openssl-1.1.1/
./config --prefix=/usr/local/openssl-1.1.1 --openssldir=/usr/local/openssl-1.1.1/ssl
make
sudo make install

```
c. OpenSSLを共有ライブラリに追加する
```bash
sudo echo "/usr/local/openssl-1.1.1/lib" > /etc/ld.so.conf.d/openssl-1.1.1.conf
sudo /sbin/ldconfig

```
d. `pyenv install` する際は `PYTHON_CONFIGURE_OPTS` で 上記でインストールしたOpenSSLを指定する.
Python 3.10.8 をインストールする場合:
```bash
env PYTHON_CONFIGURE_OPTS="--with-openssl=/usr/local/openssl-1.1.1 --with-ssl-default-suites=openssl" pyenv install 3.10.8
```
## python バージョン切り替え
### インストール済みのバージョン確認
- `pyenv versions`
### バージョン切り替え
- `pyenv shell <version>` -- 現在のシェルのセッションのみでバージョン切り替え
- `pyenv local <version>` -- カレントディレクトリ(および配下)にいるときに, 自動的にそのバージョンに切り替
- `pyenv global <version>` -- ユーザーアカウントに対してグローバルにバージョン切り替え

また, `<version>` に複数の値を渡すと, 複数のバージョンが実行可能になります(`python3.X`のように指定する. `python`を実行した場合のバージョンは左に書いたものから優先される). これは, tox などで複数のバージョンでテストを実行する際に役立ちます.

## pyenv 自体のアップデート
### Linux
`~/.pyenv`を削除して再度インストールでもよいですが, [pyenv-update](https://github.com/pyenv/pyenv-update) というプラグインを使う方法もあります.
- `pyenv-update`のインストール (pyenvはインストール済みであることが前提)
```bash
git clone https://github.com/pyenv/pyenv-update.git $(pyenv root)/plugins/pyenv-update
```
- アップデート
    - `pyenv update`

### Mac
homebrew でインストールした場合:
- `brew update && brew upgrade pyenv`
## pyenv のしくみ

pyenv は次の順序でバージョンを見つけることで, バージョン切り替え時の `shell`, `local`, `global` の機能を実現しています.
1. 環境変数`PYENV_VERSION`が存在すれば現在のシェルセッションでそのバージョンの Python を使用する. 
`pyenv shell` コマンドはこの環境変数を設定する.
2. カレントディレクトリからルートディレクトリまでをたどり, `.python-version` ファイルが存在すればそのバージョンを使用する. 
`pyenv local` コマンドはカレントディレクトリにこの設定ファイルを作成する.
3. `$(pyenv root)/version` に記載されたバージョンのPythonを使用する. 
`pyenv global` コマンドはこのファイルのバージョンを書き換える.

そして, pyenv は次のようにバージョン切り替えを実現しています.
- `python`や `pip` などの実行時には pyenv 初期化時に PATH に追加された`$(pyenv root)/shims` に置かれたコマンドが呼ばれる. 
- `shims` 内のコマンドは上記の方法でバージョンを検知し, インストールしたバイナリへリダイレクトする
    - `which python` をするとこの`shims`ディレクトリ内のバイナリのパスを指すが, インストールした実体へのパスは `pyenv which python` で知ることができる.