Github Actions を使って Windows 用 Wheel をビルドした

[mysqlclient](https://github.com/pymysql/mysqlclient-python) という Python 用の libmysqlclient を利用したクライアントライブラリをメンテナンスしています。

このライブラリでは Windows ユーザーが簡単に利用できるように libmysqlclient を静的リンクした Binary Wheel を提供しています。
この Binary Wheel のビルドを Github Actions を利用して自動化してみました。

Github Actions 初心者なのでもっといいやり方があるかもしれませんが、だれかの参考になれば。

## 感想

先に感想から書いておきます。すでに他のプロジェクトで AppVeyor を使って Windows 用の Wheel を自動ビルドした経験があったのですが、それよりも圧倒的に快適でした。

* VM起動が早い -- AppVeyor に比べて push してからVMが立ち上がるまでの時間が圧倒的に短いように感じました。 AppVeyor を使っていたときは push するたびに数分待っていたのですが、 Github Actions ではサクサクと試行錯誤することができました。
* Cache 万歳 -- 今回はビルドに数分かかる依存ライブラリがあったのですが、それをcacheすることで本体の試行錯誤をサクサクと進めることができました。
* step ごとに違うシェルを選べる -- cmd, PowerShell, そして Git for Windows の bash を、 step ごとに選択して書くことができます。 cmd や PowerShell には慣れていないので、どうやるのか迷った時にすぐに bash に逃げれるのは楽でした。
* Artifact が楽 -- ビルドした Wheel を Artifact として登録し、ダウンロードすることができます。これは AppVeyor でもできたのですが、Github Actionsの方がダウンロードするまでも楽ですし、zip化されているので複数のファイルを1つずつダウンロードしなくていいのも楽だと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1969/c4abab66-c462-dbaa-7ba9-da7f30cd69b3.png)

(このうちいくつかは実際にはAppVeyorでも同じことができるかもしれません。あくまでも個人的な感想です。)


## ビルド用のリポジトリを用意

ライブラリ本体のリポジトリを使う代わりに、ビルド用の別のリポジトリ [mysqlclient-build](https://github.com/pymysql/mysqlclient-build) を作ってそこで試行錯誤していきました。これによりライブラリのリポジトリを汚すことなく試行錯誤することができます。

```.github/workflows/windows.yaml
name: Build windows wheels
on:
  push:
    branches:
      - master
  create:
```

後述しますが、これによりチェックアウトするディレクトリが影響を受けてしまい若干面倒だったので、ライブラリ本体のリポジトリのプルリクエストで試行錯誤するのでもよかった気がします。


## 依存ライブラリのビルド

mysqlclient は libmysqlclient を利用しているのですが、Windows用Wheelでは MySQL Connector/C ではなくて互換性のある MariaDB Connector/C を利用しています。

これは MySQL への SSL 接続や、 caching_sha2_password で使う sha256 の計算などに OpenSSL を使わず Windows の API を利用するようにビルドすることができ、静的リンクしたバイナリ配布に適しているからです。

しかし、バイナリ配布されている MariaDB Connector/C は認証などのプラグインが DLL になっています。できればシングルバイナリにしたいので自分で MariaDB Connector/C をビルドします。

mysqlclient をビルドするたびにこの依存ライブラリをビルドすると時間がかかるので cache アクションを使います。 cache アクションは job が成功したときに指定したディレクトリをキャッシュしてくれるので、一旦 MariaDB Connector/C のビルドまでの状態で push して成功させ、キャッシュしておきます。これで mysqlclient をビルドするための試行錯誤をするときは、ビルド済みの MariaDB Connector/C を利用することができました。


```.github/workflows/windows.yaml
jobs:
  build:
    runs-on: windows-latest
    env:
      CONNECTOR_VERSION: "3.1.5"

    steps:
      - name: Cache Connector
        id: cache-connector
        uses: actions/cache@v1
        with:
          path: c:/mariadb-connector
          key: mariadb-connector-3.1.5-win

      - name: Download and Unzip Connector
        if: steps.cache-connector.outputs.cache-hit != 'true'
        shell: bash
        run: |
          curl -LO "https://downloads.mariadb.com/Connectors/c/connector-c-${CONNECTOR_VERSION}/mariadb-connector-c-${CONNECTOR_VERSION}-src.zip"
          unzip "mariadb-connector-c-${CONNECTOR_VERSION}-src.zip" -d c:/
          mv "c:/mariadb-connector-c-${CONNECTOR_VERSION}-src" c:/mariadb-connector-src

      - name: Build Connector
        if: steps.cache-connector.outputs.cache-hit != 'true'
        shell: cmd
        working-directory: c:/mariadb-connector-src
        run: |
          mkdir build
          cd build
          cmake -A x64 .. -DCMAKE_BUILD_TYPE=Release -DCLIENT_PLUGIN_DIALOG=static -DCLIENT_PLUGIN_SHA256_PASSWORD=static -DCLIENT_PLUGIN_CACHING_SHA2_PASSWORD=static
          cmake --build . -j 8 --config Release
          cmake -DCMAKE_INSTALL_PREFIX=c:/mariadb-connector -DCMAKE_INSTALL_COMPONENT=Development -DCMAKE_BUILD_TYPE=Release -P cmake_install.cmake
```

## mysqlclient のビルド

まずは checkout アクションで mysqlclient をチェックアウトします。デフォルトでは workflow があるリポジトリ（今回の場合は mysqlclient-build）を作業ディレクトリにチェックアウトするのですが、 `with:` を使って別のリポジトリを別のパスにチェックアウトすることができます。

ここで一つ落とし穴があったのですが、 `path: mysqlclient` を指定してチェックアウトした場合、チェックアウト先は `./mysqlclient` ではなく `../mysqlclient` になります。デフォルトの作業ディレクトリはそのリポジトリをチェックアウトするためのディレクトリで、別のリポジトリをチェックアウトするときはその下ではなく隣にディレクトリを作ってチェックアウトするという形になるからです。なのでチェックアウトした以降の step では `working-directory: ../mysqlclient` を指定しています。

ビルドした後は upload-artifact というアクションを使って wheel を Github にアップロードし、そのあとその wheel を実際にインストールして import できることを確認しておきます。

できれば動作までテストしたかったのですが、 windows-latest では MySQL Server がインストールされていないので今回はやっていません。 Docker が使えるようなので、 Docker を使って MySQL Server をセットアップできないか今後挑戦してみます。

```.github/workflow/windows.yml
      - name: Checkout mysqlclient
        uses: actions/checkout@v1
        with:
          repository: PyMySQL/mysqlclient-python
          ref: master
          fetch-depth: 10
          path: mysqlclient

      - name: Site Config
        shell: bash
        working-directory: ../mysqlclient
        run: |
          pwd
          find .
          cat <<EOF >site.cfg
          [options]
          static = True
          connector = C:/mariadb-connector
          EOF
          cat site.cfg

      - name: Build wheels
        shell: cmd
        working-directory: ../mysqlclient
        run: |
          py -3.8 -m pip install -U setuptools wheel pip
          py -3.8 setup.py bdist_wheel
          py -3.7 -m pip install -U setuptools wheel pip
          py -3.7 setup.py bdist_wheel
          py -3.6 -m pip install -U setuptools wheel pip
          py -3.6 setup.py bdist_wheel

      - name: Upload Wheel
        uses: actions/upload-artifact@v1
        with:
          name: win-wheels
          path: ../mysqlclient/dist

      - name: Check wheels
        shell: bash
        working-directory: ../mysqlclient/dist
        run: |
          ls -la
          py -3.8 -m pip install mysqlclient-1.4.6-cp38-cp38-win_amd64.whl
          py -3.8 -c "import MySQLdb"
          py -3.7 -m pip install mysqlclient-1.4.6-cp37-cp37m-win_amd64.whl
          py -3.7 -c "import MySQLdb"
          py -3.6 -m pip install mysqlclient-1.4.6-cp36-cp36m-win_amd64.whl
          py -3.6 -c "import MySQLdb"
```