三行で頼む
-
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 GuideやThe 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]]]
セクションが一つだけ存在します:
[[bin]]
name = "cargo"
test = false
doc = false
そのため、cargoのプロジェクトでは、src/bin/
ディレクトリの中にある大量のソースファイル一つ一つに対応する実行可能ファイルが生成されたりはせず、一つの実行可能ファイルcargo
だけが生成されることが分かります。なお、これ以外にも次のようなセクションが存在します:
[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/cargo
やsrc/bin
の中にあるその他の*.rs
ファイルはどのようにして利用されるのかというと、これは例の本のcrateとモジュールの章に書いてある通りです。つまり、Rust言語自体のモジュールの仕組みによって参照されることとなります。
たとえば、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()
という関数です:
/// 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.rs
のread_manifest()
関数だけがヒットしました。
read_manifest()
関数
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()
関数を読んでみましょう:
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]]
セクションの処理に相当する部分を抜粋します:
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()
関数も引用しておきます:
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.rs
かsrc/bin/main.rs
が存在している場合、let bin = layout.main()
によってこのファイルパスがbin変数に代入される。5 - それぞれの
[[bin]]
セクションの内容を走査し、イテレータのmap()
関数で処理する。- そのセクションに
path
フィールドがない場合、このセクションのpath
フィールドとして、先ほど見つけたsrc/main.rs
かsrc/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()
関数を見てみましょう:
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
フィールドが存在しない場合には、以下の優先順位により使用するソースファイルが決定される。-
src/main.rs
が存在する場合、これを使用する。 -
src/bin/main.rs
が存在する場合、これを使用する。 - libターゲットが存在する場合9、
src/bin/<binセクションのnameフィールド値>.rs
を使用する。 - 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
が目的のファイルです。おわり。
脚注
-
筆者自身は、ソースコードを読んでいる最中に疑問が生じたところは、その都度疑問を解消するべく、タグジャンプやgrepを使って追っかけて、それが何を意味するのかを探っています。この
read_manifest()
関数の解説に対して三行でまとめられているのも、先にto_manifest()
関数の中や、read_manifest()
関数を呼び出している側のコードを読んでいるからこそ、こういうスッキリした形で書けるわけです。つまり、この記事は、必ずしも筆者がコードを読んだ通りには書かれていません。まあ、コードの読み方という観点では、むしろ追っかけ方まで詳しく書く方が良いのかもしれませんが、読み物としていかんせん冗長なので、こういう形にしています。 ↩ -
let contents = paths::read(path)?;
…… この?
演算子は例の本(の第一版)には記述がありませんが、Rust 1.13で追加されたものです。この演算子はResult<T,E>
型の値を受け取ることができ、値がOk(v)
ならばv
として評価され、Err(e)
ならば現在実行中の関数からreturn Err(e)
で脱出します。いわゆるシンタックスシュガーというやつです。 ↩ -
本題とは外れますが、この
parse()
関数は関数インターフェースも実装もなかなかひどいので、反面教師として読んでみるのもいいかもしれません。こういうkludgeは、実用的なソフトウェアではたまに必要になるものの、この書き方はあまりに場当たり的で感心しないものです。 ↩ -
to_real_manifest……ってことは「Real」じゃないマニフェストって何だよ、となりますが、もう一つ、「Virtual」なマニフェストが存在します。本題には関係ないのでVirtualマニフェストについては説明しませんが、
EitherManifest
型をReal(Manifest)
とVirtual(VirtualManifest)
の二つのenumとすることにより、read_manifest()
関数が返す値を自然に表現しています。直和型便利。 ↩ -
もし
src/main.rs
とsrc/bin/main.rs
の両方が存在していた場合には、Layout::from_project_path()
関数の中で先にpushされるであろうsrc/main.rs
が優先されるように見えます。 ↩ -
このケースでは、たとえ
src/bin/<binセクションのnameフィールド値>.rs
というファイルが存在している場合でも、これが使われることはありません。path
フィールドがNoneではなくなるからです。 ↩ -
ここで
[lib]
セクションではなくlibターゲットと言っているのは、Cargo.toml
に明示的な[lib]
セクションが存在しなくても、src/lib.rs
が存在する場合、これをソースファイルとし、名前をプロジェクト名とする暗黙のlibターゲットが生成されるからです。 ↩ -
なお、このケースではcargo自身がソースファイルの有無をチェックしないため、もしファイルが存在しなければ、rustcがそのまま起動され、そしてエラーを吐くことになります。 ↩
-
暗黙のlibターゲットも含まれるので注意。 ↩
-
Ubuntuの場合、
jq
コマンドはデフォルトではインストールされていないため、apt-get install jq
とでもしてインストールしてください。 ↩