🎆

pipenvで作った複数の仮想環境を管理し、不要になったら削除する話

2022/03/20に公開

pipenvを使って開発を進めるにつれ、仮想環境が増えていきます。PCのディスク容量が減ったり、開発が一段落したなどの理由で仮想環境をまとめて削除したいことがあります。しかしながら、pipenvでは複数の仮想環境の操作はできません。

そこで、仮想環境の一覧を表示するスクリプトと、ソースコードをローカルから削除して不要になった仮想環境を削除するシェルスクリプトをまとめました。

コードについて

サンプルコードを https://github.com/Niccari/pipenv-virtualenvs-maintainer に置いています。

※ 本記事のコードはmacOS 12.3~12.5でのみ動作検証しています。

説明に関する前提

~/devに下記のPythonプロジェクトがあり、それぞれ仮想環境を作成済みとします。

  • bar-project
  • foo-project
  • hoge-project

1. 仮想環境の一覧を表示する

pipenvの仮想環境は以下のパスに構築されています[1]

  • mac, linux: ~/.local/share/virtualenvs/1ソースコードの親ディレクトリ名-ユニークな文字列[2]
  • windows: %USERPROFILE%/.virtualenvs/1ソースコードの親ディレクトリ名-ユニークな文字列

仮想環境のパス下について、対応するソースコードのフルパスが.project内に記入されています。これにより、ソースコードのパスと仮想環境のパスを対応づけることができます。

bar-projectでのソースコードのパス表示例

$ cat ~/.local/share/virtualenvs/bar-project-NTU4ZjA2/.project
/Users/some-user/dev/bar-project

以上により、仮想環境の一覧(ソースコードのパスと仮想環境のパスの対応表)を以下のスクリプトで表示できます。

仮想環境の一覧表示スクリプト

#!/bin/bash

function get_virtualenvs_path {
  path=$1
  # Check if filesystem is based on Windows or not.
  if [[ "${path}" =~ ^[a-zA-Z]:.*$ ]] || [[ "${path}" =~ ^\/mnt\/[a-zA-Z]\/.*$ ]]; then
    echo "${USERPROFILE}/.virtualenvs"
  else
    echo "${HOME}/.local/share/virtualenvs"
  fi
}

VIRTUALENVS_PATH=$(get_virtualenvs_path $0)
PROJECT_BASENAME=".project"

project_paths=$(ls ${VIRTUALENVS_PATH}/*/${PROJECT_BASENAME})
messages="PROJECT_PATH\tCODE_PATH"

for project_path in ${project_paths[@]}; do
  code_path=$(cat $project_path)
  messages="$messages\n$project_path\t$code_path"
done

echo -e $messages

仮想環境の一覧の例

PROJECT_PATH	CODE_PATH
/Users/some-user/.local/share/virtualenvs/foo-project-7KRJjpxu/.project	/Users/some-user/dev/foo-project
/Users/some-user/.local/share/virtualenvs/hoge-project-Pw7hUalW/.project	/Users/some-user/dev/hoge-project
/Users/some-user/.local/share/virtualenvs/bar-project-NTU4ZjA2/.project	/Users/some-user/dev/bar-project

このうち、CODE_PATHが存在しない仮想環境は削除することができます。そのため、以下スクリプトにより削除できる仮想環境とそうでないものをリストアップできます。なお、出力結果ではUNLINKED列で仮想環境を削除可能かどうか、VIRTUALENV_SIZE列で仮想環境のファイルサイズを表示しています。

仮想環境の一覧表示スクリプト(UNLINKED列、VIRTUALENV_SIZE列を追加)

list_projects.sh
#!/bin/bash

function get_virtualenvs_path {
  path=$1
  # Check if filesystem is based on Windows or not.
  if [[ "${path}" =~ ^[a-zA-Z]:.*$ ]] || [[ "${path}" =~ ^\/mnt\/[a-zA-Z]\/.*$ ]]; then
    echo "${USERPROFILE}/.virtualenvs"
  else
    echo "${HOME}/.local/share/virtualenvs"
  fi
}

VIRTUALENVS_PATH=$(get_virtualenvs_path $0)
PROJECT_BASENAME=".project"

project_paths=$(find ${VIRTUALENVS_PATH} -name ${PROJECT_BASENAME} -type f)
messages="PROJECT_PATH\tCODE_PATH\tUNLINKED\tVIRTUALENV_SIZE"

for project_path in ${project_paths[@]}; do
  code_path=$(cat $project_path)
  if [ ! -d $code_path ]; then
    unlinked="o"
  else
    unlinked="-"
  fi
  virtualenv_path=${project_path//\/${PROJECT_BASENAME}}
  virtualenv_size=$(du -hs $virtualenv_path | cut -f 1)

  messages="$messages\n$project_path\t$code_path\t$unlinked\t$virtualenv_size"
done

echo -e $messages

仮想環境の一覧の例(UNLINED列、VIRTUALENV_SIZE列を追加)

PROJECT_PATH	CODE_PATH	UNLINKED	VIRTUALENV_SIZE
/Users/some-user/.local/share/virtualenvs/foo-project-7KRJjpxu/.project	/Users/some-user/dev/foo-project	-	 24M
/Users/some-user/.local/share/virtualenvs/hoge-project-Pw7hUalW/.project	/Users/some-user/dev/hoge-project	-	105M
/Users/some-user/.local/share/virtualenvs/bar-project-NTU4ZjA2/.project	/Users/some-user/dev/bar-project	-	201M

各プロジェクトの内、/Users/some-user/dev/hoge-project/Users/some-user/dev/bar-projectを削除すると以下の様になります。両プロジェクトについて、UNLINKED列が"o"となっていることが確認できます。

仮想環境の一覧の例(hoge-project, bar-project削除後)

PROJECT_PATH	CODE_PATH	UNLINKED	VIRTUALENV_SIZE
/Users/some-user/.local/share/virtualenvs/foo-project-7KRJjpxu/.project	/Users/some-user/dev/foo-project	-	 24M
/Users/some-user/.local/share/virtualenvs/hoge-project-Pw7hUalW/.project	/Users/some-user/dev/hoge-project	o	105M
/Users/some-user/.local/share/virtualenvs/bar-project-NTU4ZjA2/.project	/Users/some-user/dev/bar-project	o	201M

2. 削除可能な仮想環境を削除する

削除可能な仮想環境を以下のスクリプトにより削除できます。処理の流れですが、1.節同様にソースコードのない仮想環境のリストを抽出して、それらを表示・確認の上で削除するようにしています。

delete_unused_projects.sh
#!/bin/bash

function get_virtualenvs_path {
  path=$1
  # Check if filesystem is based on Windows or not.
  if [[ "${path}" =~ ^[a-zA-Z]:.*$ ]] || [[ "${path}" =~ ^\/mnt\/[a-zA-Z]\/.*$ ]]; then
    echo "${USERPROFILE}/.virtualenvs"
  else
    echo "${HOME}/.local/share/virtualenvs"
  fi
}

VIRTUALENVS_PATH=$(get_virtualenvs_path $0)
PROJECT_BASENAME=".project"

project_paths=$(find ${VIRTUALENVS_PATH} -name ${PROJECT_BASENAME} -type f)
deletable_virtualenv_paths=""
found_projects_message="## Deletable projects ##"
for project_path in ${project_paths[@]}; do
  code_path=$(cat $project_path)
  if [ ! -d $code_path ]; then
    virtualenv_path=${project_path//\/${PROJECT_BASENAME}}
    virtualenv_size=$(du -hs $virtualenv_path | cut -f 1)

    deletable_virtualenv_paths="$virtualenv_path $deletable_virtualenv_paths"
    found_projects_message="$found_projects_message\n$virtualenv_path <-x- $code_path(size: $virtualenv_size)"
  fi
done
deletable_virtualenv_paths=( $deletable_virtualenv_paths )

if [ ${#deletable_virtualenv_paths[@]} -le 0 ]; then
  echo "no deletable virtualenvs found."
  exit 0
else
  echo -e $found_projects_message
fi

read -p "Delete unused virtualenvs? (y/N): " yn
if [[ $yn = [yY] ]]; then
  for virtualenv_path in ${deletable_virtualenv_paths[@]}; do
    if ! rm -i -rf $virtualenv_path ; then
      echo "failed to remove $virtualenv_path."
      exit 1
    fi
  done
  echo "succeeded."
else
  echo "aborted."
fi

仮想環境の削除例

$ bash delete_unused_projects.sh
## Deletable projects ##
/Users/some-user/.local/share/virtualenvs/hoge-project-Pw7hUalW <-x- /Users/some-user/dev/hoge-project(size: 105M)
/Users/some-user/.local/share/virtualenvs/bar-project-NTU4ZjA2 <-x- /Users/some-user/dev/bar-project(size: 201M)
Delete unused virtualenvs? (y/N): y
succeeded.

$ bash delete_pipenv_removable_projects.sh
no deletable virtualenvs found.

Appendix: 各仮想環境のpythonバージョンをリストアップしたい場合

各仮想環境下にあるpyvenv.cfgには以下の情報が記載されています。ここからversion_infoを切り出すとOKです。python --versionを都度実行してもOKですが、それだとpython立ち上げに一定の時間を要してしまいます。

pyvenv.cfg
home = /opt/homebrew/opt/python@3.11/bin
implementation = CPython
version_info = 3.11.2.final.0
virtualenv = 20.21.0
include-system-site-packages = false
base-prefix = /opt/homebrew/opt/python@3.11/Frameworks/Python.framework/Versions/3.11
base-exec-prefix = /opt/homebrew/opt/python@3.11/Frameworks/Python.framework/Versions/3.11
base-executable = /opt/homebrew/opt/python@3.11/bin/python3.11
prompt = sandbox
list_projects.sh
# (途中省略)
echo -e "PROJECT_PATH\tCODE_PATH\tPYTHON_VERSION\tUNLINKED\tVIRTUALENV_SIZE"

for project_path in ${project_paths[@]}; do
    # (途中省略)
    python_version=$(cat ${virtualenv_path}/pyvenv.cfg | grep version_info | cut -d "=" -f 2)
    echo -e "$project_path\t$code_path\t$python_version\t$unlinked\t$virtualenv_size"
done
脚注
  1. 環境変数でWORKON_HOMEを指定している場合はWORKON_HOME下、PIPENV_VENV_IN_PROJECTを指定している場合はソースコード直下になります ↩︎

  2. ユニークな文字列は、フルパスから算出したsha256ハッシュ値を変換したbase64文字列のうち、先頭8文字です ↩︎

Discussion