背景
最近はデスクトップでもサーバーでもARMが躍進しています。 macOSのApple Silliconはもちろん、LinuxでもAWSのGraviton2やRaspberry PiなどでARMマシンが多くの人に使われています。WindowsのARM版も今後増えてくるかもしれません。
Python拡張モジュールのメンテナとしては、そういった環境のためにバイナリwheelを提供してあげたいところですが、ビルド環境を用意するのが大変になってしまいます。そこで役に立つのが cibuildwheel です。
cibuildwheelについて
公式サイト:
cibuildwheel はCI環境上で複数の環境向けのwheelをビルドするためのツールです。現在のCIごとの対応プラットフォームは下の表の通りです。(上記公式サイトより)
GitHub Actions では全てのプラットフォームがサポートされているのがわかります。
対応しているCPUアーキテクチャは次のようになります。
- Windowsではx86_64とx86のみ
- arm64対応はまだexperimentalで、CI側がRunnerを用意するのを待つか自前でarm64版Windowsを用意する必要がある
- macOSではx86_64, arm64, universal2
- Linuxではx86_64, x86, arm64, ppc64le s390x
cibuildwheel がどうやってwheelをビルドしているか、現時点(v2.2.2)での振る舞いを簡単に説明しておきます。
Windows
nuget を使って、 wheel を作りたいバージョンとアーキテクチャのPythonをインストールし、その上でwheelをビルドします。
クロスコンパイルやエミュレーションはしていないので、ARM64版のビルドにはARM64版のWindowsが必要になります。
macOS
Python公式サイト (www.python.org) からPythonをダウンロードしてインストールし、その上でwheelをビルドします。
公式サイトのPythonがuniversal2バイナリになっているPython 3.8以降では、arm64版とuniversal2版のwheelをクロスコンパイルできますが、amd64版のRunnerではテストを実行できない(arm64版をエミュレーションできない)のでデフォルトでは無効になっています。
将来的に Apple Sillicon 版の Runner が提供されたときに、そちらでarm64版とuniversal2版のビルドをする予定になっています。
Linux
DockerとPyPAが提供しているmanylinuxやmusllinuxなどのイメージを使ってwheelをビルドしてます。
amd64とi686以外にもqemuを使うことでaarch64(arm64)やppc64le, s390xのwheelをビルドし、テストを実行できます。
注意点
上で説明した通り、cibuildwheelはCIのRunnerが提供しているPythonではなくて自力でPythonをダウンロードして実行するようになっています。
そのため、CIが提供している標準的な方法 (GitHub Actions の setup-python など) を使う場合に比べて時間もかかりますし、外部サービス (www.python.org, nuget, Dockerイメージを配布しているRed Hatのquay.io) にも負荷をかける事になります。
amd64以外のいろいろなアーキテクチャを使えるのは魅力的ですが、 GitHub の push や pull_request といった頻繁に起動されるイベントで実行するのはお勧めしません。
msgpack の例
msgpackのwheelのビルドを、自前で書いていたスクリプトからcibuildwheelに移行してみました。この設定ファイルを解説していきます。
name: Build Wheels
on:
push:
branches: [main]
create:
プルリクをマージしたときと、タグを作成したときだけに実行するため、このような設定にしました。このファイル自体を編集している間は pull_request:
を追加して、マージする直前に外しました。
jobs:
build_wheels:
strategy:
matrix:
os: [ubuntu-20.04, windows-2022, macos-10.15]
runs-on: ${{ matrix.os }}
name: Build wheels on ${{ matrix.os }}
各プラットフォームでwheelをビルドするために matrix の設定をしています。
CPUアーキテクチャやPythonのバージョンはcibuildwheelが面倒を見るのでmatrixにする必要はありません。
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
if: runner.os == 'Linux'
uses: docker/setup-qemu-action@v1
with:
platforms: arm64
Linuxでarm64版のバイナリを作成したいので、Dockerでqemuを使えるようにしています。
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
cache: "pip"
- name: Prepare
shell: bash
run: |
pip install -r requirements.txt
make cython
Cythonを使ってコード生成している部分です。これはmsgpackがCythonを使っているためで、cibuildwheelとは関係ないです。
- name: Build
uses: pypa/cibuildwheel@v2.2.2
env:
CIBW_TEST_REQUIRES: "pytest"
CIBW_TEST_COMMAND: "pytest {package}/test"
CIBW_ARCHS_LINUX: auto aarch64
CIBW_ARCHS_MACOS: x86_64 universal2 arm64
CIBW_SKIP: pp*
pypa/cibuildwheel
Actionを使って cibuildwheel を実行しています。(他にはpipでcibuildwheelをインストールして実行する方法もあります)
Actionを使う場合は環境変数で設定をします。
-
CIBW_TEST_REQUIRES
は、テストを実行するために追加でインストールするパッケージです。LinuxではDockerの中でテストを実行するので、ホスト側のPythonでpip install pytest
しても意味がありません。 -
CIBW_TEST_COMMAND
はテストを実行するためのコマンドです。このコマンドが実行される時のカレントディレクトリがパッケージの場所とは限らない(特にDockerで)ので、{package}
というプレースホルダを使っています。 -
CIBW_ARCHS_LINUX
はLinuxでビルドするCPUアーキテクチャです。デフォルトだとautoでamd64とx86だけなので、 aarch64 を追加しています。 -
CIBW_ARCHS_MACOS
はmacOSでビルドするCPUアーキテクチャです。上でも触れた通り、amd64のRunnerだとデフォルトでamd64版しかビルドしてくれないので、 universal2 と arm64 を追加しています。 -
CIBW_SKIP
はビルドから除外する対象をタグで指定するものです。 msgpack は PyPy で拡張モジュールを使うと逆に遅くなるのでPyPyを除外しています。 CPythonのprefixがcp
で PyPy のprefixがpp
なのでpp*
でPyPyを除外しています。
この他にもいろいろな設定があります。詳細はドキュメントを参照してください。
- name: Upload Wheels
uses: actions/upload-artifact@v1
with:
name: Wheels
path: wheelhouse
最後に、ビルドしたwheelをartifactにアップロードしています。 cibuildwheel が wheel を出力するデフォルトのディレクトリが wheelhouse
になっているので、そのディレクトリをアップロード対象として指定しています。