マルチユーザ環境のための pyenv

# はじめに
## 概要
Python 本体およびライブラリをシステム管理者が一括で管理し、利用するバージョンのみをユーザ各自が管理できる環境を pyenv で構築する方法を紹介します。
## 背景
Python 2 系はいまだシステム標準として利用されていますが、2020年にサポートが切れることから、Python 3 系の利用環境を整えておくのが望ましいと考えられます。そこで、システム側に Python 3 系をインストールし、python コマンドを python3.x へのシンボリックリンクに変更したところ、常駐している tuned や yum コマンドに影響を及ぼします (tuned は syntax error で起動しませんし、また yum install ができなくなります)。

そのため pyenv や virtualenv の利用が推奨されております。それらはシステムに影響を及ぼさないように 3 系をインストールできますが、その設計思想が故に、それをセットアップしたユーザでしか利用できません。すなわち、マルチユーザ環境では、ユーザが増えるたびに pyenv のインストールからしなくてはならず面倒です (pyenv がセットアップされたホームディレクトリのテンプレートを作っておいて、それをコピーすればいいのかもしれませんが)。

ここで紹介する環境は、これらの問題を回避する一つの手段となります。

## 制限事項
通常の pyenv の環境とは異なり、管理者権限がなければ、様々なバージョンの Python を各自で自由にインストールできなくなる点がデメリットになります。

# 構築
## 対象プラットフォーム
* CentOS 7

## 手順
### 必要なパッケージのインストール
pyenv は Python のソースパッケージを入手してビルドしますので、下記の開発用パッケージが必要となります。

```bash
sudo yum -y install gcc gcc-c++ make git openssl-devel bzip2-devel zlib-devel readline-devel sqlite-devel
```
### pyenv のインストール
```bash
mkdir ~/tmp
cd ~/tmp
git clone https://github.com/yyuu/pyenv.git
sudo cp -rupv pyenv /usr/local
chown -R root:root /usr/local/pyenv
```

### pyenv の修正
ユーザが利用する Python のバージョンは ${PYENV_ROOT}/version を参照することで管理されています。デフォルトのまま利用すると、この PYENV_ROOT をシステム共通の場所に設定することになるので、利用者各自でバージョン変更できるようにしようと思うと都合が悪くなります。そこで環境変数 PYENV_VERSION_FILE が設定されていたら、そちらを参照するように pyenv を変更します。

```diff
--- ./libexec/pyenv-global      2017-05-10 13:29:55.444777132 +0900
+++ /usr/local/pyenv/libexec/pyenv-global     2017-05-10 16:35:48.281371082 +0900
@@ -22,7 +22,14 @@
 fi

 versions=("$@")
-PYENV_VERSION_FILE="${PYENV_ROOT}/version"
+if [ -z "${PYENV_VERSION_FILE}" ]; then
+  PYENV_VERSION_FILE="${PYENV_ROOT}/version"
+else
+  PYENV_VERSION_FILE_DIR=${PYENV_VERSION_FILE%/*}
+  if [ ! -d ${PYENV_VERSION_FILE_DIR} ]; then
+    mkdir -p ${PYENV_VERSION_FILE_DIR}
+  fi
+fi

 if [ -n "$versions" ]; then
   pyenv-version-file-write "$PYENV_VERSION_FILE" "${versions[@]}"
--- ./libexec/pyenv-version-file        2017-05-10 13:29:55.444777132 +0900
+++ /usr/local/pyenv/libexec/pyenv-version-file       2017-05-10 16:31:36.042696438 +0900
@@ -19,10 +19,16 @@
   return 1
 }

+if [ -z "${PYENV_VERSION_FILE}" ]; then
+  PYENV_VERSION_FILE_DIR=${PYENV_ROOT}
+else
+  PYENV_VERSION_FILE_DIR=${PYENV_VERSION_FILE%/*}
+fi
+
 if [ -n "$target_dir" ]; then
   find_local_version_file "$target_dir"
 else
   find_local_version_file "$PYENV_DIR" || {
     [ "$PYENV_DIR" != "$PWD" ] && find_local_version_file "$PWD"
-  } || echo "${PYENV_ROOT}/version"
+  } || echo "${PYENV_VERSION_FILE_DIR}/version"
 fi
--- ./libexec/pyenv-rehash       2018-10-04 16:06:44.456347899 +0900
+++ /usr/local/pyenv/libexec/pyenv-rehash        2019-03-25 10:13:36.493066034 +0900
@@ -4,7 +4,7 @@
 set -e
 [ -n "$PYENV_DEBUG" ] && set -x

-SHIM_PATH="${PYENV_ROOT}/shims"
+SHIM_PATH="${HOME}/.pyenv/shims"
 PROTOTYPE_SHIM_PATH="${SHIM_PATH}/.pyenv-shim"

 # Create the shims directory if it doesn't already exist.
--- libexec/pyenv-init  2018-10-04 16:06:44.456347899 +0900
+++ /usr/local/pyenv/libexec/pyenv-init 2019-03-25 11:08:13.948005781 +0900
@@ -82,15 +82,16 @@
   exit 1
 fi

-mkdir -p "${PYENV_ROOT}/"{shims,versions}
+mkdir -p "${PYENV_ROOT}/versions"
+mkdir -p "${HOME}/.pyenv/shims"

 case "$shell" in
 fish )
-  echo "set -gx PATH '${PYENV_ROOT}/shims' \$PATH"
+  echo "set -gx PATH '${HOME}/.pyenv/shims' \$PATH"
   echo "set -gx PYENV_SHELL $shell"
 ;;
 * )
-  echo 'export PATH="'${PYENV_ROOT}'/shims:${PATH}"'
+  echo 'export PATH="${HOME}/.pyenv/shims:${PATH}"'
   echo "export PYENV_SHELL=$shell"
 ;;
 esac
--- libexec/pyenv-which 2019-03-15 16:34:52.087265948 +0900
+++ /usr/local/pyenv/libexec/pyenv-which        2019-03-26 10:57:13.635188148 +0900
@@ -19,10 +19,12 @@
   local path_to_remove="$1"
   local path_before
   local result=":${PATH//\~/$HOME}:"
+  shopt -s extglob
   while [ "$path_before" != "$result" ]; do
     path_before="$result"
-    result="${result//:$path_to_remove:/:}"
+    result="${result//:*([^:])$path_to_remove:/:}"
   done
+  shopt -u extglob
   result="${result%:}"
   echo "${result#:}"
 }
@@ -40,7 +42,7 @@

 for version in "${versions[@]}"; do
   if [ "$version" = "system" ]; then
-    PATH="$(remove_from_path "${PYENV_ROOT}/shims")"
+    PATH="$(remove_from_path "/.pyenv/shims")"
     PYENV_COMMAND_PATH="$(command -v "$PYENV_COMMAND" || true)"
   else
     PYENV_COMMAND_PATH="${PYENV_ROOT}/versions/${version}/bin/${PYENV_COMMAND}"
```

### 全ユーザ共通の設定
```bash
sudo vi /etc/profile.d/pyenv.sh
```

```:/etc/profile.d/pyenv.sh
export PYENV_ROOT="/usr/local/pyenv"
if [ -d "${PYENV_ROOT}" ]; then
    export PATH=${PYENV_ROOT}/bin:$PATH
    eval "$(pyenv init -)"
    export PYENV_VERSION_FILE=${HOME}/.pyenv/version
fi
```

### ディレクトリ作成
最初に root でログインすれば自動的に作成されますが、ここでは手動で作成しておきます。

```bash
sudo mkdir /usr/local/pyenv/shims
sudo mkdir /usr/local/pyenv/versions
```

### Python のインストール
ここでは Python 3.6.8 のみをインストールします。必要に応じて様々なバージョンをインストールすることになります。

```bash
sudo -E pyenv install 3.6.8
```

### pip のアップデート
```bash
pyenv global 3.6.8
sudo -E pip install --upgrade pip
```
自分が利用する Python のバージョンを 3.6.8 に設定して、sudo -E で自分の環境変数を継承させれば、 root が利用する Python のバージョンを変更しなくても大丈夫です。

# その他
## pip の設定
pip のバージョンを 9.x 以降にアップデートするとリスト表示時に警告メッセージが出るので以下の設定をしておきます。

```bash
sudo vi /etc/pip.conf
```
```:/etc/pip.conf
[list]
format = columns
```

## Python 3.7.x インストール時の追加対応
@hitochan777 さんの [pyenvで3.7系のインストールに失敗したときのメモ](https://qiita.com/hitochan777/items/941d4422c53978b275f8) を参考に以下のパッケージも事前にインストールしておきます。

```bash
yum install libffi-devel
```

## ruby からの警告対応
もともと /usr/local/pyenv/shims ディレクトリは world writable にしていましたが、ruby の一部の gem から以下のような警告が出るようになりました。

```
/opt/td-agent/embedded/lib/ruby/gems/2.1.0/gems/ruby-progressbar-1.8.3/lib/ruby-progressbar/calculators/length.rb:76: warning: Insecure world writable dir /usr/local/pyenv/shims in PATH, mode 042777
```

そこで pyenv-init, pyenv-which, pyenv-rehash を修正して shims ディレクトリのみ個人で持たせるように修正し、/usr/local/pyenv/shims を使用しないようにしました。この修正に伴い、例えば管理者が awscli をインストールした場合、各自が

```bash
pyenv rehash
```
を実行しないと反映されないという副作用が生じます。もちろん再ログインすれば pyenv init から pyenv rehash が実行されるので問題はありません。