Mac(M1 CPU)で、互換性のあるpyenv+pipenvの環境を作る

こんにちは、ピリカ開発チームの九鬼です。

SNSピリカやタカノメのサービスでは、バックエンドで度々Pythonを使用しています。その中でいくつかPythonスクリプトがあり、バージョンが3.7系列や3.9系列などが含まれます。

しかしながら、Mac(M1 CPU)からarm64ベースのアーキテクチャになったこともあり、一部Pythonバージョンや一部ライブラリがインストールできないことが多々あります。

そこで、複数のPythonバージョンで仮想環境を切り分けられるよう、pyenv + pipenvの環境を整えました。

(2022.03.21 追記) MacOS Monterey(12.3)からApple Clangが13.1.6になり、過去のPythonバージョンに適合しないようになっています。そのためpyenvにおいて、x86_64・arm64環境の両方で以下バージョンがインストールできません(後述のエラーが出ます)。

  • 3.7系: 3.7.12以下
  • 3.8系: 3.8.12以下
  • 3.9系: 3.9.7以下
  • 3.10系: 3.10.0

その場合、より新しいバージョンをインストールするか、旧バージョンにパッチが出るまでお待ち下さい(参考ページ: pyenv issue#2143)

ビルド時のエラー内容 configure: error: internal configure error for the platform triplet, please file a bug report

モチベーション

  • Pythonを使う限りは、なるべくarm64環境で動かした方がパフォーマンスが良いです
  • 互換性のため、Python 3.8未満の古いバージョンも使えることが必要です

前提知識

  • Python 3.8未満はm1(arm64)サポートしていません (python bug tracker メッセージ382939より)。セキュリティアップデートのみが提供されるバージョンのため、今後もサポートされません1
  • x86_64環境でも、pyenvで3.7.8未満をビルドするとインストールに失敗します。当該バージョンを入れたい場合、後述の対策を入れる必要があります
  • pyenv(および各種Python), pipenvともにHomebrewをインストールしたアーキテクチャに合わせる必要があります(合わせない場合、Pythonのビルドや依存ライブラリのインストールに失敗します)
  • pyprojなど、arm64環境でインストールできないライブラリがあります

基本方針

  • Python 3.8未満、もしくはarm環境で動かないライブラリを含む場合: x86_64(Rosetta 2)環境で動かします
  • Python 3.8以上: arm64環境で動かします

構築した環境例

検証環境

ディレクトリ構成

本稿では、以下の構成で環境構築を行うものとします。

  • arm64環境で、Python 3.9.7, 3.9.9, 3.10.0を使用
  • x86_64環境で、Python 3.7.6, 3.7.12, 3.9.7を使用

これにより、インストールされるHomebrew、pyenv、pipenvおよび仮想環境の関係は以下の通りとなります。

arm64環境

/opt/homebrew/bin: arm64向けHomebrew

  • /opt/homebrew/bin/pyenv: arm64向けpyenvバイナリ

~/.pyenv_arm64/: バージョンごとのPython置き場 (>= 3.8.0)

  • ~/.pyenv_arm64/versions/3.9.9/: Python 3.9.9
    • ~/.pyenv_arm64/versions/3.9.9/bin/pipenv: Python 3.9.9環境下のPipenv
  • ~/.pyenv_arm64/shims/pipenv: pipenvのエントリポイント(現在選択しているPythonバージョンに合わせて、pipenvを選択する)

x86_64環境

/usr/local/bin: x86_64向けHomebrew

  • /usr/local/bin/pyenv: x86_64向けpyenvバイナリ

~/.pyenv_x86/: バージョンごとのPython置き場(< 3.8.0)

  • ~/.pyenv_x86/versions/3.7.6/: Python 3.7.6
    • ~/.pyenv_x86/versions/3.7.6/bin/pipenv: Python 3.7.6環境下のPipenv
  • ~/.pyenv_x86/versions/3.7.12/: Python 3.7.12
    • ~/.pyenv_x86/versions/3.7.12/bin/pipenv: Python 3.7.12環境下のPipenv
  • ~/.pyenv_x86/shims/pipenv: pipenvのエントリポイント(現在選択しているPythonバージョンに合わせて、pipenvを選択する)

最終的なインストール構成図
最終的なインストール構成図

構築フロー

以下のフローに従って環境構築をすすめます2。 1. (任意) arm64, x86_64環境をコマンドで切り替えられるようにする 2. x86_64版、arm64版両方でHomebrewをインストール、環境構築する 3. x86_64版、arm64版両方でpyenvをインストール、環境構築する 4. 各pyenvの各Pythonバージョンをインストール 5. 各pyenvの各Pythonバージョンでpipenvをインストール

1. (任意) arm64, x86_64環境をコマンドで切り替えられるようにする

以下、~/.zshrcに追記します。これにより、x86を実行するとx86_64環境、armを実行するとarm64環境でシェルが動作するようになります。

alias x86='arch -x86_64 zsh'
alias arm='arch -arm64e zsh'

2. x86_64版、arm64版両方でHomebrewをインストール、環境構築

arm64版を以下のスクリプトでインストールします。

$ uname -m   # arm64環境で動いていることを確認する
arm64

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

arm64版をインストールできていれば、which brewでopt/homebrew/bin/brewがHomebrewの実行パスとして表示されます。

$ which brew
opt/homebrew/bin/brew

x86_64版を以下のスクリプトでインストールします。

$ x86
$ uname -m   # x86_64環境で動かせていることを確認する
x86_64

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

x86_64版をインストールできていれば、which brewで/usr/local/bin/brewがHomebrewの実行パスとして表示されます。

$ which brew
/usr/local/bin/brew

最後に、~/.zshrcで以下を追記し、brewのパスがPATHに入るようにします。

if [ "$(uname -m)" = "arm64" ]; then
  eval "$(/opt/homebrew/bin/brew shellenv)"
  export PATH="/opt/homebrew/bin:$PATH"
else
  eval "$(/usr/local/bin/brew shellenv)"
fi

x86armを実行したときにPATHがアップデートされるよう、~/.zshrcに記載しています。ログインシェルにのみ適用させるのであれば~/.zprofileに追記すべきですが、そうするとx86arm実行後にbrewの実行パスが切り替わりません。また、x86,arm実行の度に上記pathがどんどん追記されますが、実用上問題ありません。

この時点で、下図の通りx86_64版, arm64版のHomebrew両方が実行可能になります。

Homebrewインストール後の構成図
Homebrewインストール後の構成図

3. x86_64版、arm64版両方でpyenvをインストール、環境構築

arm版のHomebrewでpyenvをインストールします。

$ uname -m   # もし結果がx86_64であれば、`arm`を実行後に再度実行する
arm64

$ brew install pyenv

x86_64版のHomebrewでpyenvをインストールします。

$ uname -m   # もし結果がarm64であれば、`x86`を実行後に再度実行する
x86_64

$ brew install pyenv

最後に、~/.zshrcで以下を追記し、pyenvの初期化と実行パス追加が実行されるようにします。arm64版のpyenvは~/.pyenv_arm64、x86_64版のpyenvは~/.pyenv_x86下に各Pythonバージョンがインストールされるようにします。

なお、eval "$(pyenv init -)"では実行パス追加が行われないので、eval $(pyenv init --path)も合わせて実行する必要があります3

if [ "$(uname -m)" = "arm64" ]; then
  export PYENV_ROOT="$HOME/.pyenv_arm64"
  export PATH="$HOME/.pyenv_arm64/bin:$PATH"
else
  export PYENV_ROOT="$HOME/.pyenv_x86"
  export PATH="$HOME/.pyenv_x86/bin:$PATH"
fi
eval "$(pyenv init -)"
eval "$(pyenv init --path)"

pyenvインストール後の構成図
pyenvインストール後の構成図

4. 各pyenvの各Pythonバージョンをインストール

Python 3.8以上はarm64版のpyenvでインストールします。また、Python 3.7以下もしくはx86_64環境のPythonが必要な場合はx86_64版のpyenvでインストールします。

Python 3.9.9をインストールする場合

$ arm
$ where pyenv    # pyenv () {} の次に、/opt/homebrew/bin/pyenvが入っているはず

pyenv () {
...
}
/opt/homebrew/bin/pyenv
...

$ pyenv install 3.9.9

Python 3.7.12をインストールする場合

$ x86
$ where pyenv    # pyenv () {} の次に、/usr/local/bin/pyenvが入っているはず

pyenv () {
...
}
/usr/local/bin/pyenv
...

$ pyenv install 3.7.12

上記のプロセスに従って、残りのPythonバージョンについてもインストールしておきます。

なお、Python 3.7.8未満もしくは3.8.4未満バージョンはそのままではインストールできません。これらよりも新しいバージョンをインストールするか、インストール時にPythonビルドに必要な依存ライブラリのパス指定やパッチ適用を行う必要があります。

4.1. Python 3.7.6

x86_64環境でも、単純にpyenv install 3.7.6とするとビルドに失敗します。

BUILD FAILED 
...
./Modules/posixmodule.c:9197:15: error: implicit declaration of function 'sendfile' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
        ret = sendfile(in, out, offset, &sbytes, &sf, flags);
              ^
1 error generated.
make: *** [Modules/posixmodule.o] Error 1
make: *** Waiting for unfinished jobs....
1 warning generated.

以下の通りパラメータを加えた上でPython 3.7.6をビルドするとインストールできます。 - Big Sur以降向けのパッチを当てる - pythonビルド時にopenssl, bzip2, readline, zlibのライブラリパス, インクルードパスを指定

CFLAGS="-I$(brew --prefix openssl)/include -I$(brew --prefix bzip2)/include -I$(brew --prefix readline)/include -I$(xcrun --show-sdk-path)/usr/include" LDFLAGS="-L$(brew --prefix openssl)/lib -L$(brew --prefix readline)/lib -L$(brew --prefix zlib)/lib -L$(brew --prefix bzip2)/lib" pyenv install --patch 3.7.6 < <(curl -sSL https://github.com/python/cpython/commit/8ea6353.patch\?full_index\=1)

Python 3.8.3の場合

こちらも同様に、posixmodule.c周りのビルドでエラーが発生します。パッチおよびインクルードパス等を指定してビルドしてください。

BUILD FAILED 
...
./Modules/posixmodule.c:9197:15: error: implicit declaration of function 'sendfile' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
        ret = sendfile(in, out, offset, &sbytes, &sf, flags);
              ^
1 error generated.
make: *** [Modules/posixmodule.o] Error 1
make: *** Waiting for unfinished jobs....
1 warning generated.
CFLAGS="-I$(brew --prefix openssl)/include -I$(brew --prefix bzip2)/include -I$(brew --prefix readline)/include -I$(xcrun --show-sdk-path)/usr/include" LDFLAGS="-L$(brew --prefix openssl)/lib -L$(brew --prefix readline)/lib -L$(brew --prefix zlib)/lib -L$(brew --prefix bzip2)/lib" pyenv install --patch 3.8.3 < <(curl -sSL https://github.com/python/cpython/commit/8ea6353.patch\?full_index\=1)

ひとしきりPythonバージョンのインストールができると、下図の状態になります。

各Pythonバージョンインストール後の構成図
Pythonバージョンインストール後の構成図

5. 各pyenvの各Pythonバージョンでpipenvをインストール

pyenvで適宜Pythonバージョンを切り替えつつ、各Pythonバージョンごとにpipenvをインストールします。pipenvはpythonによる1ライブラリなので、Pythonバージョンごとにpip installする必要があります。

Python 3.7.12での例

$ x86
$ pyenv global 3.7.12
$ pyenv versions   # 今、どのpythonバージョンを使っているか確認
  system
* 3.7.12 (set by /Users/ユーザ名/.pyenv_x86/version)
  3.7.6

$ pip install pipenv  # python 3.7.12環境にpipenvがインストールされる
...

$ pipenv --version
pipenv, version 2021.11.23

Python 3.9.9での例

$ arm
$ pyenv global 3.9.9
$ pyenv versions   # 今、どのpythonバージョンを使っているか確認
  system
* 3.9.9 (set by /Users/ユーザ名/.pyenv_arm64/version)

$ pip install pipenv  # python 3.9.9環境にpipenvがインストールされる
...

$ pipenv --version
pipenv, version 2021.11.23

なお、pyenvで選択したPython環境にpipenvが入っていない場合は以下のエラーとなります。

$ arm
$ pyenv global 3.10.1
$ pipenv --version
pyenv: version `3.10.1' is not installed (set by PYENV_VERSION environment variable)

あとは同様にして、各Pythonバージョンごとにpipenvをインストールします。完了すると、下図の状態になります。

pipenvインストール後の構成図
pipenvインストール後の構成図

6. Pythonによるプロジェクトごとに仮想環境を作る

使いたいPythonのバージョンを指定しつつ、仮想環境を使用します。下記の様に、使用したいPythonを明示的に指定する必要があります。pipenvでは複数マイナーバージョンの環境があるとき、最新のものを使おうとするためです(pipenv globalpipenv shellで明示的にマイナーバージョンを指定しても同様)。

pipenv install --python 3.9.7

なお、.python-versionが存在していれば、下記の様に指定することもできます。

pipenv install --python $(cat .python-version)

以上になります、ご覧いただきありがとうございます!


  1. ちなみにPython 3.8もセキュリティアップデートのみ提供される段階に入っているものの、arm64環境で動作するようサポートされています

  2. M1MacのRosettaとARM環境にpyenv + pipenvの環境構築を行うによる環境構築を参考にいたしました

  3. pyenv init -のみを実行してみると、"WARNING: `pyenv init -` no longer sets PATH.“と警告されるようになっています