はじめに

v0.0.1のようなタグを付けてGitHubにpushすると、以下のように、各OS・アーキテクチャ向けのビルド済みバイナリが、
自動的にReleasesに配置されるRustプロジェクトのサンプルを作成しました。

image.png

本記事で使用しているactions-rs/toolchainおよびactions-rs/cargoが、先月organizationごとarchiveされてしまったようです。
直ちに使用不可となる可能性は低いかもしれませんが、今後はcrossを手動でインストールする等の代替手段を検討した方が良いかもしれません。

ソースコード

リポジトリは以下で公開しています。

 

今回メインとなるのは以下のファイルです。

 

このファイルを.github/workflows/ディレクトリに配置しておくだけで、v0.0.1のようなtagのpushをトリガーとして、
自動的にワークフローが実行されます。

name: Release

# Releasesへのファイル追加のために書き込み権限が必要
permissions:
  contents: write

on:
  push:
    tags:
      - v*

jobs:
  build:
    runs-on: ${{ matrix.job.os }}
    strategy:
      fail-fast: false
      matrix:
        job:
          - { os: ubuntu-latest  , target: x86_64-unknown-linux-gnu       , use-cross: false , extension: ""   }
          - { os: ubuntu-latest  , target: x86_64-unknown-linux-musl      , use-cross: true  , extension: ""   }
          - { os: ubuntu-latest  , target: armv7-unknown-linux-gnueabihf  , use-cross: true  , extension: ""   }
          - { os: ubuntu-latest  , target: armv7-unknown-linux-musleabihf , use-cross: true  , extension: ""   }
          - { os: ubuntu-latest  , target: aarch64-unknown-linux-gnu      , use-cross: true  , extension: ""   }
          - { os: ubuntu-latest  , target: aarch64-unknown-linux-musl     , use-cross: true  , extension: ""   }
          - { os: macos-latest   , target: x86_64-apple-darwin            , use-cross: false , extension: ""   }
          - { os: macos-latest   , target: aarch64-apple-darwin           , use-cross: false , extension: ""   }
          - { os: windows-latest , target: x86_64-pc-windows-msvc         , use-cross: false , extension: .exe }
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      # Rustのpackage名を取得して環境変数に入れておく。(後のステップで使用)
      - name: Extract crate information
        shell: bash
        run: |
          echo "PROJECT_NAME=$(sed -n 's/^name = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $GITHUB_ENV

      # rustcやcargoをインストール
      - name: Install Rust toolchain
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          target: ${{ matrix.job.target }}
          override: true
          profile: minimal

      # targetに応じてcargoもしくはcrossを使用してビルド
      - name: Build
        uses: actions-rs/cargo@v1
        with:
          use-cross: ${{ matrix.job.use-cross }}
          command: build
          args: --release --target ${{ matrix.job.target }}

      # ビルド済みバイナリをリネーム
      - name: Rename artifacts
        shell: bash
        run: |
          mv target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}{,-${{ github.ref_name }}-${{ matrix.job.target }}${{ matrix.job.extension }}}

      # ビルド済みバイナリをReleasesに配置
      - name: Release
        uses: softprops/action-gh-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          files: |
            target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}-${{ github.ref_name }}-${{ matrix.job.target }}${{ matrix.job.extension }}

解説

書き込み権限の付与

permissions:
  contents: write

Releasesへのファイル追加のため、書き込み権限が必要なので付与しています。
リポジトリ設定のActions > General > Workflow permissionsでRead and write permissionsを選択することでも付与可能ですが、
その場合は全ワークフローに書き込み権限が付与されてしまいます。
今回のようにワークフロー単位で最小限の権限のみ付与する方が安全かと思います。

ビルド対象の指定

matrix:
  job:
    - { os: ubuntu-latest  , target: x86_64-unknown-linux-gnu       , use-cross: false , extension: ""   }
    - { os: ubuntu-latest  , target: x86_64-unknown-linux-musl      , use-cross: true  , extension: ""   }
    - { os: ubuntu-latest  , target: armv7-unknown-linux-gnueabihf  , use-cross: true  , extension: ""   }
    - { os: ubuntu-latest  , target: armv7-unknown-linux-musleabihf , use-cross: true  , extension: ""   }
    - { os: ubuntu-latest  , target: aarch64-unknown-linux-gnu      , use-cross: true  , extension: ""   }
    - { os: ubuntu-latest  , target: aarch64-unknown-linux-musl     , use-cross: true  , extension: ""   }
    - { os: macos-latest   , target: x86_64-apple-darwin            , use-cross: false , extension: ""   }
    - { os: macos-latest   , target: aarch64-apple-darwin           , use-cross: false , extension: ""   }
    - { os: windows-latest , target: x86_64-pc-windows-msvc         , use-cross: false , extension: .exe }

ビルドを実行するGitHub-hosted runnerのOS、ビルド対象のtarget triple、crossの使用有無などを指定しています。
crossを使用することで、crossがサポートしているARMやMIPSなどのバイナリもビルド可能となります。
今回は、通常のx86_64用、および個人的によく使うARM用のtargetを指定しています。

補足:muslについて

Linux向けのtargetでは、同じアーキテクチャでもgnu系とmusl系という2つの選択肢があります。
gnu系はglibcと動的リンクするため、ビルド時と異なる環境では実行できない場合があります。
一方musl系はmusl libcと静的リンクし1、Golangのような依存無しのシングルバイナリが得られるため、どこでも実行出来て便利です。

ただし、musl系はgnu系に比べて遅い2 3という話もあるようです。
また、gnu系とmusl系で一部動作が異なる場合があるため、注意が必要です。

例えば、現在4musl系は、ファイル作成日時を取得するstd::fs::Metadataのcreated()メソッドに対応していません。5
以下のコードをそれぞれのtargetでビルド・実行すると、musl系の場合は取得に失敗します。

fn main() -> std::io::Result<()> {
    let file = std::fs::File::open("Cargo.toml")?;
    let metadata = file.metadata()?;
    let _ = dbg!(metadata.created());
    Ok(())
}
$ cargo run --target x86_64-unknown-linux-gnu
[src/main.rs:6] metadata.created() = Ok(
    SystemTime {
        tv_sec: 1698726456,
        tv_nsec: 826404242,
    },
)

$ cargo run --target x86_64-unknown-linux-musl
[src/main.rs:6] metadata.created() = Err(
    Error {
        kind: Unsupported,
        message: "creation time is not available on this platform currently",
    },
)

プロジェクト名の取得

- name: Extract crate information
  shell: bash
  run: |
    echo "PROJECT_NAME=$(sed -n 's/^name = "\(.*\)"/\1/p' Cargo.toml | head -n1)" >> $GITHUB_ENV

ビルド後にバイナリをリネームする際に使用するため、Cargo.tomlからプロジェクト名を取得して環境変数に設定しています。

Rust環境のインストール

- name: Install Rust toolchain
  uses: actions-rs/toolchain@v1
  with:
    toolchain: stable
    target: ${{ matrix.job.target }}
    override: true
    profile: minimal

actions-rs/toolchainを使用して、targetに応じたrustcやcargo等をインストールしています。

ビルド

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

actions-rs/cargoを使用してビルドを行ないます。
use-crossがtrueの場合はcross、falseの場合はcargoが使用されます。

ビルド済みバイナリのリネーム

- name: Rename artifacts
  shell: bash
  run: |
    mv target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}{,-${{ github.ref_name }}-${{ matrix.job.target }}${{ matrix.job.extension }}}

ビルド済みバイナリのファイル名はtargetによらず同じになってしまうため、Releasesへ配置する前に、
hoge-v0.0.1-x86_64-unknown-linux-gnuのような名前にリネームしています。

バイナリをReleasesに配置

- name: Release
  uses: softprops/action-gh-release@v1
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  with:
    files: |
      target/${{ matrix.job.target }}/release/${{ env.PROJECT_NAME }}-${{ github.ref_name }}-${{ matrix.job.target }}${{ matrix.job.extension }}

softprops/action-gh-releaseを使用してバイナリをReleasesに配置しています。

小ネタ

今回参考にしたpastelのworkflowでは、targetに応じたstripコマンドを使い分けて、ビルド済みバイナリのサイズを削減していました。
昨年リリースされたCargo 1.59でstripオプションが安定化されたため、現在はCargo.tomlに以下の内容を記述しておくだけで同様の効果が得られます。

[profile.release]
strip = "symbols"

おわりに

依存クレート次第では、crossを使用しても一部targetでのビルドが上手くいかない場合もあります。
それでも上手くビルド出来た場合は、Raspberry PiやM5Stack UnitV2、スマホなどの上でそのまま動くARM用バイナリを簡単に得られるので、一度試してみる価値はあると思います。
(非力なRaspberry Pi上でビルドしなくて済むのは嬉しい)

参考リンク

    • GitHub Actions設定の参考にしたリポジトリ

https://github.com/sharkdp/pastel
https://github.com/Nukesor/pueue

使用したAction

https://github.com/actions/checkout
https://github.com/actions-rs/toolchain
https://github.com/actions-rs/cargo
https://github.com/softprops/action-gh-release

ただし、mipsel-unknown-linux-muslとmips-unknown-linux-muslはデフォルトでは動的リンクとなります。
https://github.com/rust-lang/rust/issues/80693 ↩

https://www.reddit.com/r/rust/comments/gdycv8/why_does_musl_make_my_code_so_slow/ ↩

https://github.com/rust-lang/rust/issues/70108 ↩

本記事執筆時最新の安定版であるRust 1.73.0時点 ↩

作成日時を取得出来るstatxシステムコールにmusl libcが対応していないようです。
https://github.com/rust-lang/rust/blob/1.73.0/library/std/src/sys/unix/fs.rs#L95-L248 ↩

bannerAds