GitHubのActionでマルチ環境向けのバイナリをビルドして配布する(Rust)

Written on 2021-01-01

CLI(command line interface)ツールはRustでも力を入れてりるターゲット。RustはLLVMをバックエンドとしているし、ライブラリも抽象化されている。GUIを扱わない範囲ではWindows/Linux/Macを対象とした移植性があるCLIツールを書きやすい。さらにGitHubではActionを用いたビルドファーム(テストも)がOSSでは利用可能だ。ソースからビルドではなく、多環境向けのバイナリをGitHub Actionsでビルドしてバイナリ配布するための設定について述べる。

2020冬シーズンの篭もりプロジェクトとしてrcというコマンドラインで動作する関数電卓を作成した。リポジトリはhttps://github.com/nkon/rc-rsに、設計ノートはhttps://github.com/nkon/rc-rs/blob/master/NOTE.mdにて公開している。この記事は整理と閲覧性向上のために、設計ノートからGitHub Actionsに関して抜き出してまとめたものだ。

.github/workflows/rust.ymlにアクションを書いておけば、指定したトリガ(pushをトリガとすることが多い)に対して、CIアクションが走る。それは、ビルドファームでの配布用バイナリビルド、テスト、アクションをトリガとしたGitHubのReleaseページへの追加、などが含まれる。

Windows/Linux/Mac用の配布バイナリを生成するためにGitHub Actionsを使ってみた。Rustはクロスビルドの環境が整っているのでubuntu-latestでもWindowsバイナリを生成可能だがテストのこともあるのでwindows-latestでセルフビルドしてみた。

事前学習としてGitHub/Actionsactionsactions-rsを見ておく必要がある。GitHub/ActionsはGitHubで何ができるのかとういうこt、actionsでは汎用的なアクションを提供しているしaction-rsではRustに特化したActionsを提供している。それらのActionsはYAMLの中で参照すればインポートできる。

設定

GitHubのActionsページに行って[New Actions]ボタンを押せば、主要開発言語であるRustを判別して、適切な初期Actionを設定してくれる。以降はそれの編集について。

具体的な実装についてはhttps://github.com/nkon/rc-rs/blob/master/.github/workflows/rust.ymlがある。以下はそれの解説。

トリガパート

name: Rust

on:
  push:
    tags:
      - 'v*'

env:
  CARGO_TERM_COLOR: always
  • name ⇒好きに名前を付けてよい。
  • on:push:tags:v*push時にtagsが付いていたら発動。VS-Codeの場合はpushではtagはpushされないので、コマンドラインでpushするか(git push --tags)、コマンドパレット(F1)でGit Push Tagsを選択する。

CARGO_TERM_COLOR: はコマンド応答をカラフルに行う工夫。

Build

jobs:
  build:

    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-musl
          - x86_64-pc-windows-msvc
          - x86_64-apple-darwin
        include:
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest
          - target: x86_64-apple-darwin
            os: macos-latest

    runs-on: ${{ matrix.os }}

    steps:
      - name: Setup code
        uses: actions/checkout@v2

      - name: Install musl tools
        if : matrix.target == 'x86_64-unknown-linux-musl'
        run: |
          sudo apt install -qq -y musl-tools --no-install-recommends
      
      - name: Setup Rust toolchain
        uses: actions-rs/toolchain@v1
        with:
          profile: minimal
          toolchain: stable
          target: ${{ matrix.target }}
          override: true

      - name: test
        uses: actions-rs/cargo@v1
        with:
          command: test

      - name: Build
        uses: actions-rs/cargo@v1
        with:
          command: build
          args: --release --target=${{ matrix.target }}

      - name: Package for linux-musl
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: |
          zip --junk-paths rc-${{ matrix.target }} target/${{ matrix.target }}/release/rc

      - name: Package for windows
        if: matrix.target == 'x86_64-pc-windows-msvc'
        run: |
          powershell Compress-Archive -Path target/${{ matrix.target }}/release/rc.exe -DestinationPath rc-${{ matrix.target }}.zip

      - name: Package for macOS
        if: matrix.target == 'x86_64-apple-darwin'
        run: |
          zip --junk-paths rc-${{ matrix.target }} target/${{ matrix.target }}/release/rc

      - uses: actions/upload-artifact@v2
        with:
          name: build-${{ matrix.target }}
          path: rc-${{ matrix.target }}.zip

strategy:matrix:でターゲットを複数定義する。今回はx86_64-unknown-linux-muslx86_64-pc-windows-msvcx86_64-apple-darwinの2種類。それぞれのターゲットに応じてビルドOSを設定する。

runs-osでビルド用のOSを起動。

わかりやすいようにstepsではnameを付けている。muslツールは標準ではないのでaptで追加のセットアップを行う。Rustのツールチェインのセットアップはacsions-rsに用意されているものを使う。その上で、testとbuildを実施。クレートをキャッシュする方法もあるようだが、今回は用いていない。私が試した時は、浮動小数点の演算精度のせいで特定のターゲットでテストがFailしたりした。

Linux版はMUSLによるスタティックリンクされたバイナリ、Windows版でも.cargo/configの記述に従い、スタティックリンクされたバイナリが作成される。

その後zipでパッケージを作成する。しかし、windows-latestではzipコマンドが用意されていない。powershellの内蔵コマンドでzipパッケージを作成する。 パッケージは次のジョブで利用されるのでアーティファクトとしてアップロードしておく。

Create-Release、

  create-release:
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - id: create-release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          draft: false
          prerelease: true
      - run: |
          echo '${{ steps.create-release.outputs.upload_url }}' > release_upload_url.txt
      - uses: actions/upload-artifact@v1
        with:
          name: create-release
          path: release_upload_url.txt

YAMLなのでインデントレベルが重要。create-release:のインデントレベルはbuild:と同じにする。

buildがすべて(3つとも)完了したらcreate-release:がトリガされる。

GITHUB_TOKENはこのように書いておけば勝手に渡してくれる。GitHubにプッシュされたタグからリリース名を作ってactions/create-releaseでリリースを作成する。そうすれば、GitHubはプロジェクトごとにリリースページを持っており、そこに項目が作成される。

Release

  upload-release:
    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-musl
          - x86_64-pc-windows-msvc
          - x86_64-apple-darwin
    needs: [create-release]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v1
        with:
          name: create-release
      - id: upload-url
        run: |
          echo "::set-output name=url::$(cat create-release/release_upload_url.txt)"
      - uses: actions/download-artifact@v1
        with:
          name: build-${{ matrix.target }}
      - uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.upload-url.outputs.url }}
          asset_path: ./build-${{ matrix.target }}/rc-${{ matrix.target }}.zip
          asset_name: rc-${{ matrix.target }}.zip
          asset_content_type: application/zip

作成したリリースに、zipパッケージをアップロードする。actionsの仕様だと思うがソースコードのziptar.gzもアップロードされる。

余談

いろいろ試している時に、tagを付けずにリリースしていたらGitHubのリポジトリが壊れてえらいことになった。

詳細は忘れてしまったが、refs/tags/refs/heads/masterというブランチができていた。タグをつけると、refs/tags/v0.1.0のようなブランチができるのだが、タグを指定せずにタグをつけたら、masterの正式名であるrefs/heads/masterがタグとして認識されて、へんなブランチができたのだろう。そのせいでリモートにmasterをpushする時に複数のターゲットがマッチするのでpushできない、というような内容だったと思う。

git ls-remoteで問題のタグのフル名を特定し、そこにgit push origin :refs/tags/refs/heads/masterというpushをすることでそれを削除して復活した。