The terminal is your play button
You have written fn main() { println!("Hello"); }. You saved the file. You are staring at a folder containing Cargo.toml, a src/ directory, and a mysterious target/ folder that appeared out of nowhere. You want the program to run. In Python you type python script.py. In JavaScript you type node script.js. In Rust, you do not run the file. You run the project.
The command line is your interface, and cargo is the tool that bridges your code to the operating system. Understanding this bridge saves you from confusion when builds fail, when binaries vanish, or when performance numbers don't match your expectations. Rust compiles to native machine code. The workflow is different from interpreted languages, but the result is a standalone binary that runs anywhere without a runtime installed.
Cargo manages the build graph
Rust ships with a tool called cargo. It is the build system, the dependency manager, and the test runner. You rarely talk to the underlying compiler rustc directly. Cargo invokes rustc with the correct flags, resolves dependencies, and links everything into a single executable.
A standard Rust project follows a strict layout. Cargo expects this structure to function.
my_project/
├── Cargo.toml # Manifest: name, version, dependencies
├── Cargo.lock # Lockfile: exact dependency versions (auto-generated)
├── src/
│ └── main.rs # Entry point for binaries
└── target/ # Build artifacts: binaries, object files, metadata
The Cargo.toml file is the manifest. It declares the package name, the Rust edition, and the external crates the project requires. The src/main.rs file contains the main function, which is the entry point for executable binaries. Library crates use src/lib.rs, but libraries are not run directly from the command line.
The target/ directory holds all build output. It contains intermediate object files, dependency metadata, and the final binary. This directory can grow large. It is safe to delete at any time, though doing so forces a full rebuild. The community convention is to add target/ to .gitignore so build artifacts never enter version control.
The build lifecycle
When you run a Rust program, cargo performs a sequence of steps. It reads the manifest, resolves the dependency graph, compiles each crate, and links them together.
Here is the minimal workflow to create and run a project.
# Scaffold a new binary project. Creates the directory structure and a basic main.rs.
cargo new hello_world
cd hello_world
# Build and run the project in one command.
cargo run
The output shows cargo compiling the crate, finishing the build, and running the binary.
Compiling hello_world v0.1.0 (/path/to/hello_world)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello_world`
Hello, world!
The first run takes time because cargo must download the standard library and compile your code. Subsequent runs are fast. Cargo uses incremental compilation. It tracks which files changed and only recompiles the affected modules. If you edit main.rs and run again, the compilation step might take milliseconds.
Debug versus release builds
Cargo builds in two primary modes. The default mode is debug. The alternative is release. The choice affects compilation speed, binary size, and runtime performance.
Debug mode prioritizes fast compilation and rich debugging information. It disables most compiler optimizations. The resulting binary runs slower and occupies more disk space. Debug mode is the default for cargo run and cargo build. It is the correct choice during development because you want quick feedback when you change code.
Release mode prioritizes runtime performance. It enables aggressive optimizations like inlining, loop unrolling, and dead code elimination. The compiler spends more time analyzing and transforming the code. The resulting binary runs significantly faster and is often smaller. Release mode is required for production binaries and benchmarks.
# Build an optimized release binary.
# Output goes to target/release/ instead of target/debug/.
cargo build --release
# Run the release binary.
./target/release/hello_world
The performance difference can be drastic. A debug binary might take seconds to complete a computation that a release binary finishes in milliseconds. Always benchmark with --release. Debug mode numbers are misleading.
Running with arguments
Programs often need input from the command line. Cargo passes arguments to your binary, but you must separate cargo's flags from your program's flags.
The double dash -- is the separator. Everything before -- is for cargo. Everything after -- is for your program.
// src/main.rs
use std::env;
fn main() {
// env::args() returns an iterator over command-line arguments.
// The first argument is always the program name.
let args: Vec<String> = env::args().collect();
// Check if a second argument exists.
if args.len() > 1 {
println!("Hello, {}!", args[1]);
} else {
println!("Hello, world!");
}
}
Run the program with arguments using the separator.
# Pass "Alice" to the program.
# The -- tells cargo to stop parsing and pass the rest to the binary.
cargo run -- Alice
# Output:
# Hello, Alice!
Without the separator, cargo might interpret Alice as a flag or fail to parse the command. The separator is a convention that prevents ambiguity. Use it whenever your program accepts arguments.
Behind the scenes: rustc
Cargo is a wrapper around rustc, the Rust compiler. You can invoke rustc directly to compile a single file without a project structure. This is useful for learning or for tiny scripts that have no dependencies.
# Compile a single file to an executable named hello.
rustc src/main.rs -o hello
# Run the binary.
./hello
Direct rustc usage skips dependency management, build caching, and project scaffolding. You lose the benefits of the ecosystem. In real projects, always use cargo. The compiler flags cargo passes are complex and version-specific. Cargo handles the details so you can focus on code.
Common pitfalls
New Rust developers hit a few recurring issues when running programs. Recognizing these patterns saves time.
Command not found: cargo. This error means Rust is not installed or the shell cannot find the cargo executable. Install Rust via rustup and ensure the cargo environment is loaded. On Linux and macOS, you may need to source the environment file or restart the terminal.
# Source the cargo environment if cargo is not found.
source $HOME/.cargo/env
No manifest found. Cargo looks for Cargo.toml in the current directory and walks up the directory tree. If you run cargo run from the wrong folder, cargo fails with an error about a missing manifest. Change into the project directory first.
Stale binaries. If you rename the package in Cargo.toml, the binary name changes. The old binary remains in target/debug/ until you rebuild. Always use cargo run to execute the current binary. It resolves the correct name automatically.
Missing system libraries. Some crates depend on system libraries like OpenSSL or libpq. If a dependency fails to build, the error message usually names the missing library. Install the system package and retry the build. This is common when compiling crates that wrap C libraries.
Type errors. The compiler rejects code with type mismatches or undefined names. Error codes help identify the issue.
error[E0425]: cannot find function `println` in this scope
--> src/main.rs:3:5
|
3 | println!("Hello");
| ^^^^^^^ not found in this scope
This error indicates a typo or a missing import. Check the function name and ensure the correct crate is in scope. The compiler points to the exact line and column. Trust the error message. It is usually precise.
Workflow tools
Cargo provides subcommands that streamline development. You will use these daily.
cargo check typechecks the code without producing a binary. It skips the linking step, which is often the slowest part of the build. Run cargo check frequently while editing to catch errors quickly. It is faster than cargo build and provides the same type safety guarantees.
cargo clean removes the target/ directory. Use this when builds behave strangely or when you want to force a full rebuild. It is a nuclear option. Incremental compilation usually handles changes correctly, but a clean build resolves stale artifact issues.
cargo doc --open generates HTML documentation for your project and its dependencies. It opens the docs in a browser. This is useful for exploring the standard library and third-party crates. The docs include examples and type signatures.
Decision matrix
Choose the right command for your goal. The ecosystem provides tools for different stages of development.
Use cargo run when you are iterating on code and need immediate feedback. It builds and executes in one step, skipping compilation if nothing changed. This is the default command for development.
Use cargo build --release when you are ready to ship a binary or measure performance. It produces an optimized executable in target/release/. Use this for production deployments and benchmarks.
Use cargo check when you want to verify types without the overhead of linking. It is faster than building and integrates well with editor tooling. Run this constantly while writing code.
Use rustc when you are writing a single-file script with no dependencies or learning raw compiler flags. It bypasses cargo and compiles directly to a binary. Avoid this for multi-file projects.