Skip to content
Learn Rust
Theme

Your first project with Cargo

Let’s get Rust installed, set up your editor, and make, build and run a real project. Cargo, Rust’s all-in-one tool, does almost all of it.

RememberIf you’re wondering how to do something to a Rust project, the answer usually starts with cargo.

Rust is installed with rustup, which also installs Cargo and lets you update everything with one command. On macOS or Linux, run this in a terminal and accept the defaults:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

On Windows, download and run rustup-init.exe from rustup.rs, which adds “You may also need the Visual Studio prerequisites”: rustup may ask you to install the Visual Studio C++ build tools first, and Rust programs need them to link. (On Linux and macOS a C linker, usually from gcc or Xcode’s command line tools, plays the same part.) Then open a new terminal and check it worked. The runs on this page use the versions below:

terminal
$ rustc --version
rustc 1.99.1 (58cf8f9c3 2026-10-13)
$ cargo --version
cargo 1.99.1 (5f94df478 2026-08-27)
$ cargo fmt --version
rustfmt 1.10.0-stable (58cf8f9c35 2026-10-13)
$ cargo clippy --version
clippy 0.1.99 (58cf8f9c35 2026-10-13)

Later, rustup update moves you to the newest Rust. A new stable version comes out every six weeks, and the Book says “Each upgrade should be painless” (the Book: Stability Without Stagnation). rustup’s default profile also installs cargo fmt and cargo clippy for section 5 (rustup book: Profiles).

In most editors the thing that matters is rust-analyzer, a language server: the program that gives an editor go to definition, completion, refactorings, and errors from rustc (and clippy, if you ask for it, below) (its manual). It also shows inlay hints (variable types as grey text) and can add a missing use line. Each editor gets it a little differently:

Editor What to do Docs
VS Code the rust-analyzer extension, which bundles the server. Uninstall the old, unmaintained “Rust” extension rust-analyzer: VS Code, VS Code: Rust
JetBrains RustRover, JetBrains’ own Rust IDE (separate from rust-analyzer’s docs); it detects your toolchain or offers to download rustup RustRover quick start
Zed Rust support is built in. It uses a rust-analyzer found on your PATH, or else installs its own Zed: Rust
Helix rust-analyzer is its default Rust language server; its docs say to set up the server first Helix: language support
Neovim built-in LSP client plus nvim-lspconfig; the manual’s one line is vim.lsp.enable('rust_analyzer') rust-analyzer: Vim/Neovim

Editors that use rust-analyzer, other than VS Code and Zed (which can fetch their own), need the binary on your machine; rustup can install it with rustup component add rust-analyzer (the Profiles page above says IDE tools are added manually with rustup component add), and rust-analyzer’s own Helix notes say “it won’t install rust-analyzer automatically”. No editor was run for this page: each row follows the linked official page.

Two settings are worth finding:

  • Format on save. VS Code: turn on Editor: Format On Save; the Rust formatter is rustfmt, the same one cargo fmt runs. RustRover: Settings | Rust | Rustfmt, tick Use Rustfmt instead of the built-in formatter, then Configure actions on save and tick Reformat code (RustRover: Rustfmt).
  • Clippy hints inline. rust-analyzer runs cargo check on save by default (checkOnSave and check.command, its configuration page). In VS Code settings.json, "rust-analyzer.check.command": "clippy" makes it run clippy instead (rust-analyzer: Diagnostics).
terminal
$ cargo new hello
    Creating binary (application) `hello` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
$ ls -A hello
.git  .gitignore  Cargo.toml  src

Here’s what you get. The .git/ folder is there because cargo new starts a git repository unless you are already inside one (cargo new):

Project layout
hello/
├── .git/                       a fresh git repository
├── src/
│   └── main.rs                 your program starts here
├── .gitignore                  already ignores target/
└── Cargo.toml                  your project's settings and dependencies

Cargo.toml describes your project and its dependencies, like package.json, go.mod or pyproject.toml:

RustCargo.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]

And src/main.rs is your program. cargo new makes a binary project, and main in this file is where it starts. The ! in println! marks a macro (Macros, attributes and comments):

Rustsrc/main.rs
fn main() {
println!("Hello, world!");
}

These are the commands you’ll type all day. Here’s real output from each one (build times are shown as [time]s because they differ on every machine):

terminal
$ cargo run
   Compiling hello v0.1.0 (/home/you/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in [time]s
     Running `target/debug/hello`
Hello, world!
$ cargo check
    Checking hello v0.1.0 (/home/you/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in [time]s
$ cargo build --release
   Compiling hello v0.1.0 (/home/you/hello)
    Finished `release` profile [optimized] target(s) in [time]s
$ ./target/release/hello
Hello, world!
  • cargo run builds a debug version and runs it.
  • cargo check only checks your code for errors, without producing a program. The Book says it is often much faster than a build, and it’s what rust-analyzer runs when you save (section 2).
  • cargo build --release makes an optimised program in target/release/. That’s the one you ship or benchmark.

After the first build, two new things appear:

After cargo build
hello/
├── .git/
├── src/
│   └── main.rs
├── target/                     new: everything Cargo builds
│   └── debug/
│       └── hello               your program (plus build caches)
├── .gitignore
├── Cargo.lock                  new: exact versions, commit it
└── Cargo.toml
  • target/ holds everything Cargo builds, including your program at target/debug/hello. It can get big. It’s already in .gitignore, and Cargo rebuilds what it needs if you delete it (cargo clean does that, docs).
  • Cargo.lock records the exact version of every library you use. For a program like this one, commit it, so everyone builds the same thing (Cargo.toml vs Cargo.lock).

cargo fmt formats your code in the community’s standard style, so style is one less thing to argue about in code review. Here is a badly laid-out file. --check lists what would change and exits with an error; plain cargo fmt rewrites the file:

Rustsrc/main.rs
use std::collections::HashMap;
fn main(){
let mut stock=HashMap::new();
stock.insert("apple",3);stock.insert("pear",5);
let total:i32=stock.values().sum();
if total>7{println!("{total} items, no restock")}else{
println!("restock");
}
}
terminal
$ cargo fmt --check
Diff in /home/you/hello/src/main.rs:1:
 use std::collections::HashMap;
-fn main(){
-let mut stock=HashMap::new();
-    stock.insert("apple",3);stock.insert("pear",5);
-  let total:i32=stock.values().sum();
-    if total>7{println!("{total} items, no restock")}else{
-println!("restock");
+fn main() {
+    let mut stock = HashMap::new();
+    stock.insert("apple", 3);
+    stock.insert("pear", 5);
+    let total: i32 = stock.values().sum();
+    if total > 7 {
+        println!("{total} items, no restock")
+    } else {
+        println!("restock");
     }
 }
 
$ cargo fmt
$ cat src/main.rs
use std::collections::HashMap;
fn main() {
    let mut stock = HashMap::new();
    stock.insert("apple", 3);
    stock.insert("pear", 5);
    let total: i32 = stock.values().sum();
    if total > 7 {
        println!("{total} items, no restock")
    } else {
        println!("restock");
    }
}
$ cargo fmt --check

cargo clippy is a linter: its README says it has over 800 lints. It points out code that works but could be simpler or clearer, and tells you how. This file compiles, and clippy still has two remarks:

Rustsrc/main.rs
fn main() {
let names: Vec<String> = std::env::args().skip(1).collect();
let done = names.len() > 2;
if done == true {
println!("done");
}
if names.len() == 0 {
println!("no names");
}
}
terminal
$ cargo clippy
    Checking hello v0.1.0 (/home/you/hello)
warning: equality checks against true are unnecessary
 --> src/main.rs:4:8
  |
4 |     if done == true {
  |        ^^^^^^^^^^^^ help: try: `done`
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#bool_comparison
  = note: `#[warn(clippy::bool_comparison)]` on by default

warning: length comparison to zero
 --> src/main.rs:7:8
  |
7 |     if names.len() == 0 {
  |        ^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `names.is_empty()`
  |
  = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#len_zero
  = note: `#[warn(clippy::len_zero)]` on by default

warning: `hello` (bin "hello") generated 2 warnings (run `cargo clippy --fix --bin "hello" -p hello -- ` to apply 2 suggestions)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in [time]s

Run both before you commit, or let your editor do it (section 2). Macros, attributes and comments runs both on snippets.

  1. Greet someone by name. Replace src/main.rs with this, then run cargo run and cargo run -- Ferris. || "world".to_string() is a small function that only runs if there is no name. Everything after -- goes to your program, and nth(1) skips the first item, which the docs say is traditionally the program’s own path. With no name it prints Hello, world!; with one it prints Hello, Ferris!:

    Rustsrc/main.rs
    fn main() {
    let name = std::env::args().nth(1).unwrap_or_else(|| "world".to_string());
    println!("Hello, {name}!");
    }
    terminal
    $ cargo run
    Hello, world!
    $ cargo run -- Ferris
    Hello, Ferris!
    
  2. Break it on purpose. Change the first line of main to let count: i32 = "three"; (leave the println! under it) and run cargo check. The compiler stops with E0308, points at the string, and says it expected i32. Nothing ran:

    $ cargo check
    error[E0308]: mismatched types
     --> src/main.rs:2:22
      |
    2 |     let count: i32 = "three";
      |                ---   ^^^^^^^ expected `i32`, found `&str`
      |                |
      |                expected due to this
  3. Let clippy fix its own remarks. Paste the clumsy file from section 5 into src/main.rs of a fresh project and run cargo clippy --fix. Cargo refuses, because a project made by cargo new has uncommitted files and a fix edits them. Commit first, or add --allow-dirty. Clippy reports “Fixed src/main.rs (2 fixes)”, and the file now says if done and names.is_empty():

    terminal
    $ cargo clippy --fix
    error: the working directory of this package has uncommitted changes, and `cargo fix` can potentially perform destructive changes; if you'd like to suppress this error pass `--allow-dirty`, or commit the changes to these files:
    
      * .gitignore (dirty)
      * Cargo.toml (dirty)
      * src/ (dirty)
    $ cargo clippy --fix --allow-dirty
        Checking hello v0.1.0 (/home/you/hello)
           Fixed src/main.rs (2 fixes)
        Finished `dev` profile [unoptimized + debuginfo] target(s) in [time]s
    $ cat src/main.rs
    fn main() {
        let names: Vec<String> = std::env::args().skip(1).collect();
        let done = names.len() > 2;
        if done {
            println!("done");
        }
        if names.is_empty() {
            println!("no names");
        }
    }
    
Language New project Run Check only Release build Test Format Lint
Rust cargo new cargo run cargo check cargo build --release cargo test cargo fmt cargo clippy
Go go mod init (you write main.go) go run . go vet ./... go build (one default mode) go test ./... gofmt go vet, staticcheck
Python uv init python3 main.py python3 -m py_compile (syntax only); mypy for types nothing to compile python3 -m unittest, pytest ruff format ruff check
TypeScript npm init node main.ts (Node 22.18 or newer) tsc --noEmit tsc node --test, vitest prettier eslint
Java gradle init java Main.java, gradle run javac (compiling is the check) gradle build gradle test google-java-format, spotless checkstyle, Error Prone
Kotlin gradle init gradle run kotlinc (compiling is the check) gradle build gradle test ktlint detekt
C# dotnet new console dotnet run dotnet build dotnet publish -c Release dotnet test dotnet format analysers, which run in dotnet build
C write a Makefile or CMakeLists.txt build, then run the binary gcc -fsyntax-only gcc -O2 ctest clang-format clang-tidy
C++ write a Makefile or CMakeLists.txt build, then run the binary g++ -fsyntax-only g++ -O2 ctest clang-format clang-tidy

Many cells are separate tools, not part of the language’s own toolchain (uv, mypy, pytest, ruff, vitest, prettier, eslint, staticcheck, Gradle, ktlint, detekt, clang-*). The image has Go, Node, Java, Kotlin, Python, .NET and GCC but not those, so their cells are the usual names, not runs.

The runs behind the table
terminal
$ go mod init example.com/hello   # Go
go: creating new go.mod: module example.com/hello
go: to add module requirements and sums:
	go mod tidy
$ go run .   # Go
Hello, world!
$ gofmt -l .   # Go: lists files whose layout differs (none)
$ go vet ./...   # Go: type-checks and runs analysers
$ go test ./...   # Go
?   	example.com/hello	[no test files]
$ node main.ts   # TypeScript, run directly by Node
Hello, world!
$ tsc --noEmit main.ts   # TypeScript: checks, writes no files
$ java Main.java   # Java: compiles and runs one file
Hello, world!
$ javac Main.java   # Java: compiling is the check
$ kotlinc Main.kt   # Kotlin: compiling is the check
$ python3 -m py_compile main.py   # Python: syntax only
$ gcc -fsyntax-only main.c   # C: checks, writes no program
$ g++ -fsyntax-only main.cpp   # C++: checks, writes no program
$ dotnet new console -o app   # C#
The template "Console App" was created successfully.
$ dotnet run --project app   # C#
Hello, World!
$ dotnet format app --verify-no-changes   # C#

Most of the checks (go vet, gofmt -l, tsc --noEmit, javac, kotlinc, py_compile, the two -fsyntax-only runs and dotnet format) print nothing when they find nothing wrong. Not run: Gradle, uv, ruff, mypy, pytest, vitest, prettier, eslint, ktlint, detekt, clang-format, clang-tidy, ctest, dotnet build and dotnet test.

Paths in Cargo’s output are shown as /home/you.