crates.ioにクレートを公開するまで

この記事は[Rust Advent Calendar 2019](https://qiita.com/advent-calendar/2019/rust)の4日目の記事です。

 + 3日目はこちら→[actix-webでlive streamingに挑戦 - Qiita](https://qiita.com/Dsuke-K/items/b286b258072ef6fc6745)
 + 5日目はこちら→[ffmpegをRustから子プロセス実行して、それっぽくmp4エンコーダを書く - Qiita](https://qiita.com/tasshi/items/de36d9add14f24317f47)

こんにちは、ピクシブの福岡オフィスでエンジニアをしている@tasshiです。
最近はNintendo Switchのリングフィットアドベンチャーがマイブームです。[^1]
[^1]: [リングフィット アドベンチャー | Nintendo Switch | 任天堂](https://www.nintendo.co.jp/ring/)

# 概要

Rustのライブラリクレートを作って、[crates.io](https://crates.io)に公開するまでの手順を紹介します。
基本的にはthe bookの[Publishing a Crate to Crates.io](https://doc.rust-lang.org/book/ch14-02-publishing-to-crates-io.html)に従って進めていきます。[^2]
[^2]: [Publishing a Crate to Crates.io - The Rust Programming Language](https://doc.rust-lang.org/book/ch14-02-publishing-to-crates-io.html)

# 背景

今年の11月にcrates.ioに[fitting](https://crates.io/crates/fitting)という曲線近似ライブラリを公開しました。
実際にクレートを公開してみて得られた知見があったので、共有しようと思い記事を書いてみました。

<img width="983" alt="fitting-rs.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214775/ddce53fa-f273-b372-6ceb-926e1c809ade.png">

# crates.ioについて

[crates.io](https://crates.io)はRustにおけるパッケージのデフォルトリポジトリです。
the bookのチュートリアルで使用する[rand](https://crates.io/crates/rand)クレートなどもここに公開されています。

Rustのパッケージマネージャ（兼ビルドツール）のCargoは、デフォルトでcrates.ioを参照するようになっています。
そのため、Cargo.tomlに依存関係を書くときや、`cargo install`でコマンドラインツールをインストールするときなど、様々な場面で必ずと言っていいほどお世話になるサイトがこのcrates.ioです。

<img width="1440" alt="crates.io.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214775/b2105dd6-35e5-6c53-81f5-5f2fae6078e5.png">

# 手順
ざっくりと次のような順番で進めていきます。

 1. [クレートを実装する](#クレートを実装する)
 2. [ドキュメンテーションコメントを書く](#ドキュメンテーションコメントを書く)
 3. [メタデータを追加する](#メタデータを追加する)
 4. [crates.ioのアカウントをセットアップする](#crates.ioのアカウントをセットアップする)
 5. [クレートを公開する](#クレートを公開する)
 6. [おまけ](#おまけ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](https://doc.rust-lang.org/book/ch09-03-to-panic-or-not-to-panic.html#guidelines-for-error-handling)を参照します。

#### そのほかに注意すること
そのほかに注意する点として、以下のようなものがあります。

 + 単体テスト、結合テストを書く
 + `Cargo.toml`の依存関係のうち、クレート本体に不要な物を`[dev-dependencies]`に移動する

## ドキュメンテーションコメントを書く
コードの実装が終わったらドキュメンテーションコメントを書きます。

ドキュメンテーションコメントは[docs.rs](https://docs.rs/)に公開されるクレートのドキュメントのために書きます。
crates.ioにクレートを公開すると、ドキュメンテーションコメントを元に[docs.rs](https://docs.rs/)にドキュメントが自動生成されます。

### ドキュメンテーションコメントの記法

詳しくは[What is rustdoc? -](https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html)を参照してください。

 + 以下に対するドキュメントは3連スラッシュ（`///`）で始める
 + 親に対するドキュメントは2連スラッシュ+!（`//!`）で始める
 + マークダウンをサポート
 + いくつかのマークダウンタイトルは特別な意味をもつ

例えば`# Example`タイトルで始まるセクションは関数などの使い方をコードを載せて例示することができます。

#### ドキュメンテーションコメントの例

```rs:sample.rs
/// 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()
}
```

#### 生成されるドキュメントの例
<img width="987" alt="documentation_comment_sample.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214775/0e3fb04e-3744-8239-86e5-4ab9ce1960ce.png">

## メタデータを追加する

次に`Cargo.toml`にクレートのメタ情報を記入していきます。
元々記入済のフィールドに加えて、`description`と`license`との2つは最低限必要になります。

### 主要なフィールド
|フィールド名|説明|
|---|---|
|name|crates.ioにおけるクレートの名前|
|description|検索結果に表示されるクレートの名前|
| keywords |自由入力で5つまで設定可能|
| categories |[category_slugs](https://crates.io/category_slugs)の中から、5つまで設定可能|
| license |ライセンス|

その他にもたくさんフィールドはあるので[公式ページ](https://doc.rust-lang.org/cargo/reference/manifest.html)を参照して適宜追加してください。

公式ページ：[The Manifest Format - The Cargo Book](https://doc.rust-lang.org/cargo/reference/manifest.html)

### クレートの名前について
`Cargo.toml`の`name`フィールドはcrates.ioにおけるそのクレートの名前です。
この名前はcrates.ioにおいては、ユニークである必要があり、かつ確保は早い物勝ちになっています。
そのため、事前にcrates.ioで被っていないか確認してからクレートの名前を決定するのが、公開直前の名前変更などのトラブルを避ける意味でも良さそうです。

備考：crates.ioにはSquattingのポリシーが存在しないので、放置された名前の開放はないそうです。[^3]

[^3]: [Crates.io Package Policies - crates.io](https://crates.io/policies)

### ライセンスについて
クレートのライセンスですが、Rust APIガイドラインによればMIT/Apache2.0のデュアルライセンスが最も望ましく、そうでない場合もMITライセンスやBSDライセンスなどの寛容な(permissive)ライセンスを使用することを推奨しています。

[Necessities - Rust API Guidelines](https://rust-lang.github.io/api-guidelines/necessities.html)

### サンプル

以下にサンプルとして、fittingクレートのメタ情報を載せておきます。

```toml:Cargo.toml(一部抜粋)
[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ログイン）
<img width="981" alt="crates.io_startup.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214775/46497a77-b7b3-9a58-4fa6-122291570a8e.png">

 + https://crates.io/me にアクセスしてAPI Access Tokenを生成します

<img width="1041" alt="crates.io_new_api_key.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214775/613fc25f-c3b8-4a48-7949-27ae8889c1cc.png">


 + ローカルでターミナルを開いて以下のコマンドを実行します

```sh
cargo login <Your Access Token>
```

## クレートを公開する
以上で準備は完了です。
公開したいプロジェクトのルートで下記のコマンドを実行しましょう。

```sh
❯ cargo publish
```

ドキュメントの生成、テストに問題がなければパッケージがアップロードされて公開完了です。
[https://crates.io/crates/{crate_name}](https://crates.io/crates/) にアクセスしてクレートが公開されていることを確認しましょう。
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ではビルドの失敗を許容します

```yaml
rust:
  - stable
  - beta
  - nightly

jobs:
  allow_failures:
    - rust: nightly
```

#### キャッシュの設定
キャッシュを使ってビルド時間の高速化を図ります。
キャッシュの設定については[Travis CI公式の方法](https://docs.travis-ci.com/user/caching/#rust-cargo-cache)よりも、Qiitaで見た指定方法のほうが良さそうだったのでそちらを使用しています。

参考記事：[Travis CIでのRust cacheとうまく付き合う - Qiita](https://qiita.com/justmonika/items/6cf62bbdb9b4913d9aa6)

```yaml
cache:
  directories:
    - ${HOME}/.cargo
    - ${HOME}/.rustup
before_cache:
  - rm -rf /home/travis/.cargo/registry
```

#### コードフォーマッタやLintの実行
`before_script`タグで、ビルド直前に処理を追加します。
今回はコードフォーマッタ(`cargo fmt`)とLint(`cargo clippy`)を実行させています。

```yaml
before_script: |
  cargo fmt --version
  cargo fmt -- --check
  cargo clippy --version
  cargo clippy
  cargo clean
```

#### 最終的なTravis CIの設定サンプル
```yml:.travis.yml(一部抜粋)
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にいってビルドが正常に動いているのかを確認します。

<img width="1103" alt="travis.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214775/03c1510c-31bb-3eac-3b08-bfea87cc0115.png">


## おまけ2: コードカバレッジを計測する

コードカバレッジも計測します。

カバレッジサービスは[Codecov](https://codecov.io/gh)か[Coveralls](https://coveralls.io/)を使います。

計測ツールについては現在主流なものは以下の3つです。それぞれ使える環境に違いがあるので自分に適した物を選びます。

|name|os|comment|
|---|---|---|
|[tarpaulin](https://github.com/xd009642/tarpaulin)| Linux |nightly推奨|
|[cargo-cov](https://github.com/kennytm/cov)|FreeBSD, Linux, macOS, Windows|nightly推奨[^4]|
|[cargo-kcov](https://github.com/kennytm/cargo-kcov)|Linux, macOS|PersonalityというDockerコンテナ内で呼び出せないシステムコールを使っている[^5][^6]|

[^4]: [kennytm/cov: LLVM-GCOV Source coverage for Rust](https://github.com/kennytm/cov#usage-for-testing-on-stable-rust-119)
[^5]: [Executing kcov in a docker image fails · Issue #151 · SimonKagstrom/kcov](https://github.com/SimonKagstrom/kcov/issues/151#issuecomment-248845453)
[^6]: [Seccomp security profiles for Docker | Docker Documentation](https://docs.docker.com/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile)

僕はnightlyをあまり使いたくなかったのでcargo-kcov + Travis CI + Codecovでカバレッジを計測しました。

### cargo-kcov + Travis CI + Codecovの設定サンプル
cargo-kcovではバックエンドとしてkcovを使う必要があります。

[Codecovの提供するサンプルコード](https://github.com/codecov/example-rust)では、毎回kcovのソースをmakeしていて、あまりビルドが速くなかったため、キャッシュを使うように変更しています。

```yaml:.travi.yml

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](https://crates.io/crates/cargo-update)というクレートがあるらしく、これをインストールすればいいらしいです。

```yaml
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](https://codecov.io/gh)にいってカバレッジが取れているかを確認します。

<img width="1172" alt="codecov.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214775/83ac2a2e-f66b-aa3a-8896-2d5a150ba424.png">

# まとめ
クレートを公開してみた感想ですが、公式のドキュメントがしっかりしていたので公開までは意外と簡単にできました。
一方で、CIやカバレッジ計測に関しては自分で調査しないといけないようなハマりどころも多く、納得して動かせるようになるまでにかなり苦戦しました。

また、crates.ioについて、ユーザで名前空間が別れていないので強い名前をつけてしまうとかなり目立ってしまうという印象を受けました。名前空間を占拠しているという自覚を持ってメンテナンスに励みたいと思います。

公開自体は簡単なので、「欲しいライブラリがcrates.ioで見つからない、、」という方は、ぜひ自分で実装して公開してみてはいかがでしょうか？

ここまで読んでいただきありがとうございました。