LoginSignup
16
11

More than 5 years have passed since last update.

cargoのソースを読んでみよう - 第1回 : main関数はどこだ

Last updated at Posted at 2017-06-25

三行で頼む

  • main関数の「正しい」見つけ方とは
  • cargoのターゲットまわりは結構複雑。結論だけ知りたいなら binターゲットについてのまとめを参照。
  • main関数のソースの位置を知りたいだけならcargo metadata --no-deps | jq '.packages[].targets[] | select(.kind==["bin"])'が楽よ

前提条件

前回に引き続き、次の環境を想定しています:

  • 使用コンパイラ: rustc 1.15.1
  • 読むソース: cargo 0.16.0

また、例の本は一読していることも想定しています。

前回の補足

補足1: 使用コンパイラについて

前回からだいぶ時間が経って、現行のstableリリースは1.17.0となっているわけですが、ここで使用しているのは1.15.1ですので、次のようにして1.15.1のバイナリとソースを明示的にインストールしておくことをお勧めします:

% rustup toolchain install 1.15.1
% rustup component add --toolchain 1.15.1 rust-src

なお、racer-emacsが参照するソースをこのバージョンに設定するためには、emacsを起動するときにRUST_SRC_PATHを設定するか、racer-rust-src-pathをcustomize-set-variableすると可能なようですが、正直面倒くさいので、

% rustup default 1.15.1

としてしまっています。cargoプロジェクトごとに使うコンパイラを切り替えられて、それが自動的にracer-emacsにも反映されると楽なんですけどねえ。

なお、rustup toolchainであらかじめインストールされていないバージョンのtoolchainをrustup defaultでデフォルトに設定すると、次に別のバージョンをデフォルトにした時に破棄されてしまい、毎回ダウンロードすることになるようなので、rustup defaultする前に明示的にバージョンを指定してrustup toolchain installしておいた方がいいようです。

追記(2017-07-03)

何気なくrustupコマンドのusageを見ていたら、rustup overrideという機能があることに気づきました。

% cd cargo-0.16.0
% rustup override set 1.15.1
% cd ..
% cd cargo-0.17.0
% rustup override set 1.16.0
...

とかやっておくと、それぞれのソースに対して使用するtoolchainのバージョンが設定できます。また、racer-emacsもこの設定を見てくれるようです。ただし、一つのEmacsプロセスの中では異なるバージョンを混在させることはできないようですが。

補足2: ビルドについて

前回、「あらかじめcargo buildしておくと良い」と書きましたが、オプションなしでcargo buildを実行すると、各crateについて、Cargo.tomlに記述されているバージョンに対して互換性のある中で最新のものを取得してくるため、コードを読むという観点では参照先が安定しないことになります。cargo 0.16.0リリース時点で使用が想定されていたcrateのバージョンはCargo.lockに保存されており、次のように--lockedオプションを付けてビルドすると、このバージョンがダウンロードされます:

% cargo build --locked

なお、以前に--lockedを付けずにcargo buildしてしまった場合には、Cargo.lockが更新されてしまっている場合があるため、一度ワーキングディレクトリをrevertしてやり直すとよいでしょう:

% git reset --hard
% cargo build --locked

はじめに

ソースを読む場合、その目的によって、どこから読み始めるのかが変わってくるのですが、大きく分けると正面から攻める方法と裏口から攻める方法があります。何か特定の調べごとがある場合は裏口からショートカットするのですが、今回は実用的なアプリケーションの標準的な書き方の作法を知るのが目的なので、正面から攻めることにします。つまり、しばらくプログラムのエントリポイントから順番に読んで行こうと思っているところです。

さて、例の本を読むと、Rustのプログラムのエントリポイントはmain関数だということが分かります。そこで、まずはmain関数がどのファイルに書かれているかを探すことになります。まあ、名前が分かってるのだからgrepで候補を絞り込んで目視で探せば一発で見つかるわけですが:

% grep -r "fn main(" .
...大量の偽マッチ...
./src/bin/cargo.rs:fn main() {
./src/cargo/util/toml.rs:    fn main(&self) -> Option<&PathBuf> {
./src/cargo/ops/cargo_new.rs:fn main() {
./src/doc/build-script.md:fn main() {
./src/doc/build-script.md:fn main() {
./src/doc/build-script.md:fn main() {
./src/doc/build-script.md:fn main() {
./src/doc/build-script.md:fn main() {
./src/doc/index.md:fn main() {
./src/doc/guide.md:fn main() {
./src/doc/guide.md:fn main() {

というわけで、src/bin/cargo.rsが目的のファイルだろうと想像できますね。おわり。

……我々プログラマが目の前の仕事を片付ける場合には、こういうアドホックな方法で十分なのですが、今回の目的は、標準的な書き方の作法を知ることなので、「プロジェクトディレクトリに大量に存在する*.rsファイルのうち、cargoはどうやってこのsrc/bin/cargo.rsにたどり着き、このmain関数をエントリポイントとして扱うのか」というところまで掘り下げてみたほうがいいんじゃないかなあと思います。

ビルドシステムとしてのcargoの振る舞い

ドキュメントを見てみる

cargoは他の多くのRustアプリケーションと同様に、ビルドシステムとしてcargoそれ自身(もっといえばcargo buildサブコマンド)を採用しています。cargoの基本的な使い方は、例の本の初っ端、Hello, Cargo!に書いてあるので読んでると思います。しかし、例の本の中には、cargoプロジェクトのファイル構成についてあまり詳しくは述べられておらず、

Cargoはソースファイルが src ディレクトリにあるものとして動く

とか

今回の例では実行可能ファイルを作るので main.rs の名前を引き続き使います。 もしライブラリを作りたいなら lib.rs という名前にすることになります。 この規約はCargoでプロジェクトを正しくコンパイルするのに使われていますが、望むなら上書きすることも出来ます。

という非常にざっくりとした解説しかありません。そこで、cargo自体のドキュメントを漁って、Cargo GuideThe Manifest Formatを読むと、もう少し詳しい仕様が分かります。実行可能ファイルを作るプロジェクトの場合には、以下のような仕様であると記述されています:

  • プロジェクトが生成する実行ファイルのデフォルトのソースファイルは src/main.rs となる。
  • さらに、src/bin/*.rs が存在し、なおかつCargo.tomlに[[bin]]セクションが一つも存在しない場合、これらのファイルにそれぞれ対応する実行ファイルが自動的に生成される。
  • Cargo.toml[[bin]]セクションが一つ以上存在する場合には、各[[bin]]セクションのpathフィールドで明示的に指定された.rsファイルが、その[[bin]]セクションのターゲットの実行ファイルのソースファイルとなる。

そこで、まずはcargoソースの./src/binを覗いてみます。すると、次のようにたくさんのソースファイルが含まれていることが分かります:

% ls src/bin
bench.rs              help.rs            package.rs        test.rs
build.rs              init.rs            pkgid.rs          uninstall.rs
cargo.rs              install.rs         publish.rs        update.rs
clean.rs              locate_project.rs  read_manifest.rs  verify_project.rs
doc.rs                login.rs           run.rs            version.rs
fetch.rs              metadata.rs        rustc.rs          yank.rs
generate_lockfile.rs  new.rs             rustdoc.rs
git_checkout.rs       owner.rs           search.rs

一方でCargo.tomlには、次のような[[bin]]]セクションが一つだけ存在します:

Cargo.toml
[[bin]]
name = "cargo"
test = false
doc = false

そのため、cargoのプロジェクトでは、src/bin/ディレクトリの中にある大量のソースファイル一つ一つに対応する実行可能ファイルが生成されたりはせず、一つの実行可能ファイルcargoだけが生成されることが分かります。なお、これ以外にも次のようなセクションが存在します:

Cargo.toml
[lib]
name = "cargo"
path = "src/cargo/lib.rs"

つまり、このプロジェクトは、cargoという名前のライブラリと、cargoという名前のバイナリを生成することが明示されているということになります。

一方で、ドキュメントからだけでは分からない部分が存在します:

  • 疑問1: [lib]セクションの方には明示的にpathフィールドが設定されているが、[[bin]]セクションにはpathフィールドが存在しない。この場合には何をソースファイルとするのか。
  • 疑問2: src/bin/ディレクトリにある大量の*.rsファイルと、実行ファイルの関係はどうなっているのか。

以下では、これらの振る舞いについて探っていくことにします。

cargo buildの動作を観察する

まずは、cargo buildの動作を観察するところから始めます。cargo build--verboseオプションを渡すと、実際にrustcコマンドがどのようにして起動されているのかを表示することができます(見やすいように加工してあります):

% cargo clean
% cargo build --locked --frozen --verbose
   Compiling crossbeam v0.2.10

        ...省略... (外部crateのビルドが延々と続く)

   Compiling cargo v0.16.0 (file:///home/tshiozak/gitwork/cargo)

     Running `rustc --crate-name cargo src/cargo/lib.rs --crate-type lib -g -C m
etadata=1cc647335d74a3d0 -C extra-filename=-1cc647335d74a3d0 --out-dir /home/tsh
iozak/gitwork/cargo/target/debug/deps --emit=dep-info,link -L dependency=/home/t
shiozak/gitwork/cargo/target/debug/deps --extern libc=/home/tshiozak/gitwork/car
go/target/debug/deps/liblibc-73c6422e7b19c2f3.rlib --extern glob=/home/tshiozak/
        ...省略... (外部crateに対する--extern指定が続く)
2f37841.rlib -L native=/home/tshiozak/gitwork/cargo/target/debug/build/libgit2-s
ys-f3c0c49dc3037181/out/lib -L native=/usr/lib/x86_64-linux-gnu -L native=/usr/l
ib/x86_64-linux-gnu -L native=/usr/lib/x86_64-linux-gnu -L native=/usr/lib/x86_6
4-linux-gnu -L native=/home/tshiozak/gitwork/cargo/target/debug/build/miniz-sys-
428121ff932ec089/out`

     Running `rustc --crate-name cargo src/bin/cargo.rs --crate-type bin -g -C m
etadata=1681829e50c2a656 -C extra-filename=-1681829e50c2a656 --out-dir /home/tsh
iozak/gitwork/cargo/target/debug/deps --emit=dep-info,link -L dependency=/home/t
shiozak/gitwork/cargo/target/debug/deps --extern libc=/home/tshiozak/gitwork/car
        ...省略... (外部crateに対する--extern指定が続く)
2f37841.rlib --extern cargo=/home/tshiozak/gitwork/cargo/target/debug/deps/libca
rgo-1cc647335d74a3d0.rlib -L native=/home/tshiozak/gitwork/cargo/target/debug/bu
ild/libgit2-sys-f3c0c49dc3037181/out/lib -L native=/usr/lib/x86_64-linux-gnu -L 
native=/usr/lib/x86_64-linux-gnu -L native=/usr/lib/x86_64-linux-gnu -L native=/
usr/lib/x86_64-linux-gnu -L native=/home/tshiozak/gitwork/cargo/target/debug/bui
ld/miniz-sys-428121ff932ec089/out`

    Finished debug [unoptimized + debuginfo] target(s) in 130.99 secs

外部crateのビルドを除くと、cargo本体のビルドは、上記2回のrustcの実行ですべてが完結しています。最初のrustc呼び出しで直接コマンドライン引数に指定されているソースファイルはsrc/cargo/lib.rsだけですし、もう一方ではsrc/bin/cargo.rsだけが指定されていることが分かると思います。これで、第一の疑問は何となく解消できそうです。つまり、pathフィールドが存在しない場合にはsrc/bin/バイナリ名.rsというファイルを参照しにゆくのであろうと推測できます。

一方、第二の疑問、つまり、src/cargosrc/binの中にあるその他の*.rsファイルはどのようにして利用されるのかというと、これは例の本のcrateとモジュールの章に書いてある通りです。つまり、Rust言語自体のモジュールの仕組みによって参照されることとなります。

たとえば、src/cargo/lib.rsの先頭付近には次のように書いてあります:

src/cargo/lib.rs
pub mod core;

この指定により、rustcは既定のルールによってsrc/cargo/core/mod.rsを読み込むことになります。そして、このようにして読み込んだファイルにmod文があれば、それに従って再帰的にソースファイルが読み込まれていくことになります。

上のcargo build --verboseの様子を見る限り、これらの*.rsファイルに対してcargo自身が参照などをしている気配はありませんが、実際には全く参照しないというわけでもありません。しかしながら、「main関数はどこだ」という今回のテーマからは外れるため、これ以上は次回以降のテーマとします。

ソースを読んで仕様を確認する

というわけで、pathフィールドが存在しない場合の仕様は何となく推測できたため、本当にそうなっているかをcargoのソースから確認してみましょう。

こういう場合、ソースの先頭から探っていくと効率が悪いため、アタリをつけて探ってみることにします。シンプルなバイナリプロジェクトの場合、src/main.rsがデフォルトのソースファイル名となるというのは先ほども述べました。そこで、「main.rs」という文字列でソースツリーを検索してみます:

% grep -r 'main\.rs' src
src/bin/cargo.rs:    run         Build and execute src/main.rs
src/bin/run.rs:Run the main binary of the local package (src/main.rs)
  ...マニュアルページにヒットしてるので省略...
src/cargo/util/toml.rs:        try_add_file(&mut bins, root_path.join("src").join("main.rs"));
src/cargo/util/toml.rs:                Some(s) => s == "main.rs",
src/cargo/util/toml.rs:                       either src/lib.rs, src/main.rs, a [lib] section, or \
src/cargo/util/toml.rs:        let name = if &**bin == Path::new("src/main.rs") ||
src/cargo/util/toml.rs:                      *bin == layout.root.join("src").join("main.rs") {
src/cargo/ops/cargo_new.rs:        Test { proposed_path: format!("src/main.rs"),     handling: H::Bin },
src/cargo/ops/cargo_new.rs:        Test { proposed_path: format!("main.rs"),         handling: H::Bin },
src/cargo/ops/cargo_new.rs:             relative_path: "src/main.rs".to_string(),
src/cargo/ops/cargo_new.rs:            if i.relative_path != "src/main.rs" {
  ...ドキュメントにヒットしてるので省略...

この中で明らかに怪しいのはsrc/cargo/util/toml.rsです。そこで、このファイルを覗いてみることにします。

Layout::from_project_path()関数

先ほどのgrepでマッチしていたのは、このファイルのLayout::from_project_path()という関数です:

src/cargo/util/toml.rs(抜粋)

/// Representation of the projects file layout.
///
/// This structure is used to hold references to all project files that are relevant to cargo.

#[derive(Clone)]
pub struct Layout {
    pub root: PathBuf,
    lib: Option<PathBuf>,
    bins: Vec<PathBuf>,
    examples: Vec<PathBuf>,
    tests: Vec<PathBuf>,
    benches: Vec<PathBuf>,
}

impl Layout {
    /// Returns a new `Layout` for a given root path.
    /// The `root_path` represents the directory that contains the `Cargo.toml` file.
    pub fn from_project_path(root_path: &Path) -> Layout {
        let mut lib = None;
        let mut bins = vec![];
        let mut examples = vec![];
        let mut tests = vec![];
        let mut benches = vec![];

        let lib_canidate = root_path.join("src").join("lib.rs");
        if fs::metadata(&lib_canidate).is_ok() {
            lib = Some(lib_canidate);
        }

        try_add_file(&mut bins, root_path.join("src").join("main.rs"));
        try_add_files(&mut bins, root_path.join("src").join("bin"));

        try_add_files(&mut examples, root_path.join("examples"));

        try_add_files(&mut tests, root_path.join("tests"));
        try_add_files(&mut benches, root_path.join("benches"));

        Layout {
            root: root_path.to_path_buf(),
            lib: lib,
            bins: bins,
            examples: examples,
            tests: tests,
            benches: benches,
        }
    }

    fn main(&self) -> Option<&PathBuf> {
        self.bins.iter().find(|p| {
            match p.file_name().and_then(|s| s.to_str()) {
                Some(s) => s == "main.rs",
                None => false
            }
        })
    }
}

fn try_add_file(files: &mut Vec<PathBuf>, file: PathBuf) {
    if fs::metadata(&file).is_ok() {
        files.push(file);
    }
}
fn try_add_files(files: &mut Vec<PathBuf>, root: PathBuf) {
    match fs::read_dir(&root) {
        Ok(new) => {
            files.extend(new.filter_map(|dir| {
                dir.map(|d| d.path()).ok()
            }).filter(|f| {
                f.extension().and_then(|s| s.to_str()) == Some("rs")
            }).filter(|f| {
                // Some unix editors may create "dotfiles" next to original
                // source files while they're being edited, but these files are
                // rarely actually valid Rust source files and sometimes aren't
                // even valid UTF-8. Here we just ignore all of them and require
                // that they are explicitly specified in Cargo.toml if desired.
                f.file_name().and_then(|s| s.to_str()).map(|s| {
                    !s.starts_with('.')
                }).unwrap_or(true)
            }))
        }
        Err(_) => {/* just don't add anything if the directory doesn't exist, etc. */}
    }
}

この部分はLayout構造体というものの定義と、その生成ルーチンを表しています。Layout構造体自体は単純なもので、この構造体に固有の実装はこれですべてです。

まず、補助的なプライベート関数から見ていくと、次のように読めます:

  • try_add_file()関数は、第二引数で指定されたファイルが存在すれば、これを第一引数に渡されたベクタに追加する。
  • try_add_files()は、第二引数で指定されたディレクトリの中にあるすべての*.rsファイルを第一引数に渡されたベクタに追加する。

これを踏まえてLayout::from_project_path()関数の中を見てみると、新しくLayout構造体を生成し、この構造体の各フィールドに対して次のような処理をしています:

  • ファイルsrc/lib.rsがあれば、これをlibフィールドにセットする。
  • ファイルsrc/main.rsがあれば、これをbinsベクタフィールドに追加する。
  • ディレクトリsrc/binの中にあるすべての*.rsファイルをbinsベクタフィールドに追加する。
  • ディレクトリsrc/examplesの中にあるすべての*.rsファイルをexamplesベクタフィールドに追加する。
  • ディレクトリsrc/testsの中にあるすべての*.rsファイルをtestsベクタフィールドに追加する。
  • ディレクトリsrc/benchsの中にあるすべての*.rsファイルをbenchsベクタフィールドに追加する。

つまり、Layout::from_project_path()関数が返すLayout構造体のインスタンスの各フィールドが保持しているものは、Cargo.tomlに明示的なターゲットセクションが存在しない場合に自動的にプロジェクトのターゲットに対するソースファイルとして選ばれるものと一致していることが分かります。

次に、Layout::from_project_path()関数を呼び出しているところをgrepで探してみると、src/cargo/ops/cargo_read_manifest.rsread_manifest()関数だけがヒットしました。

read_manifest()関数

src/cargo/ops/cargo_read_manifest.rs(抜粋)
pub fn read_manifest(path: &Path, source_id: &SourceId, config: &Config)
                     -> CargoResult<(EitherManifest, Vec<PathBuf>)> {
    trace!("read_package; path={}; source-id={}", path.display(), source_id);
    let contents = paths::read(path)?;

    let layout = Layout::from_project_path(path.parent().unwrap());
    let root = layout.root.clone();
    util::toml::to_manifest(&contents, source_id, layout, config).chain_error(|| {
        human(format!("failed to parse manifest at `{}`",
                      root.join("Cargo.toml").display()))
    })
}

この関数は、

  • path引数として渡されてきたマニフェストファイル(プロジェクトルートにあるCargo.toml)の中身をすべてcontents変数に文字列の形で読みこむ。
  • マニフェストファイルのあるディレクトリをプロジェクトのルートディレクトリとしてLayout::from_project_path()関数に渡し、Layout構造体のインスタンスを生成する。
  • これらをutil::toml::to_manifest()関数に渡し、実際に使用するマニフェストを得る。

という動作です。返値型など、細かいところで疑問が浮かんできますが、いま知りたい事象に対しては、この詳細はあまり本質的ではないので省きます。1 2

to_manifest()関数

src/cargo/util/toml.rsに戻ってto_manifest()関数を読んでみましょう:

src/cargo/util/toml.rs(抜粋)
pub fn to_manifest(contents: &str,
                   source_id: &SourceId,
                   layout: Layout,
                   config: &Config)
                   -> CargoResult<(EitherManifest, Vec<PathBuf>)> {
    let manifest = layout.root.join("Cargo.toml");
    let manifest = match util::without_prefix(&manifest, config.cwd()) {
        Some(path) => path.to_path_buf(),
        None => manifest.clone(),
    };
    let root = parse(contents, &manifest, config)?;
    let mut d = toml::Decoder::new(toml::Value::Table(root));
    let manifest: TomlManifest = Decodable::decode(&mut d).map_err(|e| {
        human(e.to_string())
    })?;

    return match manifest.to_real_manifest(source_id, &layout, config) {
        Ok((mut manifest, paths)) => {
  ...省略...
            Ok((EitherManifest::Real(manifest), paths))
        }
        Err(e) => {
  ...省略...
        }
    };
  ...省略...
}

全部書くと長いので、必要なところだけを抜粋しています。この処理の概要は、

  • parse()関数でCargo.tomlを構文解析し、再帰的なKey-Valueストア構造でこれを格納したtoml::Table構造体を生成する。3
  • これをRustのシリアライザであるrust_serialize crate の仕組みを使ってTomlManifest構造体へとデシリアライズする。
  • TomlManifest::to_real_manifest()関数を呼び出すことによって、TomlManifest構造体の内容を検証しつつ、実際にcargoの他のモジュールから利用しやすいManifest構造体へと変換する。

となっています。いま知りたいことにとって重要なのは、TomlManifest構造体からManifest構造体へと変換するTomlManifest::to_real_manifest()関数4の処理です。

TomlManifest::to_real_manifest()関数

この関数の中で、[[bin]]セクションの処理に相当する部分を抜粋します:

src/cargo/util/toml.rs(抜粋)
impl TomlManifest {
    fn to_real_manifest(&self,
                        source_id: &SourceId,
                        layout: &Layout,
                        config: &Config)
                        -> CargoResult<(Manifest, Vec<PathBuf>)> {
  ...省略...
        let bins = match self.bin {
            Some(ref bins) => {
                let bin = layout.main();

                for target in bins {
                    target.validate_binary_name()?;
                }

                bins.iter().map(|t| {
                    if bin.is_some() && t.path.is_none() {
                        TomlTarget {
                            path: bin.as_ref().map(|&p| PathValue::Path(p.clone())),
                            .. t.clone()
                        }
                    } else {
                        t.clone()
                    }
                }).collect()
            }
            None => inferred_bin_targets(&project.name, layout)
        };
  ...省略...
        // Get targets
        let targets = normalize(&lib,
                                &bins,
                                new_build,
                                &examples,
                                &tests,
                                &benches);
  ...省略...

ついでに、このコードから呼ばれているinferred_bin_targets()関数とvalidate_binary_name()関数も引用しておきます:

src/cargo/util/toml.rs(抜粋)
fn inferred_bin_targets(name: &str, layout: &Layout) -> Vec<TomlTarget> {
    layout.bins.iter().filter_map(|bin| {
        let name = if &**bin == Path::new("src/main.rs") ||
                      *bin == layout.root.join("src").join("main.rs") {
            Some(name.to_string())
        } else {
            bin.file_stem().and_then(|s| s.to_str()).map(|f| f.to_string())
        };

        name.map(|name| {
            TomlTarget {
                name: Some(name),
                path: Some(PathValue::Path(bin.clone())),
                .. TomlTarget::new()
            }
        })
    }).collect()
}
...
    fn validate_binary_name(&self) -> CargoResult<()> {
        match self.name {
            Some(ref name) => {
                if name.trim().is_empty() {
                    Err(human("binary target names cannot be empty.".to_string()))
                } else {
                    Ok(())
                }
            },
            None => Err(human("binary target bin.name is required".to_string()))
        }
    }
...

ターゲットセクション、つまり、[[bin]]セクションなどのpathフィールドは、二段階で決定されます。上で引用したうちの前半部分が一段目、normalize()関数の中の処理が二段目です。

Layout::from_project_path()関数の処理を思い出しつつto_real_manifest()関数の一段目の処理を読むと、次のようになっています:

  • [[bin]]セクションが存在する場合、match文のSome()に分岐する。
    • 以前にソースツリーを走査した時にsrc/main.rssrc/bin/main.rsが存在している場合、let bin = layout.main()によってこのファイルパスがbin変数に代入される。5
    • それぞれの[[bin]]セクションの内容を走査し、イテレータのmap()関数で処理する。
      • そのセクションにpathフィールドがない場合、このセクションのpathフィールドとして、先ほど見つけたsrc/main.rssrc/bin/main.rsのどちらかが使われる。どちらも見つかっていない場合にはNoneが代入される。6
      • pathフィールドがある場合、これがそのまま使われる。
    • なお、[[bin]]セクションにはnameフィールドが必須(validate_binary_name()関数を参照のこと)。
  • 一つも[[bin]]セクションが存在しなければ、inferred_bin_targets()関数がターゲットセクションを自動生成する:
    • 以前にソースツリーを走査した時にsrc/main.rsが存在していた場合、nameフィールドがプロジェクト名、pathフィールドがこのsrc/main.rsとなるような[[bin]]セクションを生成する。
    • 同じくsrc/bin/ファイル名.rsというようなパス名のソースファイルが存在していた場合、それぞれのソースファイルに対してnameフィールドが「ファイル名」、pathフィールドがこのsrc/bin/ファイル名.rsとなるような[[bin]]セクションを生成する。

一つ注意しないといけないのが、src/main.rsの扱いです。

この段階で、ほとんどのケースでは各[[bin]]セクションのpathフィールドの値が決定されていますが、まだ未決定なケースが一つだけ残っています(Noneが代入されている)。これがまさに今回知りたいケースなのですが、これは最終的にnormalize()関数の中で解決されることになります。

normalize()関数

というわけで、normalize()関数を見てみましょう:

src/cargo/util/toml.rs(抜粋)
fn normalize(lib: &Option<TomlLibTarget>,
             bins: &[TomlBinTarget],
             custom_build: Option<PathBuf>,
             examples: &[TomlExampleTarget],
             tests: &[TomlTestTarget],
             benches: &[TomlBenchTarget]) -> Vec<Target> {
  ...省略...

    fn bin_targets(dst: &mut Vec<Target>, bins: &[TomlBinTarget],
                   default: &mut FnMut(&TomlBinTarget) -> PathBuf) {
        for bin in bins.iter() {
            let path = bin.path.clone().unwrap_or_else(|| {
                PathValue::Path(default(bin))
            });
            let mut target = Target::bin_target(&bin.name(), &path.to_path());
            configure(bin, &mut target);
            dst.push(target);
        }
    }

  ...省略...

    let mut ret = Vec::new();

    if let Some(ref lib) = *lib {
        lib_target(&mut ret, lib);
        bin_targets(&mut ret, bins,
                    &mut |bin| Path::new("src").join("bin")
                                   .join(&format!("{}.rs", bin.name())));
    } else if bins.len() > 0 {
        bin_targets(&mut ret, bins,
                    &mut |bin| Path::new("src")
                                    .join(&format!("{}.rs", bin.name())));
    }

  ...省略...

最終的にはnormalize()関数の中のbin_targets()関数で処理されることになります。bin_targets()関数の第三引数として渡された関数が、対象となる[[bin]]セクションのpathフィールドがNoneだった場合にデフォルトのソースファイルパスを自動生成するためのクロージャとなっています。これを見ると、libターゲットの有無によって影響を受け7、各[[bin]]セクションのpathフィールドは

  • libターゲットが存在する場合、src/bin/<binセクションのnameフィールド値>.rs
  • libターゲットが存在しない場合、src/<binセクションのnameフィールド値>.rs

が設定されることが分かります8。else if のbins.len() > 0というチェックが冗長なのは御愛嬌。

以上で、binターゲットについて、すべてのケースを調査することができました。

それにしても、一つのフィールドの値を決定するためのコードが分散していて、どうにもスッキリとしないコードでしたね。

binターゲットについてのまとめ

ソースを読んだ結果、得られた仕様をまとめてみましょう。

  • Cargo.toml[[bin]]セクションが一つも存在しない場合
    以下の両方が適用される:
    • src/main.rs が存在する場合、次のようなフィールドを持つbinターゲットが生成される:
      name = "プロジェクト名"
      path = "src/main.rs"
    • src/bin/*.rs というファイルが存在する場合、これらのすべてに対してそれぞれ対応するbinターゲットが生成され、それぞれのフィールドは次のようになる:
      name = "ファイル名"
      path = "src/bin/ファイル名.rs"
  • Cargo.toml[[bin]]セクションが一つ以上存在する場合
    • 上述したような暗黙のターゲットは生成されない。
    • すべての[[bin]]セクションに対応するbinターゲットが生成される。
    • [[bin]]セクションには、ターゲット名を指定するためのnameフィールドが存在している必要がある。
    • pathフィールドに関しては、以下の規則により決定される:
      • pathフィールドが存在する場合、これをソースファイルとして使用する。
      • pathフィールドが存在しない場合には、以下の優先順位により使用するソースファイルが決定される。
        1. src/main.rsが存在する場合、これを使用する。
        2. src/bin/main.rsが存在する場合、これを使用する。
        3. libターゲットが存在する場合9src/bin/<binセクションのnameフィールド値>.rsを使用する。
        4. libターゲットが存在しない場合、src/<binセクションのnameフィールド値>.rsを使用する。

さて、実際にこのような仕様で動いているかどうかを確かめるために、サンプルを作成しました。これを実行してみると、次の通りです:

% ./cargo-bin-target.sh build
...省略...
% ./cargo-bin-target.sh run
run in bin-with-bin-main
  run bin1
    source: src/bin/main.rs
  run bin2
    source: src/bin/bin2.rs
  run bin3
    source: src/bin/main.rs
run in bin-with-main-and-bin-main
  run bin1
    source: src/main.rs
  run bin2
    source: src/bin/bin2.rs
  run bin3
    source: src/main.rs
run in bin-with-main-and-bin-main-and-lib
  run bin1
    source: src/main.rs
  run bin2
    source: src/bin/bin2.rs
  run bin3
    source: src/main.rs
run in bin-without-main
  run bin1
    source: src/bin1.rs
  run bin2
    source: src/bin/bin2.rs
run in bin-without-main-with-lib
  run bin1
    source: src/bin/bin1.rs
  run bin2
    source: src/bin/bin2.rs
run in no-bin
  run bin1
    source: src/bin/bin1.rs
  run bin2
    source: src/bin/bin2.rs
  run no-bin
    source: src/main.rs

……これだけ見ても何のことだかわからないかもしれませんが、各プロジェクトのCargo.tomlの記述内容と、各バイナリがどのソースから生成されたかをよく観察してみると、確かに上述したような仕様で動いていることが分かると思います。

他のセクションについてはまたの機会ということで。

cargo metadata

さて、目の前にあるcargoプロジェクトの情報を知りたい場合には、cargo metadataサブコマンドが便利です。引数を付けずに実行すると、依存関係の解決をして副作用が発生してしまうため、次のように--no-depsオプションをつけると良いでしょう:

cargo metadata --no-deps

出力はJSON形式ですが、このままだと人間には読みにくいため、次のようにjqコマンドを使うのが便利です10:

cargo metadata --no-deps | jq .

今回のようにbinターゲットについて知りたい場合には、jqコマンドのフィルタを書いて限定してもいいでしょう(個人的にはjq . | lessで目視すれば十分な気もしますが)。たとえば、cargoのソースで次のように実行してみましょう:

% cargo metadata --no-deps | jq '.packages[].targets[] | select(.kind==["bin"])'
{
  "kind": [
    "bin"
  ],
  "name": "cargo",
  "src_path": "src/bin/cargo.rs"
}

というわけで、src/bin/cargo.rsが目的のファイルです。おわり。

脚注


  1. 筆者自身は、ソースコードを読んでいる最中に疑問が生じたところは、その都度疑問を解消するべく、タグジャンプやgrepを使って追っかけて、それが何を意味するのかを探っています。このread_manifest()関数の解説に対して三行でまとめられているのも、先にto_manifest()関数の中や、read_manifest()関数を呼び出している側のコードを読んでいるからこそ、こういうスッキリした形で書けるわけです。つまり、この記事は、必ずしも筆者がコードを読んだ通りには書かれていません。まあ、コードの読み方という観点では、むしろ追っかけ方まで詳しく書く方が良いのかもしれませんが、読み物としていかんせん冗長なので、こういう形にしています。 

  2. let contents = paths::read(path)?; …… この?演算子は例の本(の第一版)には記述がありませんが、Rust 1.13で追加されたものです。この演算子はResult<T,E>型の値を受け取ることができ、値がOk(v)ならばvとして評価され、Err(e)ならば現在実行中の関数からreturn Err(e)で脱出します。いわゆるシンタックスシュガーというやつです。 

  3. 本題とは外れますが、このparse()関数は関数インターフェースも実装もなかなかひどいので、反面教師として読んでみるのもいいかもしれません。こういうkludgeは、実用的なソフトウェアではたまに必要になるものの、この書き方はあまりに場当たり的で感心しないものです。 

  4. to_real_manifest……ってことは「Real」じゃないマニフェストって何だよ、となりますが、もう一つ、「Virtual」なマニフェストが存在します。本題には関係ないのでVirtualマニフェストについては説明しませんが、EitherManifest型をReal(Manifest)Virtual(VirtualManifest)の二つのenumとすることにより、read_manifest()関数が返す値を自然に表現しています。直和型便利。 

  5. もしsrc/main.rssrc/bin/main.rsの両方が存在していた場合には、Layout::from_project_path()関数の中で先にpushされるであろうsrc/main.rsが優先されるように見えます。 

  6. このケースでは、たとえsrc/bin/<binセクションのnameフィールド値>.rsというファイルが存在している場合でも、これが使われることはありません。pathフィールドがNoneではなくなるからです。 

  7. ここで[lib]セクションではなくlibターゲットと言っているのは、Cargo.tomlに明示的な[lib]セクションが存在しなくても、src/lib.rsが存在する場合、これをソースファイルとし、名前をプロジェクト名とする暗黙のlibターゲットが生成されるからです。 

  8. なお、このケースではcargo自身がソースファイルの有無をチェックしないため、もしファイルが存在しなければ、rustcがそのまま起動され、そしてエラーを吐くことになります。 

  9. 暗黙のlibターゲットも含まれるので注意。 

  10. Ubuntuの場合、jqコマンドはデフォルトではインストールされていないため、apt-get install jqとでもしてインストールしてください。 

16
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
11