Rustのcargo runとcargo buildの使い分けを完全解説!初心者でもわかるプロジェクト管理とビルド
生徒
「Rustでプログラムを動かすとき、cargo runとcargo buildのどちらを使えばいいんですか?」
先生
「どちらもRustプロジェクトをビルドするコマンドですが、使い分けがあります。cargo runはビルド後に自動で実行までしてくれて、cargo buildはビルドのみを行います。」
生徒
「それぞれどんな場面で使うのがベストなんでしょうか?」
先生
「開発中はcargo runが便利ですが、本番環境用のバイナリを作るときや、実行タイミングを制御したいときはcargo buildを使います。詳しく見ていきましょう!」
1. cargo runとcargo buildの基本的な違い
RustのパッケージマネージャーであるCargoは、プロジェクト管理やビルドシステムとして非常に強力なツールです。その中でもcargo runとcargo buildは、開発時に最もよく使うコマンドです。
cargo buildは、Rustのソースコードをコンパイルして実行可能なバイナリファイルを生成するコマンドです。ビルドが完了すると、target/debugディレクトリ(デバッグビルドの場合)またはtarget/releaseディレクトリ(リリースビルドの場合)に実行ファイルが作成されます。ビルド後に自動実行はされないため、プログラムを動かすには別途実行ファイルを起動する必要があります。
一方、cargo runは、内部的にcargo buildを実行した後、生成されたバイナリを自動的に実行してくれるコマンドです。つまり、ビルドと実行を一度に行えるため、開発中のテストや動作確認に非常に便利です。コードを修正するたびにcargo runを実行すれば、すぐに結果を確認できます。
2. cargo runの使い方と開発時の利便性
cargo runは開発サイクルを高速化するための最適なコマンドです。プログラムを書いて動作を確認する際、ビルドと実行を別々に行う手間を省けます。
基本的な使い方は非常にシンプルです。プロジェクトのルートディレクトリで以下のコマンドを実行するだけです。
cargo run
このコマンドを実行すると、Cargoは自動的にソースコードの変更を検知し、必要な場合のみ再ビルドを行います。変更がない場合は、既存のバイナリをそのまま実行するため、無駄なコンパイル時間を削減できます。
実際に簡単なRustプログラムで試してみましょう。
fn main() {
println!("cargo runでプログラムを実行しています!");
let version = "1.0.0";
println!("バージョン: {}", version);
}
このコードをsrc/main.rsに保存してcargo runを実行すると、以下のような出力が得られます。
Compiling rust_project v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 0.50s
Running `target/debug/rust_project`
cargo runでプログラムを実行しています!
バージョン: 1.0.0
このように、cargo runはコンパイル情報と実行結果の両方を表示してくれます。開発中は何度もコードを修正して動作確認を繰り返すため、このワンステップで完結する利便性が非常に高いのです。
3. cargo buildの使い方とビルド成果物の管理
cargo buildは、実行ファイルを生成するだけでプログラムを起動しません。ビルド結果を確認したり、生成されたバイナリを別の場所にコピーしたり、デプロイ準備をしたりする場合に使用します。
デバッグビルドを行う場合は以下のコマンドを実行します。
cargo build
これにより、target/debugディレクトリに実行ファイルが生成されます。デバッグビルドは最適化が行われないため、コンパイル時間は短いですが、実行速度は遅くなります。
本番環境向けの最適化されたビルドを行う場合は、--releaseフラグを付けます。
cargo build --release
リリースビルドでは、target/releaseディレクトリに実行ファイルが生成されます。このバイナリは最適化が施されており、実行速度が大幅に向上しますが、コンパイル時間は長くなります。
生成されたバイナリは、以下のように直接実行できます。
./target/debug/rust_project
./target/release/rust_project
このように、cargo buildを使うことで、実行タイミングを自分で制御できます。特に、ビルド後にファイルサイズを確認したり、バイナリを他のサーバーに転送したりする場合には、cargo buildを使う方が適しています。
4. 開発フェーズでの使い分けパターン
実際の開発現場では、開発フェーズに応じてcargo runとcargo buildを使い分けることが重要です。それぞれの特性を理解して、効率的な開発を行いましょう。
開発初期段階やプロトタイピング時は、cargo runを頻繁に使用します。コードを少し書いては動作確認、という繰り返しが多いため、ビルドと実行がワンステップで完了するcargo runが最適です。エラーメッセージもすぐに確認でき、修正サイクルを高速化できます。
機能開発やデバッグ時もcargo runが主役です。特定の機能を実装して動作を確認する際、毎回ビルドと実行を別々に行うのは非効率です。cargo runなら、コマンド一つで最新のコードが実行されます。
パフォーマンステストや本番環境準備時は、cargo build --releaseを使用します。リリースビルドは最適化が施されているため、実際のパフォーマンスを測定する際には必須です。デバッグビルドとリリースビルドでは、実行速度が数倍から数十倍も異なることがあります。
CI/CDパイプラインやデプロイメント時もcargo buildが適しています。ビルド成果物を明示的に管理し、テストや静的解析を行った後に、そのバイナリをデプロイするという流れが一般的です。
5. コマンドライン引数の渡し方の違い
プログラムにコマンドライン引数を渡す場合、cargo runとcargo buildでは方法が異なります。この違いを理解しておくと、デバッグや動作確認がスムーズになります。
cargo runで引数を渡す場合は、--の後に引数を記述します。
cargo run -- 引数1 引数2 引数3
--は、Cargoへのオプションとプログラムへの引数を区別するための区切り文字です。実際のコード例で見てみましょう。
fn main() {
let args: Vec<String> = std::env::args().collect();
println!("受け取った引数の数: {}", args.len());
for (i, arg) in args.iter().enumerate() {
println!("引数[{}]: {}", i, arg);
}
}
このプログラムをcargo run -- hello world rustで実行すると、以下のような出力になります。
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/rust_project hello world rust`
受け取った引数の数: 4
引数[0]: target/debug/rust_project
引数[1]: hello
引数[2]: world
引数[3]: rust
一方、cargo buildで生成したバイナリを実行する場合は、直接引数を渡すことができます。
./target/debug/rust_project hello world rust
この場合、Cargoを経由しないため、--は不要です。バイナリに直接引数が渡されます。
6. ビルドキャッシュと再コンパイルの仕組み
Cargoは非常に賢いビルドシステムを持っており、変更されたファイルのみを再コンパイルする仕組みが組み込まれています。これはcargo runでもcargo buildでも同様に機能します。
初回ビルド時は、すべての依存クレートとプロジェクトコードがコンパイルされます。しかし、二回目以降は変更されたファイルのみが再コンパイルされるため、ビルド時間が大幅に短縮されます。
例えば、大規模なプロジェクトで一つの関数だけを修正した場合、その関数が含まれるモジュールのみが再コンパイルされます。依存関係のないモジュールはキャッシュから読み込まれるため、数秒でビルドが完了します。
この仕組みは、targetディレクトリ内に保存されたビルドキャッシュによって実現されています。もしビルドに問題が発生した場合は、以下のコマンドでクリーンビルドを実行できます。
cargo clean
cargo build
cargo cleanはtargetディレクトリを削除し、次回のビルドですべてを最初から再コンパイルします。通常は不要ですが、依存関係の不整合やビルドエラーが解消されない場合に有効です。
7. デバッグビルドとリリースビルドの最適化レベル
Rustのビルドには、デバッグビルドとリリースビルドの二つのモードがあります。この違いを理解することは、適切なビルド方法を選択する上で非常に重要です。
デフォルトのcargo buildやcargo runは、デバッグビルドを生成します。デバッグビルドは最適化が無効化されており、コンパイル時間が短い代わりに実行速度は遅くなります。また、デバッグ情報が含まれているため、GDBやLLDBなどのデバッガを使った詳細なデバッグが可能です。
リリースビルドは、--releaseフラグを付けることで生成されます。このモードでは、コンパイラがコードの最適化を積極的に行い、実行速度が大幅に向上します。ループ展開、インライン展開、不要なコードの削除などが行われ、パフォーマンスが最大化されます。
実際のパフォーマンス差を測定する簡単なプログラムを見てみましょう。
fn main() {
let start = std::time::Instant::now();
let mut sum: u64 = 0;
for i in 0..100_000_000 {
sum += i;
}
let duration = start.elapsed();
println!("合計: {}", sum);
println!("処理時間: {:?}", duration);
}
このプログラムをデバッグビルドとリリースビルドで実行すると、処理時間に大きな差が出ます。デバッグビルドでは数百ミリ秒かかる処理が、リリースビルドでは数ミリ秒で完了することもあります。
開発中はデバッグビルドで素早く動作確認を行い、最終的なパフォーマンステストや本番環境へのデプロイ時にはリリースビルドを使用するのが一般的なワークフローです。
8. ビルドオプションとCargo.tomlでのカスタマイズ
Cargoでは、Cargo.tomlファイルでビルドプロファイルをカスタマイズできます。デバッグビルドとリリースビルドの設定を細かく調整することで、開発効率とパフォーマンスのバランスを最適化できます。
Cargo.tomlに以下のような設定を追加することで、ビルドプロファイルを調整できます。
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
opt-levelは最適化レベルを指定するオプションで、0から3までの値を設定できます。0は最適化なし、3は最大限の最適化を意味します。デバッグビルドでは通常0、リリースビルドでは3が使用されます。
ltoはリンク時最適化(Link Time Optimization)を有効にするオプションです。この設定をtrueにすると、リンク時にさらなる最適化が行われ、実行ファイルのサイズが小さくなり、パフォーマンスも向上します。ただし、ビルド時間は長くなります。
codegen-unitsは、コンパイル時の並列化単位を指定します。1に設定すると、すべてのコードが一つの単位でコンパイルされるため、最適化の効果が最大化されますが、ビルド時間は長くなります。並列ビルドを活用する場合は、この値を大きくすることでコンパイル時間を短縮できます。
これらの設定を調整することで、プロジェクトの特性に合わせたビルドプロセスを構築できます。小規模なプロジェクトでは初期設定で十分ですが、大規模なプロジェクトやパフォーマンスが重要なアプリケーションでは、細かな調整が効果を発揮します。
9. 実践的な使い分けのシナリオと tips
実際の開発現場では、状況に応じてcargo runとcargo buildを使い分けることで、開発効率が大きく向上します。ここでは、具体的なシナリオごとの使い分け方を紹介します。
シナリオ1: 新機能の開発とテスト
新しい機能を実装する際は、cargo runを使って頻繁に動作確認を行います。コードを少し書いては実行し、期待通りの動作をするか確認します。この段階では、ビルド速度が重要なので、デバッグビルドで十分です。
シナリオ2: ベンチマークやパフォーマンス測定
パフォーマンスを測定する際は、必ずcargo build --releaseでリリースビルドを作成します。デバッグビルドでは最適化が行われていないため、正確なパフォーマンス測定ができません。リリースビルドで生成されたバイナリを直接実行し、処理時間やメモリ使用量を計測します。
シナリオ3: クロスプラットフォームビルド
異なるプラットフォーム向けにビルドする場合は、cargo build --targetオプションを使用します。例えば、Linux環境でWindows向けのバイナリを作成する場合などです。この場合、ビルド後にバイナリを別の環境に転送するため、cargo buildを使用します。
シナリオ4: 自動テストとの組み合わせ
テスト駆動開発を行う場合、cargo testでテストを実行した後、cargo runで実際の動作を確認するという流れが一般的です。テストが通ったら、実際にプログラムを動かして、ユーザー視点での動作確認を行います。
開発のワークフローに合わせて、これらのコマンドを適切に使い分けることで、効率的かつ高品質なRust開発が実現できます。Cargoの強力なビルドシステムを最大限に活用しましょう。
10. トラブルシューティングとよくある質問
Cargoを使った開発では、時々ビルドエラーやパフォーマンスの問題に遭遇することがあります。ここでは、よくある問題とその解決方法を紹介します。
ビルドが異常に遅い場合
初回ビルドは依存クレートのコンパイルが必要なため時間がかかりますが、二回目以降も遅い場合は、Cargo.tomlの依存関係を見直しましょう。不要な依存クレートを削除することで、ビルド時間を短縮できます。また、cargo build --releaseは最適化に時間がかかるため、開発中はcargo run(デバッグビルド)を使用しましょう。
cargo runで古いバージョンが実行される場合
稀に、コードを変更したのに古いバージョンが実行されることがあります。この場合、cargo cleanを実行してビルドキャッシュをクリアし、再度cargo runを実行してみましょう。通常はCargoが自動的に変更を検知しますが、ファイルシステムの問題などでキャッシュが正しく更新されないことがあります。
実行ファイルのサイズが大きい場合
デバッグビルドではデバッグ情報が含まれるため、実行ファイルのサイズが大きくなります。本番環境用には必ずcargo build --releaseを使用し、さらにstripコマンドでデバッグシンボルを削除することで、ファイルサイズを削減できます。
依存クレートのビルドエラー
依存クレートのバージョン不整合やビルドエラーが発生した場合、cargo updateで依存関係を更新するか、Cargo.lockを削除して再ビルドを試みましょう。また、Rustのバージョンが古い場合、最新の安定版にアップデートすることで解決することもあります。
これらのトラブルシューティング手法を知っておくことで、開発中の問題を素早く解決し、スムーズな開発フローを維持できます。Cargoは非常に強力なツールですが、その仕組みを理解することで、より効果的に活用できるようになります。