LoginSignup
10
6

GitHub ActionsでRustのプロジェクトをビルドしてReleasesにバイナリを置く

Last updated at Posted at 2023-11-10

はじめに

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をトリガーとして、
自動的にワークフローが実行されます。

release.yml
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 permissionsRead 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::Metadatacreated()メソッドに対応していません。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に応じたrustccargo等をインストールしています。

ビルド

- 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に以下の内容を記述しておくだけで同様の効果が得られます。

Cargo.toml
[profile.release]
strip = "symbols"

おわりに

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

参考リンク

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

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

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

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

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

10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6