🐿️

[Rust] 自作CLIツールをGitHubとcrate.ioで配布するまでの記録

2025/02/04に公開

はじめに

opという自作の個人的なCLIツール(元々はGo製でした)をRustで配布する為に行った記録を書きます。

RustでCLIツールを作った時、GitHubのリリースページとcrate.ioで公開すれば、色々な人が手軽にインストールしてみることができるようになるので良いと思います。(このツールは本当に個人的なので、ちょっと私以外に使う方がおられるのか謎ですが…笑)

このツール自体は、中々にザツなノリの開発でやってますので、記事に書いている手法はそこそこアナログです。以下のような方法でGitHubとcrate.ioでリリース配布してますので、1つの方法として参考にしてみてください。

最近はgo-releaserがRust対応したりとしていますので、色々ともっと良い方法が取れると思いますが、日記だと思って見てもらえればと思います。

環境構築

ビルドとはあまり関係ありませんが、環境構築について記載します。
基本はdevContainer環境でローカルでちょっとテストしたいこともあったので、DockerfileではDebian系のイメージを入れ、musl-toolsmingw-w64を入れました。

これにより、LinuxかつDebian系の環境で、そのまんまx86_64-unknown-linux-musl, x86_64-pc-windows-gnuが作成できるようになります(ホストマシンがamd64の場合)。
これらのターゲットは、Alpine(と言うかスタティックビルドされているのでLinuxならほぼなんでも)とWindows 10~の環境でほぼっほぼ間違いなく動くことが期待できます。

Dockerfile
FROM rust:1.84.1-slim-bullseye

# Create the user
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

RUN groupadd --gid $USER_GID $USERNAME \
  && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME

# Install Dependencies
RUN apt-get update && apt-get install -y --no-install-recommends  \
  build-essential \
  mingw-w64 \
  musl-dev \
  musl-tools \
  git \
  zip \
  unzip 

USER $USERNAME

WORKDIR /workspaces

ENV SHELL=/bin/bash

CMD ["/bin/bash"]

最近はZigという便利なコンパイラも出てきてるのでcargo-zigのようなツールを使うのも有益だと思います。

また、rust-toolchain.tomlを書いておくと、使うバージョン・ターゲットをビルドする時に勝手に落としてくれるようになりますので、それも書いてます。

rust-toolchain.toml
[toolchain]
channel = "1.84.1"
components = ["rustfmt", "rustc", "cargo"]
targets = [
  "x86_64-unknown-linux-gnu",
  "x86_64-unknown-linux-musl",
  "aarch64-unknown-linux-musl",
  "x86_64-pc-windows-gnu",
  "aarch64-apple-darwin",
]
profile = "minimal"

GitHub Release Pageでのリリース

GitHub Actions経由で、「バージョンタグのタグプッシュがされたらリリース」ということにします。
複数ターゲットの対応はcrossというツールとマトリクスでのMac OSイメージを使いました。

release.yml
name: Release

on:
  push:
    tags:
      - "v*.*.*"

permissions:
  contents: write

jobs:
  release:
    name: Release
    strategy:
      matrix:
        os:
          - ubuntu-latest
          - macos-latest
    runs-on: ${{ matrix.os }}
    steps:
      - name: Set Environment Variables -- APP_VERSION
        run: |
          echo "APP_VERSION=${{ github.ref_name }}" >> "$GITHUB_ENV"

      - name: Checkout Repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Rust
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: stable

      - name: Setup cross
        run: cargo install cross
        if: startsWith(matrix.os, 'ubuntu')

      - name: Test
        run: |
          cargo test --release
          rm -rf target

      # see https://github.com/cross-rs/cross
      - name: Build By Cross
        if: startsWith(matrix.os, 'ubuntu')
        run: |
          targets=(
            "x86_64-unknown-linux-gnu"
            "x86_64-unknown-linux-musl"
            "aarch64-unknown-linux-musl"
            "x86_64-pc-windows-gnu"
          )
          for target in "${targets[@]}"; do
            cross build --release --target $target
          done

      # Build with MacOS images since Mac does not support cross by default
      - name: Build on macOS
        if: startsWith(matrix.os, 'macos')
        run: cargo build --release --target aarch64-apple-darwin

      - name: Archive Artifact To dist Folder
        run: bash scripts/publish.sh

      - name: Release
        uses: softprops/action-gh-release@v2
        if: startsWith(github.ref, 'refs/tags/')
        with:
          files: |
            dist/*.zip
            dist/*.tar.gz
          generate_release_notes: true
          draft: true

crossはDockerイメージを使って良い感じにクロスビルドができるツールです。普通にCargoからインストールでき、GitHub Actionsでそのまんま動くのでとても良いです。

ところが、Mac OSはデフォルトで対応してません(もっとも、Mac OSは公証セキュリティが厳しいのでゴリ押ししないと使えないかもしれないんですが…笑)。この対処方法は色々あると思うのですが、単純にMac OSイメージで別でビルドするようにしました。

jobs:
  release:
    name: Release
    strategy:
      matrix:
        os:
          - ubuntu-latest
          - macos-latest
    runs-on: ${{ matrix.os }}
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Rust
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: stable

      - name: Setup cross
        run: cargo install cross
        if: startsWith(matrix.os, 'ubuntu')

      # 省略

      # see https://github.com/cross-rs/cros
      - name: Build By Cross
        if: startsWith(matrix.os, 'ubuntu')
        run: |
          targets=(
            "x86_64-unknown-linux-gnu"
            "x86_64-unknown-linux-musl"
            "aarch64-unknown-linux-musl"
            "x86_64-pc-windows-gnu"
          )
          for target in "${targets[@]}"; do
            cross build --release --target $target
          done

      # Build with MacOS images since Mac does not support cross by default
      - name: Build on macOS
        if: startsWith(matrix.os, 'macos')
        run: cargo build --release --target aarch64-apple-darwin

targetフォルダからアーカイブする

ビルドしたバイナリはtargetフォルダにあり、それぞれのターゲット名を示したフォルダの中に入っています。
これとREADME.mdなどのファイルをアーカイブした形で配布したかったので、スクリプトによってゴリ押しで対応しています。

以下のBash用スクリプトは、distフォルダおよびその中にアーカイブしたいフォルダを作成し、targetフォルダからバイナリをコピー、および一部ドキュメントをコピーしてzipないし.tar.gzにアーカイブします。

publish.sh
#!/bin/bash

set -e

APP_NAME=op
test -n "$APP_VERSION" || APP_VERSION="v0.0.0"

rm -rf dist
mkdir -p dist

TARGET_DIR="target"
ROOT_DIR="$(pwd)"
DIST_DIR="$(pwd)/dist"

# Collect binaries for each target folder and move them to the dist folder.
# Also copy documents to that folder.
cd $TARGET_DIR
for dir in */; do
    dir=${dir%/}
    [[ "$dir" == "CHACHEDIR.TAG" ]] && continue
    
    binary_path="$dir/release/${APP_NAME}"
    [[ -f "$binary_path.exe" ]] && binary_path+=".exe"
    [[ -f "$binary_path" ]] || { echo "Binary not found in $dir"; continue; }
    
    target_dir="${DIST_DIR}/$dir"
    mkdir -p "$target_dir"
    
    cp "$binary_path" "$target_dir/"
    cp "${ROOT_DIR}/README.md" "${ROOT_DIR}/CREDITS" "$target_dir/"
done

# Archive each target folder
cd $DIST_DIR
for dir in */; do
    dir=${dir%/}
    archive_name="${APP_NAME}_${dir}_${APP_VERSION}"
    if [[ "$dir" == *windows* ]]; then
        zip -r "${archive_name}.zip" -j "$dir"
    else
        tar -czf "${archive_name}.tar.gz" -C "$dir" .
    fi
done

最後にactions-gh-releaseを使ってリリースをすれば、完了です。

crate.ioにアップロードする

crate.ioへのアップロードについて記載します。

Cargo.tomlの精査

Cargo.tomlをチェックします。特に重要になるのはpackage.nameです。既にopというパッケージは先に作られている方がいらっしゃったので、別の名前で対応しています。
その場合、バイナリ名は違う名前にしたいよと言う時は、[[bin]]キーを設定します。

[package]
name = "kawana77b-op"
version = "1.1.0"
edition = "2021"
description = "Open the file path or web address in the prescribed file explorer or browser"
authors = ["shimarisu_121"]
license = "MIT"
repository = "https://github.com/kawana77b/op"
readme = "README.md"
keywords = ["open", "explorer"]
categories = ["command-line-utilities"]
autotests = false
rust-version = "1.84.1"
include = ["/Cargo.toml", "/LICENSE", "/README.md", "/src/**"]

[[bin]]
name = "op"
path = "src/main.rs"

create.ioのAPIキー作成

crate.ioにGitHubアカウントで登録し、メール認証をします。
APIトークンを作ります。この時、publish-new, publish-updateを設定します。また、トークンの期限をお好みで設定します。
多くの方はご承知だと思いますが、トークンは2度と見れないのでパスワード管理ソフトなどにメモします。

GitHub Actionsからのアップロード

アップロードはGitHub Actionsから行うこととしました。
GitHubリポジトリの設定ページからSecretsを設定します。APIキーを値として設定します。
今のところリリースとは別個に手動で行うようにしています。そのyamlを以下に示します。

name: crate-io

on:
  workflow_dispatch:

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Rust
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with:
          toolchain: stable

      - run: cargo publish --token ${CRATES_TOKEN}
        env:
          CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }}

これでディスパッチすれば、crate.ioにアップロードされて、Rust環境がある人なら手軽にインストールできるようになります。

終わりに

Rust自体は正直あまり触れてませんので、もうちょっと色々知識が深まったら方法の改善など色々するかもしれません。以上、2025年2月現在の現状の記録でした。

皆様のご参考になれば幸いです。

Discussion