Rustで作ったCLIをgoreleaser + tagpr + brew tapで快適配布する
はじめに
バイナリにコンパイルできる言語[1]でコマンドラインツールを開発し、公開するときには、次のような項目を整備したくなる場面があるかと思います。
要素 | 作業 |
---|---|
ツール開発 | 複数アーキテクチャ向けのバイナリの作成 |
ツール開発 | 作成したバイナリの配布 |
リリース管理 | CHANGELOGの作成 |
リリース管理 | バージョンタグの付与 |
リリース管理 | GitHub Releaseの作成 |
これらの作業は、ブログタイトルにあるようなOSSを組み合わせることで効率的に実施できます。とくに、上記表でいう「ツール開発」や「GitHub Releaseの作成」には、Go製のgoreleaserというツールが非常に便利です。
そして2025年1月にリリースされたgoreleaser(v2.5)から、Rustにも正式対応しました。ちょうどRustで作成したCLIを配布する機会があり、実際に活用してみて大変便利だったため、本記事を書くことにしました。
前提
Rustはクレートの構成によってはクロスコンパイルが難しい場合があります。環境によっては上手くいかないケースもあると思いますので、本記事で紹介する構成を前提の一例として参考にしていただければ幸いです。
- リリース対象はコンパイル済みバイナリであり、crates.ioへのpublishは対象外
- クロスコンパイルには、GitHub ActionsのStandard GitHub-hosted runnerである
macos-14
を使用(なぜubuntu-latest
ではないかは後述) - クレートの構成は以下の通り
- 配布対象のアーキテクチャは以下の3種類
- x86_64-apple-darwin
- aarch64-apple-darwin
- x86_64-unknown-linux-gnu
- ※windows(x86_64-pc-windows-gnu)はツールの仕様上対応していませんが、後述するgoreleaserの設定に追加し、コンパイルできることは確認済です。
この記事で紹介している設定は、以下のリポジトリに実装例があります。詳細設定を確認したい場合はこちらをご参照ください。
構成
これらの作業はすべてGitHub Actions上で実行します。各作業項目と利用ツールの対応関係は以下のとおりです。
要素 | 作業 | ツール |
---|---|---|
ツール開発 | 複数アーキテクチャ向けのバイナリの作成 | goreleaser |
ツール開発 | 作成したバイナリの配布 | goreleaser, brew tap |
リリース管理 | CHANGELOGの作成 | tagpr |
リリース管理 | バージョンタグの付与 | tagpr |
リリース管理 | GitHub Releaseの作成 | goreleaser |
フローは次のようなイメージです。
設定内容
goreleaserの設定
goreleaserは2.6系を使用しています。設定ファイルは次のとおりです。
version: 2
before:
hooks:
- rustup default stable
- cargo install --locked cargo-zigbuild
builds:
- id: "cal2prompt"
builder: rust
binary: cal2prompt
targets:
- x86_64-apple-darwin
- aarch64-apple-darwin
- x86_64-unknown-linux-gnu
tool: "cargo"
command: zigbuild
flags:
- --release
skip: false
archives:
- formats: ["tar.gz"]
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
checksum:
name_template: "checksums.txt"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
brews:
- repository:
owner: shuntaka9576
name: homebrew-tap
token: "{{ .Env.GH_PAT }}"
commit_author:
name: goreleaserbot
email: bot@goreleaser.com
directory: Formula
license: MIT
name: cal2prompt
url_template: "https://github.com/shuntaka9576/cal2prompt/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
homepage: "https://shuntaka.dev/"
description: "✨ Fetches your schedule (e.g., from Google Calendar) and converts it into a single LLM prompt. It can also run as an MCP (Model Context Protocol) server."
test: |
system "#{bin}/cal2prompt --help"
install: |
bin.install "cal2prompt"
クロスコンパイルにはcargo-zigbuild
を利用しています。macos-14
(ARM)上でx86_64-apple-darwin
やx86_64-unknown-linux-gnu
をビルドしています。cargo以外ではcrossにも対応しています。詳しくはドキュメントやサンプルリポジトリが参考になります。
tagprの設定
[tagpr]
vPrefix = true
releaseBranch = main
versionFile = Cargo.toml
release = false
tagprはGitHub Releaseの作成機能を備えていますが、今回はgoreleaser側でリリースを行うためrelease=false
としています。
versionFile
としてCargo.toml
を指定しています。CLI本体では、コンパイル時にCargo.toml
のバージョンを埋め込む仕様にしています。以下のコードでは、ついでにGitのハッシュとCLI名も埋め込んでいます。
mainブランチにマージされた際に実行されるGitHub Actions定義
name: tagpr
on:
push:
branches:
- "main"
workflow_dispatch:
jobs:
tagpr:
runs-on: macos-latest
permissions:
contents: write
pull-requests: write
steps:
- name: checkout
uses: actions/checkout@v4
- name: tagpr
id: tagpr
uses: Songmu/tagpr@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/actions/release
if: "steps.tagpr.outputs.tag != ''"
with:
goreleaser_token: ${{ secrets.GORELEASER_TOKEN }}
goreleaser_args: "release --clean"
macos-latest
を選択した理由は、リンクオプションでmacOS SDKのCoreFoundationを要求してしまい、ubuntu-latest
だとコンパイルエラーとなったためです。
上記ワークフローでは、tagprが作成した「バージョンを付与するためのPR」がmainブランチにマージされるとsteps.tagpr.outputs.tag
にバージョン名が入り、そこからリリース用のcomposite actionが実行される仕組みになっています。以下がそのcomposite actionです。
name: release
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
inputs:
goreleaser_token:
description: goreleaser token
required: true
goreleaser_args:
description: "goreleaser args"
required: true
runs:
using: composite
steps:
- name: "Setup zig"
uses: mlugg/setup-zig@v1
# - uses: rui314/setup-mold@v1
- name: "Release"
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: "~> v2"
args: ${{ inputs.goreleaser_args }}
env:
GH_PAT: ${{ inputs.goreleaser_token }}
GITHUB_TOKEN: ${{ github.token }}
このcomposite actionでは、goreleaserを使って次の作業を実施します。
- クロスコンパイル
- GitHub Releaseの作成
- brew tapへの設定反映
GitHub側の設定
PR作成権限の付与
GitHub ActionsからPRを作成できるようにするために、リポジトリの設定から「GitHub ActionsにPull requestを作成する権限」を付与します。これでtagprがPRを作成できるようになります。
brew tap連携のためのPAT設定
別リポジトリ(homebrew-tap)へ書き込み権限を与える必要があるため、Personal Access Token(PAT)を発行し、Secretsとして登録します。
-
PATを発行する
-
CLI側のリポジトリでAction Secretsとして登録
実際の流れ
実際に流れを確認してみましょう。下記のスクリーンショットは作業後のものなので、すでにマージ済みですがご容赦ください。
-
バグ修正などを行い、
main
ブランチへマージすると…
-
tagprによって、次のようなバージョンタグ付与用PRが作成されます。
-
PRの内容は以下のように、
.tagpr
で指定したversionFile
であるCargo.toml
が更新されます。
※
Cargo.lock
については現状、別の依存関係に影響を与えないよう手動で更新していますが、将来的には改善したいです。良い方法があれば... -
バージョンが
v0.1.2
としてリリース可能なタイミングでこのPRをマージすると、自動的にGitHub Releaseが作成されます。
-
brew tap側もgoreleaser経由で更新されていることが確認できます。
-
実際にインストールすると、
v0.1.2
が取得できていることを確認できます。
$ brew install shuntaka9576/tap/cal2prompt
==> Auto-updating Homebrew...
...
cal2prompt 0.1.1 is already installed but outdated (so it will be upgraded).
==> Upgrading shuntaka9576/tap/cal2prompt
0.1.1 -> 0.1.2
...
$ cal2prompt --version
cal2prompt version 0.1.2 (rev:df26235)
その他で詰まったところ
reqwestのコンパイル周り
macos-latest
でx86_64-unknown-linux-gnu
向けにコンパイルしようとすると、openssl
が見つからないエラーが出ました。これはreqwest
を使用している影響が大きそうでした。
reqwest = { version = "0.12.12", features = ["json"] }
warning: openssl-sys@0.9.104: Could not find directory of OpenSSL installation, ...
error: failed to run custom build command for `openssl-sys v0.9.104`
...
解決のため、システムにインストールされたOpenSSLを使うのではなく、内部に同梱(vendored)されたOpenSSLをビルドして利用するよう変更しました。
openssl = { version = "0.10", features = ["vendored"] }
しかし、これによりコンパイル時間が大幅に伸びてしまったため、最終的にはTLSライブラリとしてrustls-tls
を使用し、openssl
クレートを削除してビルド時間を短縮しました。
reqwest = { version = "0.12", default-features = false, features = [
"json",
"rustls-tls",
] }
この変更により、ビルド時間を約6分短縮できました。詳細は以下のPRをご覧ください。
goreleaserでcargo workspaceのバイナリビルドはまだ不安定
cargo workspace全体を対象に今回のようなバイナリビルドを行う構成は、現時点ではまだ難しい部分があるようです(publishのフロー自体は対応しているようです)。興味がある方は公式ドキュメントやGitHub Issuesなども併せて参照してみてください。
さいごに
goreleaserをすでにGoで使っていた方はヌルッと使えると思います!
現状手元で確認した限りmacos-latestとcargo-zigbuild構成だとmoldが使われていないようで、この分のオーバヘッドがありそうです。
クロスコンパイルにこだわるのもROI的に微妙ですね。長期を見据えたら大人しく環境ごとにコンパイルするのが色んな地雷踏まずに済むので良い気がします。今回は手早くリリースしたかったので無理やりクロスコンパイルが通る構成でゴリ押ししました。何かの参考になれば幸いです!
他にももっと良いやり方やTipsがあれば共有して頂けるとありがたいです!
例を挙げるとC, C++, Go, Rustなどが該当します。DenoやNode.jsのシングルバイナリ作成機能も含みます。 ↩︎