Rustにおけるバージョン管理,ライセンス管理,テスト,ドキュメンテーション,CI/CDのためのcrate,CLIツール,GitHub Actionなどについてまとめた.本記事では以下を扱う.
- Cargoプロジェクトにおける
Cargo.toml/Cargo.lockの役割及びバージョン管理 - 依存関係のライセンス管理
- Format・Lint
- 各種テストの実装方法と実行
- llvm-covによるテストカバレッジの計測
- メモリ及びスレッドのValgrindによる動的解析
- ドキュメンテーション
- GitHub Actions
依存パッケージ・バージョン管理
Cargo.toml と Cargo.lock
周知のように,Cargo.toml には当該プロジェクトにおいて直接使用するcratesのバージョン制約条件が記載される.間接的(推移的)な依存関係にあるcrateのバージョン制約条件には一切触れない.
例えば,anyhow = "1" とだけ書いた場合には,Cratesレジストリであるcrates.ioに登録された anyhow という名称のcrateであって,major version 1であるようなもの(の中でその他の依存関係と両立的な範囲のもの)という制約条件と解釈される.anyhow が依存するcrateや,さらにそのcrateが依存するcrate,…といった推移的依存関係についてのバージョン制約は一切行われない.
これに対して Cargo.lock は,当該プロジェクトの全ての推移的依存関係について,その正確なバージョン及びSHA-256ダイジェストが指定される.
Cargo.lock は次のようなエントリからなる:
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
ここで,crate name,semantic version(major.minor.patch),source(crates.ioやGitHubなど),当該バージョンのSHA-256ダイジェストが記録されている.
ビルド時には,指定されたsourceから指定されたバージョンのcrateをフェッチしてくることになるが,その際 Cargo.lock に記載のSHA-256ダイジェストとの照合が行われる(ハッシュピンニング).これにより改竄された依存関係が誤ってインストールされることを防いでいる.サプライチェーン攻撃対策になるわけである.ただし,lockファイルによって管理されるのはRust言語の範囲内の依存関係のみであって,Foreign Function Interface(FFI)を介して利用される他言語のライブラリの依存関係については管理されないことに注意.
lockファイルは cargo clippy や cargo build など,推移的依存関係の解決が必要な操作を実行した際に,自動生成される.より正確には,lockファイルが存在しない場合にのみ自動生成され,以降は更新の必要がある場合(tomlファイルに依存crateを追加した場合など)のみ最小限の更新が行われる.原則として手動作成・編集は推奨されない.
依存crateが新しいバージョンをリリースしても,自動では Cargo.lock には反映されないことに注意.これは Cargo.lock がビルドに使用する依存crateのバージョンをlockするためのものだからである.依存関係をアップデートしたい場合は
-
cargo updateで指定したcrateを個別にアップデート -
Cargo.tomlの該当crateのバージョン指定を書き換え → 依存関係の解決が必要な操作を実行 → 関連する依存crateのみバージョン更新 -
cargo generate-lockfileでCargo.lockを再生成
などを行う.最後の方法は全ての依存crateが Cargo.toml に指定された制約条件を満たす最新バージョンに更新されるので注意.
Gitリポジトリへのlockファイルの同梱の有無
バイナリcrateについてはビルド再現性(reproducibility)を担保するために同梱することが推奨される.一方でライブラリcrateについては同梱しないことが多い1.
後述するが,GitHub Actions workflowにおいて,インストール済み依存関係を含むビルドアーティファクトをキャッシュすることができるが,このキャッシュのキーとして Cargo.lock が用いられる.
なお Cargo.lock はcrates.ioには登録できない(パッケージとして同梱できない).
cargo generate-lockfile
静的解析・ビルドなどは行わず,依存関係の解決と Cargo.lock の生成のみを行いたい場合に使う.RustupにてRust開発環境をインストールしていれば,基本的なコマンド build test fmt とともにインストールされる.
cargo generate-lockfile
Cargo.lock が既に存在する場合には Cargo.toml のバージョン指定(及び rust-version)と両立的する範囲の最新までアップデートされる.
cargo-msrv — MSRV自動判定
Minimum Supported Rust Version(MSRV)を自動探索してくれるツールである.Rustツールチェインのバージョンを変えながら cargo check を実行し,二分探索か何かでMSRVを決定するのだと思われる.
# Install
cargo install cargo-msrv
# Find MSRV
cargo msrv find
# Check MSRV
cargo msrv verify
# List dependencies' MSRVs
cargo msrv list
ライセンス管理
バージョンとライセンスの関係
OpenSSLは主にC言語で書かれた暗号化ライブラリだが,OpenSSL 3.xより古いバージョンでは独自ライセンス(OpenSSLライセンス)で提供されており,OpenSSL 3.xからライセンスがApache-2.0に変更された.そのため,古いOpenSSLからのコード移植を含むソフトウェアのライセンスは,OpenSSLライセンスとの連言(conjunction; AND)の形になっている場合が多い.実際,aws-lc-sys v0.38.0のライセンスは "ISC AND (Apache-2.0 OR ISC) AND OpenSSL" という複雑怪奇なことになっているが2,これは
- aws-lc-sysはBoringSSLのforkであり,aws-lc-sysに固有のコードは "Apache-2.0 OR ISC" のデュアルライセンスである
- BoringSSLはOpenSSLのforkであり,BoringSSLに固有のコードはISCライセンスである
- OpenSSLはOpenSSLライセンスである
ことに由来する.
このようにバージョン管理とライセンス管理は不可分の関係にある.
cargo-license — 依存関係のライセンス確認
Cargo.lock の依存関係のライセンスを収集して,ライセンスごとに整理して表示してくれるもの.Cargo.lock が存在しない場合は自動生成される.
# Install
cargo install cargo-license
# Run
cargo license
例えば以下のような出力が得られる.SPDXフォーマットのライセンス毎に,依存crate及びworkspace crateが整理されて表示される.
$ cargo license
(Apache-2.0 OR MIT) AND Unicode-3.0 (1): unicode-ident
Apache-2.0 (3): tpm2-cli, tss-esapi, tss-esapi-sys
Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT (1): wasi
Apache-2.0 OR MIT (73): android_system_properties, anstream, anstyle, anstyle-parse, anstyle-query, anstyle-wincon, anyhow, autocfg, base64, bitfield, bumpalo, cc, cfg-if, chrono, clap, clap_builder, clap_derive, clap_lex, colorchoice, core-foundation-sys, enumflags2, enumflags2_derive, find-msvc-tools, flexi_logger, getrandom, heck, hex, iana-time-zone, iana-time-zone-haiku, is_terminal_polyfill, itoa, js-sys, libc, log, num-derive, num-traits, oid, once_cell, once_cell_polyfill, picky-asn1, picky-asn1-der, picky-asn1-x509, pkg-config, proc-macro2, quote, regex, regex-automata, regex-syntax, rustversion, serde, serde_bytes, serde_core, serde_derive, serde_json, shlex, stable_deref_trait, syn, thiserror, thiserror-impl, utf8parse, wasm-bindgen, wasm-bindgen-macro, wasm-bindgen-macro-support, wasm-bindgen-shared, windows-core, windows-implement, windows-interface, windows-link, windows-result, windows-strings, windows-sys, zeroize, zeroize_derive
Apache-2.0 WITH LLVM-exception (1): target-lexicon
MIT (5): hostname-validator, mbox, nu-ansi-term, strsim, zmij
MIT OR Unlicense (2): aho-corasick, memchr
cargo-deny — 依存関係のライセンス要件の充足性チェック
設定ファイル deny.toml に許容するライセンス,許容しないライセンスなどを指定しておくと,依存関係を走査して問題ないかチェックしてくれる.
# Install
cargo install cargo-deny
# Initialise deny.toml
cargo deny init
# Check
cargo deny check licenses
ありとあらゆる許容ライセンスを事前に指定するのは手間なので,cargo license を実行して出てきたライセンスのみ,Crateの配布形態・ライセンスと両立的かチェックし,問題ないライセンスをポジティブリストに追加するといいだろう.
それとは別に,CC-BY-SAやGPLのように継承が必須であるようなライセンスや,crateの配布形態において特別な手順(利用申請等)を要するライセンスなど,依存関係に含まれていると面倒なライセンスをネガティブリストに追加してもよい.
ただしライセンス同士の共存・競合などの「内的」な関係までをもチェックしてくれるわけではない.あくまでポジティブリスト及びネガティブリストに記載したライセンスの有無をチェックするだけである.
依存関係のライセンスだけでなく脆弱性情報のチェック機能なども持つ.あまりに多機能なのでやや使いづらい気もする.依存関係の脆弱性チェックであれば cargo-audit という単機能のツールも利用可能である.
cargo-about — サードパーティライセンス文書の自動生成
依存crateをライセンス毎に整理したHTMLを生成してくれるもの.バイナリを配布する場合など依存crateのライセンス文書を同梱したい場合に便利である.
# Install
cargo install cargo-about
# Initialise about.toml and about.hbs
cargo about init
# Generate HTML
cargo about generate about.hbs > license.html
設定ファイル about.toml に cargo-deny と同様にライセンスのallowリスト/denyリストを記述すると,これに基づき cargo deny check licenses と同様に依存crateのライセンスチェックを実行する.また about.hbs には出力されるHTMLのフォーマットを記述する.
Format/Lint
rustfmt — 標準フォーマッタ
Rustの標準フォーマッタである.インデントや改行などが一定の規則に従っているかをチェックし自動修正する.
とくに何も指定せずにRustをインストールすればデフォルトで入っている.
# Install
rustup component add rustfmt
# Run
cargo fmt --all
cargo-clippy — 標準Lintツール
依存関係を解決してコンパイルできるか(リンクは実行しない),慣用的(idiomatic)なコーディングスタイルに準拠しているか,#[deprecated] 属性を持つ公開APIを使用していないか,などをチェックするとともに,可能な場合は自動解決する.
とくに何も指定せずにRustをインストールすればデフォルトで入っている.
# Install
rustup component add clippy
# Run
cargo clippy --all-targets
テスト
cargo test — 標準テストツール
Rust toolchainに予め同梱されているツールであり,crateを test モードでコンパイルした後,単体テスト,統合テスト,ドキュメントテスト等を実行する.
.
├── Cargo.toml
├── Cargo.lock
├── src/ # crate + unit tests + Rust docs
├── tests/ # integration tests
├── benches/ # benchmark tests
└── examples/ # example binary crates for library crate
Unit test
単体テストについては,crateを構成する各モジュール内にて,以下のような形で記述する:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_f() {
let result = f(sample_input);
assert!(post_execution_condition);
}
}
ここで #[cfg(test)] 注釈があるから,通常のビルド時には tests モジュールはビルドされず,cargo test 実行時のみビルドされる.
Integration test
統合テストは tests/ 内に記述する.複数モジュールが絡むテストや,(バイナリcrateの場合)ビルドされた実行可能バイナリを使ったテストなどは,ここに含める.単体テストの場合と違って #[cfg(test)] 注釈は不要である.というのも tests/ ディレクトリ内の各モジュールは cargo test 実行時のみビルドされるからである.
Doc test
ドキュメンテーションコメント(///)内にはRustコードブロックを含めることができる.例えば,public APIの使用方法を
/// short description of `f`
///
/// ```
/// use example_crate::f;
///
/// let result = f(sample_input);
/// ```
pub fn f(...) {
...
}
のようにコメント内に含めることができる.このコードブロックは cargo test の際にコンパイル・実行される.したがってコンパイル・実行可能な完全な形で記述する必要がある.単体では実行できないスニペットを記述するとドキュメンテーションエラーとして扱われる.
当該コードブロックが環境依存のコードを含む場合,外部サーバとの通信を伴う場合など,ドキュメントテスト時に実行されるべきでない場合には,コードブロックを no_run 指定することで,実行しないようにできる.ただしこの場合もコンパイルは実行される.
/// short description of `f`
///
/// ```no_run
/// use example_crate::f;
///
/// let result = f(sample_input);
/// ```
pub fn f(...) {
...
}
Benchmark test
実行時間を(必要なら複数回繰り返して)計測してプロットしてくれるもの.単体テストと統合テストの両方で利用可能.
Example crate / test
ライブラリcrateは単独では実行可能バイナリを生成しない.当該ライブラリを使用法を例示する実行可能バイナリを同梱したい場合には examples/ ディレクトリ内に実装すればよい.
Example crateは cargo run --example crate_name でコンパイル・実行できるほか,cargo test によるテストも可能である.
テストの実行
# 全てのテストを実行
cargo test
# ライブラリtargetテスト
cargo test --lib
# バイナリtargetテスト
cargo test --bins
cargo test --bin bin_crate_name
# example targetテスト
cargo test --examples
cargo test --example example_crate_name
# bench targetテスト
cargo test --benches
cargo test --bench bench_name
# 全targetテスト
cargo test --tests
cargo test --test test_name
# ドキュメントテスト
cargo test --doc
cargo-valgrind — メモリ・スレッドエラーの動的解析ツール
Pure Rustは基本的にメモリ安全であるが,低レベル領域では生ポインタを弄らざるをえない状況や,インラインアセンブリでCPU命令を直接叩きたくなる状況もある.また,低レベルに限らず,他言語のソフトウェアスタックをそのまま利用したくなる状況は少なくない.極限まで最適化された数値計算ライブラリが他言語に存在するが,Rust実装が存在しない場合などである.このような場合にはFFIを通してメモリ安全でない他言語のライブラリを呼び出す必要が生じる.
このように,Rustで書かれたプログラムであっても,メモリ安全性が必ずしも保証されておらず,メモリリーク等のバグが混入している可能性がある.とくにメモリリークは必ずしもエラーやパニックを引き起こさないので,通常のテスト(cargo test)では必ずしも検出できない.
メモリ管理・スレッド管理におけるバグを静的解析によって検出することは(原理的に不可能な訳ではないが)困難である.そこで,仮想マシン上で実行可能バイナリを実行してトレースすることで,これらのバグを動的に検出するためのツールがValgrindである.これのRust wrapperが cargo-valgrind である.
# Install
apt update && apt install -y valgrind
cargo install cargo-valgrind
# Run
cargo valgrind test
cargo-llvm-cov — テスト網羅性の測定
テストはある種の仕様記述であり,テストを実行するということは,コードの仕様適合性の検証(verification)にほかならない.当該テストが要求仕様を記述し尽くしたようなテストであるなら構わないが,現実のテストは幾つかの典型例に対して,期待される入出力(例外・エラーを含む)の組み合わせとの一致を見るに留まる.
このようなテストにおいてはカバレッジ(coverage;網羅性)が重要である.カバレッジを高めることはテストの信頼性(テストをパスすることによるコードに対する信頼の度合い)に繋がるからである.カバレッジといっても,コードサイズ(LOCなど)を基準として実際に実行されるコードの割合,全ての実行パスに対して実際に通るパスの割合,実際に実行される関数・モジュールの割合など,測定方法は色々あるだろう.
llvm-covは,テストを実行するとともに,テストカバレッジを複数の尺度で測定し,モジュール単位及びコード全体に対するカバレッジの統計レポートを出力してくれるツールである.これのRustラッパがcargo-llvm-covである.
# Install
cargo install cargo-llvm-cov
# Run
cargo llvm-cov
# => target/llvm-cov/*
カバレッジレポートの出力フォーマットはプレーンテキスト(--text)のほか,HTML(--html),JSON(--json),LCOV(--lcov),Codecov(--codecov)などから選べる.構造化されたフォーマットを選べばCI/CDにも組み込みやすいだろう.
ドキュメンテーション
cargo-doc — Rustドキュメント生成
テストに記載の通り,RustにおけるパブリックAPI(関数・型・メンバ等)のドキュメンテーションコメントは /// を使って書く.パブリックモジュールのdocコメントには //! を使う.いずれもMarkdown記法を用いる.これを cargo doc でビルドすることで,HTML形式のドキュメントページが生成される.
基本的にはライブラリcrateのドキュメンテーションのためのものである.とくにオプションを指定しない限り公開APIのみドキュメントが生成される.バイナリcrateは公開APIを持たないのが普通であるので,大抵は main.rs モジュールのdocコメントだけが出力される.
# Install
rustup component add rust-docs
# Run
cargo doc --no-deps
# Run & open
cargo doc --no-deps --open
cargo-rdme — READMEファイル生成
文字通り README.md を自動生成する.正確に言えば, lib.rs(ライブラリcrate)または main.rs(バイナリcrate)のトップレベル(モジュール)docコメントをそのまま README.md に流し込むものである. README.md ファイルの中に
<!-- cargo-rdme start -->
<!-- cargo-rdme end -->
というHTMLコメントを入れておくと,これで囲まれた部分がトップレベルdocコメントに記載された内容へと自動更新される.
.cargo-rdme.toml ファイルを用いて挙動を変えることもできる.
# Install
cargo install cargo-rdme
# Generate (replace) README.md
cargo rdme
# Check only
cargo rdme --check
clap_mangen — CLIツールのドキュメント生成
RustでCLIツールを実装する場合は clap を使うのがde facto standardである.コマンドラインオプションを enum とdocコメントを用いて定義すると,ここからCLIパーサが自動生成されるというものである.ヘルプメッセージ表示(--help, -h)やバージョン表示(--version, -v)などのオプションは自動生成される.
clap_mangen は clap を用いて実装されたCLIツール(バイナリcrate)から,コマンド/サブコマンドの使用方法などを記したドキュメント(man page)を自動生成するツールである.ビルドスクリプト build.rs またはcargo-xtaskとともに使う.
GitHub Actions
dtolnay/rust-toolchain — Rustツールチェインのセットアップ
GitHub Actions runnerにRustツールチェインをインストールするためのGitHub Actionである.
Swatinem/rust-cache — ビルド成果物のキャッシュ管理
Rust/Cargoプロジェクトのリポジトリにおいて,ビルド成果物などを適切にキャッシュするためのGitHub Actionである.これによりビルド済みの依存関係の再ビルドが抑制され,GitHub Actions workflowの実行時間が大幅に短縮される.cache key周りのオプションを変更することにより,どのkeyからキャッシュを探索し,どのkeyにキャッシュを書き込むか,などを細かく設定することもできる.
デフォルトでは Cargo.lock がcache keyにmixされている3.
actions-rust-lang/setup-rust-toolchain — Rustツールチェインのセットアップ + ビルド成果物のキャッシュ管理
GitHub Actions runnerにRustツールチェインをインストールするためのGitHub Actionであるが, rust-cache と統合されている.つまりこれを使っておけばRustツールチェインのインストールとビルド成果物のキャッシュを一度に済ませることができる.
enarx/spdx — SPDXライセンス識別子のチェック
リポジトリ内の全てのRustソースファイルにSPDXライセンス識別子が付与されているかどうかをチェックする.次のようなコメントを各ソースファイルに記載しておく:
// SPDX-License-Identifiers: Apache-2.0 OR MIT
Rust以外の幾つかの言語(C,C++,Python,Shellなど)のソースファイルについてもチェックされるが,主要言語であっても未対応なものが多い.例えばGoやZigは非対応である.
-
というのも依存解決時にupstream側のlockファイルは無視されるからである.依存解決はdownstream側の責任だからである.さもなくば,downstream側は,全ての推移的依存関係について全く同じバージョンを指定しているlibrary crateしか利用できなくなるが,これは現実的でない. ↩
-
論理的には "ISC AND (Apache-2.0 OR ISC)" と "ISC" は同値である.束論における吸収律(absorption law)に対応する.よって "ISC AND (Apache-2.0 OR ISC) AND OpenSSL" のライセンスというのは全体としては "ISC AND OpenSSL" と変わらないように思われるかもしれない.しかし, "AND" 節のそれぞれのライセンスは,当該ソフトウェアの異なる部分に対して適用されるライセンスであって,それらのライセンスの論理的連言に相当するライセンスが全体に対して適用されるわけではない.当該ライセンス表記は
- BoringSSL部分はISC,aws-lc-sys部分はApache-2.0,OpenSSL部分はOpenSSL
- BoringSSL部分はISC,aws-lc-sys部分はISC,OpenSSL部分はOpenSSL
というデュアルライセンスを意味する.なお,このような複雑なライセンスに対して論理的に正しい同値変形を行うツール(license-logic)を開発したので,興味がある読者は試してみてほしい.もちろん,既に述べた通り,これはライセンス的に正しい同値変形ではない. ↩
-
何故なら
Cargo.lockに変化がないならビルド済みの依存関係を使えばいいし,変化があれば新たにダウンロード及びビルドをし直す必要があるからである.よってrust-cacheを有効に機能させるためにはCargo.lockをリポジトリに同梱しておくことが推奨される. ↩