この記事はRust Advent Calendar 2019の4日目の記事です。
こんにちは、ピクシブの福岡オフィスでエンジニアをしている@tasshiです。
最近はNintendo Switchのリングフィットアドベンチャーがマイブームです。1
概要
Rustのライブラリクレートを作って、crates.ioに公開するまでの手順を紹介します。
基本的にはthe bookのPublishing a Crate to Crates.ioに従って進めていきます。2
背景
今年の11月にcrates.ioにfittingという曲線近似ライブラリを公開しました。
実際にクレートを公開してみて得られた知見があったので、共有しようと思い記事を書いてみました。
crates.ioについて
crates.ioはRustにおけるパッケージのデフォルトリポジトリです。
the bookのチュートリアルで使用するrandクレートなどもここに公開されています。
Rustのパッケージマネージャ(兼ビルドツール)のCargoは、デフォルトでcrates.ioを参照するようになっています。
そのため、Cargo.tomlに依存関係を書くときや、cargo install
でコマンドラインツールをインストールするときなど、様々な場面で必ずと言っていいほどお世話になるサイトがこのcrates.ioです。
手順
ざっくりと次のような順番で進めていきます。
- クレートを実装する
- ドキュメンテーションコメントを書く
- メタデータを追加する
- crates.ioのアカウントをセットアップする
- クレートを公開する
- [おまけ](#おまけ1: CIを設定する)
クレートを実装する
まずはライブラリのコードを実装します。
とは言ったものの、実際の開発だとクレートを作ろうと思った時点で既に素体となるコードはあるので、ここでは公開するための修正・リファクタリングを行います。
バイナリクレートとライブラリクレート
Rustのクレートにはバイナリクレートとライブラリクレートとがあります。
クレートのタイプ | クレートのルート | 説明 |
---|---|---|
バイナリクレート | src/main.rs |
main関数が含まれる(プログラムのエントリーポイント) |
ライブラリクレート | src/lib.rs |
機能群が含まれる |
バイナリクレートは実行可能なバイナリを提供するためのクレートで、cargo install
でインストールして実行したり、cargo run
でローカルでコンパイルして実行したりすることができます。
ライブラリクレートは他のプログラムで取り込むことに適した機能群を提供するためのクレートで、Cargo.toml
のdependencies
フィールドに追加して使用することができます。
公開するクレートはこれらのどちらか、もしくは両方を含むことができます。
エラーハンドリングを行う
試験的に使用しているunwrap()
やexpect()
などのpanicするメソッドを消し、Result
やOption
を返すように置き換えます。
特にライブラリクレートではpanicするのはなるべく避けます。
panicするべき状態についてはthe bookのGuidelines for Error Handlingを参照します。
そのほかに注意すること
そのほかに注意する点として、以下のようなものがあります。
- 単体テスト、結合テストを書く
-
Cargo.toml
の依存関係のうち、クレート本体に不要な物を[dev-dependencies]
に移動する
ドキュメンテーションコメントを書く
コードの実装が終わったらドキュメンテーションコメントを書きます。
ドキュメンテーションコメントはdocs.rsに公開されるクレートのドキュメントのために書きます。
crates.ioにクレートを公開すると、ドキュメンテーションコメントを元にdocs.rsにドキュメントが自動生成されます。
ドキュメンテーションコメントの記法
詳しくはWhat is rustdoc? -を参照してください。
- 以下に対するドキュメントは3連スラッシュ(
///
)で始める - 親に対するドキュメントは2連スラッシュ+!(
//!
)で始める - マークダウンをサポート
- いくつかのマークダウンタイトルは特別な意味をもつ
例えば# Example
タイトルで始まるセクションは関数などの使い方をコードを載せて例示することができます。
ドキュメンテーションコメントの例
/// Returns a value of gaussian function.
///
/// # Examples
/// Returns a value of gaussian function.
///
/// ```
/// use fitting::gaussian::val;
///
/// let (mu, sigma, a): (f64, f64, f64) = (5., 3., 1.);
/// let x = 5.;
/// let y = val(x, mu, sigma, a);
/// assert_eq!(y, a);
///
/// ```
pub fn val(x: f64, mu: f64, sigma: f64, a: f64) -> f64 {
a * (-(x - mu).powi(2) / (2. * sigma.powi(2))).exp()
}
生成されるドキュメントの例
メタデータを追加する
次にCargo.toml
にクレートのメタ情報を記入していきます。
元々記入済のフィールドに加えて、description
とlicense
との2つは最低限必要になります。
主要なフィールド
フィールド名 | 説明 |
---|---|
name | crates.ioにおけるクレートの名前 |
description | 検索結果に表示されるクレートの名前 |
keywords | 自由入力で5つまで設定可能 |
categories | category_slugsの中から、5つまで設定可能 |
license | ライセンス |
その他にもたくさんフィールドはあるので公式ページを参照して適宜追加してください。
公式ページ:The Manifest Format - The Cargo Book
クレートの名前について
Cargo.toml
のname
フィールドはcrates.ioにおけるそのクレートの名前です。
この名前はcrates.ioにおいては、ユニークである必要があり、かつ確保は早い物勝ちになっています。
そのため、事前にcrates.ioで被っていないか確認してからクレートの名前を決定するのが、公開直前の名前変更などのトラブルを避ける意味でも良さそうです。
備考:crates.ioにはSquattingのポリシーが存在しないので、放置された名前の開放はないそうです。3
ライセンスについて
クレートのライセンスですが、Rust APIガイドラインによればMIT/Apache2.0のデュアルライセンスが最も望ましく、そうでない場合もMITライセンスやBSDライセンスなどの寛容な(permissive)ライセンスを使用することを推奨しています。
Necessities - Rust API Guidelines
サンプル
以下にサンプルとして、fittingクレートのメタ情報を載せておきます。
[package]
name = "fitting"
version = "0.1.0"
authors = ["Rustacean Smith <rustacean@example.com>"]
edition = "2018"
description = "Pure Rust curve fitting library"
documentation = "https://docs.rs/fitting/"
homepage = "https://crates.io/crates/fitting"
repository = "https://github.com/tasshi-me/fitting-rs"
readme = "README.md"
keywords = ["fitting","statistics","probability","distribution","math"]
categories = ["science"]
license = "MIT"
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
ndarray = { version = "0.13.0", features = ["approx"] }
approx = "0.3.2"
failure = "0.1.6"
crates.ioのアカウントをセットアップする
いよいよ公開まであと少しです。
crates.ioのアカウントを作成して、ローカルPCのターミナルでログインします。
手順
- https://crates.io/ でアカウント作成します(GitHubログイン)
- https://crates.io/me にアクセスしてAPI Access Tokenを生成します
- ローカルでターミナルを開いて以下のコマンドを実行します
cargo login <Your Access Token>
クレートを公開する
以上で準備は完了です。
公開したいプロジェクトのルートで下記のコマンドを実行しましょう。
❯ cargo publish
ドキュメントの生成、テストに問題がなければパッケージがアップロードされて公開完了です。
https://crates.io/crates/{crate_name} にアクセスしてクレートが公開されていることを確認しましょう。
docs.rsへの反映には5~10分程度時間がかかるのでしばらくしてから確認してみてください。
おまけ1: CIを設定する
クレートは公開できましたが、やっぱりCIを使ってテストを自動化したいですよね。
というわけでCIの設定をしていきます。
CIサービスの選定について
目視で確認した限りだとTravis CIかAppveyorを利用しているクレートが多いです。
crates.ioがビルドステータスの表示に対応しているのは以下の6サービス(2019年11月)
| Appveyor | CircleCI | Cirrus CI | GitLab | Azure DevOps | TravisCI |
今回はTravis CIを使っていきます。
Travis CIの設定
Travis CIでは設定ファイルでlanguage: rust
を設定するとbuildやtestは勝手に実行してくれます。
そのため設定ファイルでは
- Rustの実行環境の指定
- キャッシュの設定
- コードフォーマッタやLintの実行
を設定していきます
Rustの実行環境の指定
stable, beta, nightlyそれぞれでビルドします。
nightlyではビルドの失敗を許容します
rust:
- stable
- beta
- nightly
jobs:
allow_failures:
- rust: nightly
キャッシュの設定
キャッシュを使ってビルド時間の高速化を図ります。
キャッシュの設定についてはTravis CI公式の方法よりも、Qiitaで見た指定方法のほうが良さそうだったのでそちらを使用しています。
参考記事:Travis CIでのRust cacheとうまく付き合う - Qiita
cache:
directories:
- ${HOME}/.cargo
- ${HOME}/.rustup
before_cache:
- rm -rf /home/travis/.cargo/registry
コードフォーマッタやLintの実行
before_script
タグで、ビルド直前に処理を追加します。
今回はコードフォーマッタ(cargo fmt
)とLint(cargo clippy
)を実行させています。
before_script: |
cargo fmt --version
cargo fmt -- --check
cargo clippy --version
cargo clippy
cargo clean
最終的なTravis CIの設定サンプル
language: rust
rust:
- stable
- beta
- nightly
cache:
directories:
- ${HOME}/.cargo
- ${HOME}/.rustup
before_cache:
- rm -rf /home/travis/.cargo/registry
jobs:
allow_failures:
- rust: nightly
fast_finish: true
before_script: |
cargo fmt --version
cargo fmt -- --check
cargo clippy --version
cargo clippy
cargo clean
CIの確認
.travis.yml
をコミットしたら、Travis CIにいってビルドが正常に動いているのかを確認します。
おまけ2: コードカバレッジを計測する
コードカバレッジも計測します。
カバレッジサービスはCodecovかCoverallsを使います。
計測ツールについては現在主流なものは以下の3つです。それぞれ使える環境に違いがあるので自分に適した物を選びます。
name | os | comment |
---|---|---|
tarpaulin | Linux | nightly推奨 |
cargo-cov | FreeBSD, Linux, macOS, Windows | nightly推奨4 |
cargo-kcov | Linux, macOS | PersonalityというDockerコンテナ内で呼び出せないシステムコールを使っている56 |
僕はnightlyをあまり使いたくなかったのでcargo-kcov + Travis CI + Codecovでカバレッジを計測しました。
cargo-kcov + Travis CI + Codecovの設定サンプル
cargo-kcovではバックエンドとしてkcovを使う必要があります。
Codecovの提供するサンプルコードでは、毎回kcovのソースをmakeしていて、あまりビルドが速くなかったため、キャッシュを使うように変更しています。
cache:
directories:
- ${HOME}/kcov/ # kcovのインストールディレクトリ
env:
- PATH=$PATH:${HOME}/kcov/bin # 環境変数にkcovのディレクトリを加える
before_install: | # もし${HOME}/kcov/binがなかったら、kcovをmake installする
if [ ! -d "${HOME}/kcov/bin" ];
then
echo "Install kcov" &&
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz &&
tar xzf master.tar.gz &&
cd kcov-master &&
mkdir build &&
cd build &&
cmake -DCMAKE_INSTALL_PREFIX=${HOME}/kcov .. &&
make &&
make install &&
cd ../.. &&
rm -rf kcov-master;
fi
after_success: | # カバレッジ計測の実行 & Codecovへのアップロード
cargo kcov --no-clean-rebuild --verbose --all --no-fail-fast \
--output ./target/cov -- --verify --include-path=. --exclude-path=./target \
--exclude-region=kcov-ignore-begin:kcov-ignore-end --exclude-line=kcov-ignore-line &&
bash <(curl -s https://codecov.io/bash) &&
echo "Uploaded code coverage"
cargo-kcovのインストールについて
cargo install
はビルドシステムであってパッケージのバージョン管理ツールではないため、同名のクレートのinstallはエラーになってしまいます。
そのためこの設定ファイルでは、ローカルのcargo-kcov
とリモートのcargo-kcov
のバージョンを比較して、バージョンが違う場合のみ再インストールを実行するような処理を実装しました。
補足:cargo-updateというクレートがあるらしく、これをインストールすればいいらしいです。
before_install: |
if ! type cargo-kcov > /dev/null;
then
echo "Install cargo-kcov" &&
cargo install cargo-kcov -f;
else
echo "Check cargo-kcov is latest"
REMOTE_KCOV_VERSION=`cargo search cargo-kcov --limit 1 | sed -e 's/^cargo-kcov = "\(.*\)".*/\1/'`
LOCAL_KCOV_VERSION=`cargo-kcov --version | sed -e 's/^cargo-kcov \(.*\).*/\1/'`
if [ ! $REMOTE_KCOV_VERSION = $LOCAL_KCOV_VERSION ];
then
echo "Upgrade cargo-kcov" &&
cargo install cargo-kcov -f;
fi
fi
カバレッジの確認
ここまででカバレッジ計測の設定も完了です。
あとはCodecovにいってカバレッジが取れているかを確認します。
まとめ
クレートを公開してみた感想ですが、公式のドキュメントがしっかりしていたので公開までは意外と簡単にできました。
一方で、CIやカバレッジ計測に関しては自分で調査しないといけないようなハマりどころも多く、納得して動かせるようになるまでにかなり苦戦しました。
また、crates.ioについて、ユーザで名前空間が別れていないので強い名前をつけてしまうとかなり目立ってしまうという印象を受けました。名前空間を占拠しているという自覚を持ってメンテナンスに励みたいと思います。
公開自体は簡単なので、「欲しいライブラリがcrates.ioで見つからない、、」という方は、ぜひ自分で実装して公開してみてはいかがでしょうか?
ここまで読んでいただきありがとうございました。