GitHub Actions と cibuildwheel を使ってarm64対応 wheel をビルドする

## 背景

最近はデスクトップでもサーバーでもARMが躍進しています。 macOSのApple Silliconはもちろん、LinuxでもAWSのGraviton2やRaspberry PiなどでARMマシンが多くの人に使われています。WindowsのARM版も今後増えてくるかもしれません。

Python拡張モジュールのメンテナとしては、そういった環境のためにバイナリwheelを提供してあげたいところですが、ビルド環境を用意するのが大変になってしまいます。そこで役に立つのが cibuildwheel です。

## cibuildwheelについて

公式サイト:

https://cibuildwheel.readthedocs.io/en/stable/

cibuildwheel はCI環境上で複数の環境向けのwheelをビルドするためのツールです。現在のCIごとの対応プラットフォームは下の表の通りです。(上記公式サイトより)
GitHub Actions では全てのプラットフォームがサポートされているのがわかります。


> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1969/532b0036-8c8e-e090-7109-81d707150f08.png)
>

対応している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に移行してみました。この設定ファイルを解説していきます。

https://github.com/msgpack/msgpack-python/blob/bdf0511e29b02427437b03e7d8454bd5076e837f/.github/workflows/wheel.yml

```yaml:.github/workflows/wheel.yml
name: Build Wheels
on:
  push:
    branches: [main]
  create:
```

プルリクをマージしたときと、タグを作成したときだけに実行するため、このような設定にしました。このファイル自体を編集している間は `pull_request:` を追加して、マージする直前に外しました。

```yaml
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にする必要はありません。

```yaml
    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を使えるようにしています。

```yaml
      - 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とは関係ないです。

```yaml
      - 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を除外しています。

この他にもいろいろな設定があります。詳細は[ドキュメント](https://cibuildwheel.readthedocs.io/en/stable/options/)を参照してください。

```yaml
      - name: Upload Wheels
        uses: actions/upload-artifact@v1
        with:
          name: Wheels
          path: wheelhouse
```

最後に、ビルドしたwheelをartifactにアップロードしています。 cibuildwheel が wheel を出力するデフォルトのディレクトリが `wheelhouse` になっているので、そのディレクトリをアップロード対象として指定しています。