Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

やりたいこと

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

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

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

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

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

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

失敗例

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

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を生成します。

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

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

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からの相対で探してほしい)

成功例

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

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...]の部分です。これを実行すると以下が生成されます。

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宣言を自動生成することができるようになりました。

dalance
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away