Rust のビルド情報を簡単に表示するライブラリ

Rust プログラムのビルド情報を表示するためのちょっとした小技系ライブラリの紹介です。

<a href="https://github.com/yushiomote/ever"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/yushiomote/ever.png" width="460px"></a>

## ビルド情報

デプロイされてるバイナリの動きが怪しいが、どのコミットで壊れたか調べたい。そもそも誰がどこでビルドしたのか？どのコミットでつくられたの？この辺がわからなくて混乱や余計な手間がかかる時があります。CI/CD 等が確立されてしっかり管理されてれば避けられる問題かもですが。それでも往々にしてプログラムでビルド情報を表示できるようしたくなります。

```sh
# こんなやつ
$ ./my_program -version
v0.1.24
 commit: abcdef89726d
 date: 2020-10-31
```

## Build Script

Rust においてビルド情報を集めてプログラムで表示できるようにする正攻法では [Build Script](https://doc.rust-lang.org/cargo/reference/build-scripts.html) (`build.rs`) を使うことになるかと思います。Build Script とはビルド時に実行される Rust のコードです。主に、依存する C 言語のライブラリを事前にコンパイルしたり、Rust のコード自体を生成したりといった目的のためにあります。

ビルド情報を表示できるようにするためには、この Build Script を使って、ビルド時のコミットハッシュや場所等のビルド情報を集めて、プログラム本体に組み込みます。

```
.
├── Cargo.lock
├── Cargo.toml
├── build.rs ... ビルド時にこれが実行されるのでここでビルド情報を集める
└── src
    └── main.rs
```

Rust でビルド情報を表示する既存の crates たちも基本的に、ユーザが自分で Build Script を書く必要があります。

• [build_info](https://github.com/danielschemmel/build-info/)
• [built](https://github.com/lukaslueg/built)

例えば、 `build_info` という crate では、このように、`build.rs` を用意します。

```rust:build.rs
fn main() {
    // ここでビルド情報を収集する。
    build_info_build::build_script();
}
```

そして、このようにすることで、 `main.rs` からビルド情報が取れるようになります。

```rust:main.rs
build_info::build_info!(fn build_info);

fn main() {
    println!("{:#?}", build_info());
}
```

これでも十分シンプルではあります。ただ、たくさんの Rust アプリを開発するにあたって `build.rs` すら省いて、もっと貪欲に少ない手数でビルド情報を付けられないかなと思いました。

# ever

このライブラリは `main` の先頭でマクロをひとつ呼んでおけばビルド情報を表示できるようになります。`build.rs` は不要です。

```rust
use ever::ever;

fn main() {
    ever!();

    println!("Hello, world!");
}
```

ビルド情報を表示したいときだけ、プログラム実行時に環境変数 `EVER` をセットします。これでプログラムはビルド情報をはいて停止します。

```sh
$ EVER=1 ./my_program
my_program 0.1.0 (debug):

    date:     Sat Dec  5 11:17:09 2020 +0900
    commit:   49fec228607448df6fcb8950171441a1f56c2e7b-dirty
    user:     me
    host:     my_host
    builddir: /home/me/my_program
    rustc:    1.48.0 (7eac88abb 2020-11-16)
```

環境変数 `EVER` をセットしなければ通常の処理が走ります。

```sh
$ ./my_program
Hello, world!
```

基本機能はこれだけです。

補助機能として、各種情報を個別に取れるような API もあります。

コミットハッシュを表示する API です。

```rust
// "49fec228607448df6fcb8950171441a1f56c2e7b-dirty" のように出力されます。
println!("{}", ever::build_commit_hash!());
```

コミットハッシュ末尾の `-dirty` はコミットされてないファイルがあるときにビルドした場合に付けられます。

また、以下はビルドに使用したコンパイラ `rustc` のバージョンを表示する API です。

```rust
// "1.48.0 (7eac88abb 2020-11-16)" のように出力されます。
println!("{}", ever::rustc_version!());
```

API 一覧は [こちら](https://docs.rs/ever/0.2.0/ever/index.html)。

# 仕組み

`build.rs` のほかにビルド時に実行される Rust コードがあります。[`proc-macro`](https://doc.rust-lang.org/reference/procedural-macros.html) です。 `proc-macro` とは、 `macro` なのですが、 Rust のコードを Rust のコードでパース＆生成するタイプの `macro` です。

そこで `ever` は `proc_macro` 内ででビルド情報を収集しています。

```rust
// これが proc-macro の定義です。 `ever!()` というマクロが呼ばれると、ビルド時にこの関数が評価されます。
pub fn ever(input: TokenStream) -> TokenStream {
    // ...

    // ここはビルド時に走るロジックです。それぞれで一生懸命ビルド情報を収集しています。
    let name = vars::package_name();
    let version = vars::package_version();
    let about = vars::package_description();
    let date = vars::build_date();
    let commit = vars::build_commit_hash();
    let user = vars::build_username();
    let host = vars::build_hostname();
    let builddir = vars::build_dir();
    let rustc = vars::rustc_version();
    let mode = vars::build_mode();
    let lock = vars::lock_file();

    // 以下 `quote!` で囲まれた部分が
    // `ever!()` マクロが展開されたときに出来上がる Rust コードです。
    // `main` の先頭にこのコードが挿入されます。
    (quote! {
        match std::env::var("EVER").as_deref() {
            Ok("1") | Ok("true") => {
                // 環境変数 `EVER` がセットされていればビルド情報を表示。
                println!(
                r#"{name} {version} ({mode}): {about}
    date:     {date}
    commit:   {commit}
    user:     {user}
    host:     {host}
    builddir: {builddir}
    rustc:    {rustc}"#,
                    name = #name,
                    version = #version,
                    about = #about,
                    date = #date,
                    commit = #commit,
                    user = #user,
                    host = #host,
                    builddir = #builddir,
                    rustc = #rustc,
                    mode = #mode,
                );

                std::process::exit(1);
            }
            // ...
            _ => {}
        }
    }).into()
}
```

ビルド情報を回収する際に以下の crates を利用しています。

* [whoami](https://github.com/libcala/whoami): ユーザ名やホスト名を取得しています。
* [rustc_version](https://github.com/Kimundi/rustc-version-rs): コンパイラバージョンを取得しています。
* [git2](https://github.com/rust-lang/git2-rs): コミットハッシュなどを取得しています。

## `proc-macro-hack` について

`ever` は [`proc-macro-hack`](https://github.com/dtolnay/proc-macro-hack) というちょっととがった名前の crate を利用しています。この理由は Rust 1.45 までは `proc-macro` で作られたマクロが式の位置に配置できない問題がありました。

```rust
// これができなかった。
println!("{}", ever::rustc_version!());
```

それを解決するのが `proc-macro-hack` でした。`proc-macro-hack` でラップされた `proc-macro` は式の位置に配置できるようになります。そこで、`ever` ではこれをつかっていました。今でも、1.45 以前の Rust でも利用できるように使い続けています。

## おまけ機能： Cargo.lock の吐き出し

おまけ機能として Cargo.lock が吐き出せます。環境変数 `EVER` に `dump_lock` を指定します。

`ever` コンパイラのバージョンも吐き出せますので、仮に Cargo.lock や rust-toolchain で依存関係やコンパイラが固定できていなくても、Cargo.lock とコンパイラバージョンでざっくりとビルド時の状態を再現できるようになります。

```sh
$ EVER=dump_lock ./your_program
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
    name = "autocfg"
    version = "1.0.1"
    source = "registry+https://github.com/rust-lang/crates.io-index"
...
```

## まとめ

`ever` というビルド情報を表示するちょっとしたライブラリの紹介でした。