build.rsでmod宣言を自動生成する

# はじめに

`build.rs`とは、Rustのプロジェクトにてビルド前に実行したい処理を書いておくソースコードです。例えば関連するCのコードをコンパイルしたり、Rustのコードを生成したりできます。今回この`build.rs`にてサブモジュールを宣言する`mod ...`を生成しようとしたところ、素直にはできなかったのでやり方をまとめておきます。

# やりたいこと

以下のようなディレクトリ構成を想定します。

```
|-- build.rs
|-- Cargo.toml
`-- src
    |-- main.rs
    |-- plugins
    |   |-- xxx.rs
    |   `-- yyy.rs
    `-- plugins.rs
```

このような構成の場合、`plugins.rs`は以下のようになります。

```rust:plugins.rs
pub mod xxx;
pub mod yyy;
pub use xxx::*;
pub use yyy::*;
```

`pub use`は必須ではありませんが、このようにエクスポートしておくことで`use plugins::*`のように一括でインポートできるようになります。

ここで、pluginsディレクトリの中身が大量にあり、頻繁に足したり消したりする場合を考えます。そのようなときに毎回`plugins.rs`を書き換えるのは面倒なのでなんとか自動生成したい、というのが今回の目的です。

# 失敗例

最初に失敗例を載せておきます。

```rust:build.rs
fn main() {
    let out_dir = env::var("OUT_DIR").unwrap();

    let mut plugins = Vec::new();
    for entry in WalkDir::new("src/plugins") {
        let entry = entry.unwrap();
        if entry.file_type().is_file() {
            let file_name = String::from(entry.path().file_stem().unwrap().to_string_lossy());
            plugins.push(file_name);
        }
    }

    plugins.sort();

    let out = Path::new(&out_dir).join("gen.rs");
    let mut out = File::create(&out).unwrap();

    for file_name in &plugins {
        let _ = write!(out, "pub mod {};\n", file_name);
        let _ = write!(out, "pub use {}::*;\n", file_name);
    }
}
```

`build.rs`では`WalkDir`を使って`src/plugins`以下のファイル一覧を取得し、以下のような`gen.rs`を生成します。

```rust:gen.rs
pub mod xxx;
pub use xxx::*;
pub mod yyy;
pub use yyy::*;
```

これを`include!`マクロを使って取り込みます。

```rust:plugins.rs
include!(concat!(env!("OUT_DIR"), "/gen.rs"));
```

これをコンパイルすると

```
error[E0583]: file not found for module `xxx`
 --> ..../target/debug/build/..../out/gen.rs:1:9
  |
1 | pub mod xxx;
  |         ^^^
  |
  = help: name the file either xxx.rs or xxx/mod.rs inside the directory "..../target/debug/build/..../out"
```

というエラーになります。これは「サブモジュールのファイルを`gen.rs`からの相対パスで探しにいって失敗している」ということです。（本当は`plugins.rs`からの相対で探してほしい）

# 成功例

「サブモジュールのパスが見つからないなら指定すればよい」ということで以下が成功例です。

```rust:build.rs
fn main() {
    // 追加
    let root_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let out_dir = env::var("OUT_DIR").unwrap();

    let mut plugins = Vec::new();
    for entry in WalkDir::new("src/plugins") {
        let entry = entry.unwrap();
        if entry.file_type().is_file() {
            let file_name = String::from(entry.path().file_stem().unwrap().to_string_lossy());
            plugins.push(file_name);
        }
    }

    plugins.sort();

    let out = Path::new(&out_dir).join("gen.rs");
    let mut out = File::create(&out).unwrap();

    for file_name in &plugins {
        // 追加
        let _ = write!(
            out,
            "#[path = \"{}/src/plugins/{}.rs\"]\n",
            root_dir, file_name
        );
        let _ = write!(out, "pub mod {};\n", file_name);
        let _ = write!(out, "pub use {}::*;\n", file_name);
    }
}
```

追加は`#[path...]`の部分です。これを実行すると以下が生成されます。

```rust:gen.rs
#[path = "..../src/plugins/xxx.rs"]
pub mod xxx;
pub use xxx::*;
#[path = "..../src/plugins/yyy.rs"]
pub mod yyy;
pub use yyy::*;
```

`#[path]`は`mod`に付けることのできるアトリビュートで、そのモジュールのファイルパスを指定できます。このようにすることで、`build.rs`からmod宣言を自動生成することができるようになりました。