• 作成:

HaskellプログラムをGitHub Actionsでビルドしてクロスプラットフォーム向けにバイナリをReleaseにアップロードする

あくまでクロスビルド/クロスコンパイルではなく、クロスプラットフォームでビルドしてまとめてアップロードする方法です。

参考サイト

作ったプログラム

ncaq/homura-stopwatch

このプログラムの作成自体は他のプロジェクトのビルド待ち時間に書いてたら即座に終わりました。

hourglass :: Stackage Server という簡単に時間変換が出来るライブラリのおかげでもありますが。

time :: Stackage Server は日時って感じで時間を変換するのには向いてない気がしました。 timeDiffのようにさっくり時間と時間を比較する関数とかあれば使いましたが…

やりたかったこと

Linuxで開発しているのでMacやWindowsをいちいち起動するのは面倒なので、 tag付けしてpushしたら自動的に各プラットフォームでReleaseにバイナリをアップロードして欲しい。

CircleCIでも良かったかもしれませんが、これまでGitHub Actionsを使ったことが無かったのでとりあえず使ってみたかったのです。

workflowsファイル

name: Main
on: push
jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macOS-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v2
      - if: runner.os == 'Windows' # 何故かWindowsだけキャッシュがダウンロードされないことがある(不確定)
        uses: actions/cache@v1
        with:
          path: ~\AppData\Local\Programs\stack
          key: ${{ runner.os }}-stack-${{ hashFiles('**/stack.yaml.lock') }}-${{ hashFiles('**/package.yaml') }}
          restore-keys: |
            ${{ runner.os }}-stack-${{ hashFiles('**/stack.yaml.lock') }}-
      - if: runner.os != 'Windows'
        uses: actions/cache@v1
        with:
          path: ~/.stack
          key: ${{ runner.os }}-stack-${{ hashFiles('**/stack.yaml.lock') }}-${{ hashFiles('**/package.yaml') }}
          restore-keys: |
            ${{ runner.os }}-stack-${{ hashFiles('**/stack.yaml.lock') }}-
      - uses: mstksg/setup-stack@v2
      - if: runner.os == 'Linux'
        run: |
          stack install hlint
          hlint .
      - run: stack build
      - run: stack install --local-bin-path dist
      - if: runner.os != 'Windows'
        run: |
          cd dist
          zip homura-stopwatch-release-binary-${{ matrix.os }}.zip *
          rm homura-stopwatch
      - uses: actions/upload-artifact@v1
        with:
          name: ${{ matrix.os }}
          path: dist
      - uses: softprops/action-gh-release@v1
        if: startsWith(github.ref, 'refs/tags/')
        with:
          files: "dist/*"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

pushした時毎回実行する

最初はpushした時versionタグが付いているときだけ実行しようと思ったのですが、何故かトリガーが引かれなかったのと(今考えてみるとworkflow追加前のtagだと引かれないのも当然か)、 buildエラーはバージョン付ける前に発見してもらいたいのでビルド自体は毎回行ってアップロードはtagがあるときだけ行うようにしました。

setup-stackを使う

GitHub ActionsにはGitHub公式のHaskell環境である actions/setup-haskell: Set up your GitHub Actions workflow with a specific version of Haskell (GHC and Cabal) があるのですが、これはCabalの使用が前提なのでStackを利用したい私には向いていません。

なので mstksg/setup-stack: Github action for setting up haskell stack を使います。

ここで注意するべき点は、 setup-stackはversion 1ではWindowsに対応してないので、 mstksg/setup-stack@v2を指定する必要があります。

pull requestで修正案が出ています。罠。 Readme: Use version "v2" by andys8 · Pull Request #6 · mstksg/setup-stack

2022-08-25 追記: 公式のsetup-haskellで良い

haskell/actions: Github actions for Haskell CI がStackをサポートするようになったので、公式のものを使えば良いです。

WindowsだとStackのディレクトリ位置が異なるのでキャッシュ時に注意

Linux, MacだとStackのpathは~/.stackですが、 Windowsだと~\AppData\Local\Programs\stackなので、 ifを使って分けて実行する必要があります。 yarn cache dirのようなキャッシュディレクトリを出力するコマンドはStackには見つからなかったので、手で分岐します。

Windowsだとキャッシュが見つからないことがあります

絶対キャッシュが保存されたはずなのに次のビルドでキャッシュが見つからないことがWindowsでのみ発生しました。その次の次ぐらいのビルドだとダウンロードされたので、反映が遅いのかもしれません。

hlintが現在(ghc-8.8.1)Windowsだと動かないのでLinuxでのみ動かす

なんか動かないそうです。 ghci ghc-lib-parser load test fails on windows with ghc-8.8.1 · Issue #142 · digital-asset/ghc-lib

hlintはsyntaxに対して動くツールなのでクロスプラットフォームで動かす必要はないので、 Linuxでのみ動かすようにしました。

softprops/action-gh-releaseを使う

softprops/action-gh-release: 📦 GitHub Action for creating GitHub Releases を使うとリリース時のアップロードの様々な面倒をサクッと処理してくれるようです。

バイナリが置かれる場所がよく分からない

プラットフォームごとに置かれる場所把握するのだるすぎる。

stack install --local-bin-path dist

でワーキングディレクトリに置くことで解決。

Releaseのassetに同じ名前が使えないのでzipにしてしまう

Windowsだと実行ファイルには.exe拡張子が付きますが、 LinuxとMacだと実行ファイル名は同じになります。

同じ名前のファイルはReleaseにアップロード出来ません。

(node:1110) UnhandledPromiseRejectionWarning: HttpError: Validation Failed: {"resource":"ReleaseAsset","code":"already_exists","field":"name"}

とエラーになります。

実行バイナリファイルの名前をプラットフォームごとに分けることも考えましたが、コマンド名が変更になってしまうのでそれはイヤですね。

仕方ないのでzipに単体ファイルを格納することにしました。 zipファイルにプラットフォーム名を入れます。単体ファイルは削除。サムライズムに怒られそう。 zipファイルをお送り頂いた場合の手数料について | 株式会社サムライズム

まあここで書かれているzipファイルの問題点は

  • 文字コードが違う → 解凍ターゲットと同じプラットフォームでアーカイブしているので差異が発生しにくい
  • ディレクトリ構成 → 単体ファイルなので関係ない
  • パスワード → かけてない

ので発生しないので問題ないと判断しました。

tarの方が良いかと思いましたが、 tar.zstが受け取り先のmacで対応されているのかよくわからないので、 tar.xzを使っても単体ファイルとか大して圧縮されないし単にzipで良いと判断しました。

GitHub ActionsのWindowsはtarとか使えるようなのでzipも普通に使えると思ってzipをさせてみましたが、謎にエラーになりました。解決しようかと思ったのですが、 Windowsは元よりexe拡張子で名前が分けられているのでアーカイブする必要も無いかと判断してそのままにすることにしました。

7zipを使えば良いのかなと思いましたが、 GitHubホストランナーにインストールされるソフトウェア - GitHub ヘルプによるとMacには入ってないらしいですね。

やっぱりzipはクソだな!(結論)

Linux以外がやたらと遅いことがある

Macだけ異常に(5倍ぐらい?)遅いことがビルド回していて頻繁にありました。

ビルドが遅いとかそういう次元ではなく、ステップを進めるのに数分かかっていました。

Twitterで教えてもらったのですが、 MacはAzureを使っているわけではなく外注だったそうですね。

GitHubホストランナーの仮想環境 - GitHub ヘルプ

でも今日回してみたらWindowsが一番遅かったですね… 休日はMacのリソース開きがちなんですかね? 確かにMacのクラウドサーバとかCI/CDにしか使われなさそうなので開発を行わない休日には空きそうですね。 Azureは休日にも各サービスでガンガン使うでしょうけど。

Linuxが常に爆速なのは謎です。

クロスプラットフォームを意識しないならやっぱりCircleCIが良さそうに思えます

Travis CI, AppVeyor, CircleCI, Azure Pipelines, GitHub Actionsを使った感想ですが、 Linuxだけで良いならやはりCircleCIを使うのが良さそうだと思いました。

GitHub Actionsはトリガーがよく分からないとか、実行中のログが表示されるのが遅いとか、エラー時のログが意味不明だとか、なんか起動が遅いとか、はっきりとした問題点として言いづらいけど何かUXが悪いです。

それでもメモリが7GB使えてクロスプラットフォームが無料で使えるのは魅力的なので、クロスプラットフォーム向けにビルドする時は解決策になります。少なくとも元になったであろうAzure Pipelinesよりはよほどマシですね(アレはひどかった)。