Hello, World! and Hello, Cargo!
Write and run your first Rust program using the compiler directly and then with Cargo, the build system and package manager. Learn how Cargo.toml and Cargo.lock work and how to produce an optimized release build.
Rust is a compiled language — before a program can run, it must be translated from source code into machine code by the Rust compiler. You have two ways to drive that compilation: the raw rustc command and the cargo tool. Most real projects use Cargo, but understanding what rustc does on its own makes the Cargo workflow feel less like magic.
This guide walks through both paths. You will write a tiny program, compile and run it with rustc, then rebuild the same idea using Cargo. Along the way, you'll learn what the files Cargo generates mean and how to switch from a quick development build to an optimized release binary.
Hello, World! with the Rust Compiler (rustc)
Before you lean on Cargo for everything, it’s worth seeing the compiler work directly. This first example uses only the Rust installation — no project files, no configuration — just a single source file, the compiler, and the resulting executable.
Writing the program
Create a file named hello.rs. The .rs extension is conventional but not enforced by the compiler. Put the following code inside:
fn main() {
println!("Hello, world!");
}
This is the smallest complete Rust program that does something visible. Let’s break it down.
fn main()declares a function namedmainthat takes no arguments. In a Rust binary (as opposed to a library),mainis the entry point — the first code that runs when the executable starts.println!("Hello, world!")prints the text to the terminal and appends a newline. Theprintln!syntax looks like a function call, but the!tells you it is a macro. Macros expand into more code at compile time; for now, you can treatprintln!as if it were a built‑in print function.- The semicolon after the macro call makes the line a statement. In Rust, statements perform actions but do not return a value. If you accidentally omit the semicolon in this spot, the compiler will report an error because
println!returns(), and a bare()as the last expression of a function would become the implicit return value — which is not an error here, but in many contexts the missing semicolon changes the meaning. For beginners, the rule is straightforward: when a line does something (prints, declares a variable, etc.), end it with;.
Compiling and running
Open a terminal in the directory where you saved hello.rs. Run the compiler:
rustc hello.rs
rustc reads the source file, checks it for errors, translates it to machine code, and links it into an executable. If everything succeeds, the command finishes silently and produces a binary.
Compiler output:
If the command completed with no messages, the compilation succeeded. You now have an executable file in the same directory.
The binary’s name depends on your operating system:
- On Linux and macOS: a file named
hello(no extension) - On Windows: a file named
hello.exe
Run the program. Use the tab below that matches your platform.
./hello
Expected output:
Hello, world!
Binary size:
The generated executable will be several megabytes, even for this trivial program. That’s because the default build includes debug symbols, unwinding information, and the standard library. Later you’ll see how Cargo’s release mode shrinks this considerably.
If you were to change the message inside the println! string, you would need to re‑run rustc hello.rs before running the binary again. Compilation is a separate step.
Direct rustc usage:
rustc works well for tiny, single‑file experiments, but it quickly becomes tedious. Real projects need dependency tracking, incremental compilation, and consistent build options — all of which Cargo provides. Use rustc only for learning or one‑off scripts that you are certain will never grow.
Hello, Cargo! — The Rust Build System and Package Manager
Cargo is the tool you will use every day. It compiles your code, fetches libraries from the ecosystem, runs tests, builds documentation, and produces release artifacts. Most importantly, it enforces a standard project layout so that every Rust developer can immediately understand your repository.
The following steps take you from zero to a running Cargo project, explain the configuration files it creates, and show how to switch between development and release builds.
Step 1: Create a new Cargo project
Use the cargo new command to generate a fresh project. Replace hello_cargo with any name you like; Cargo will create a directory with that name.
cargo new hello_cargo
Inside the hello_cargo folder you will find:
hello_cargo
├── Cargo.toml
└── src
└── main.rs
Cargo.toml— the project manifest. It holds the package name, version, edition, and eventually your dependencies.src/main.rs— the same “Hello, world!” source code you wrote manually earlier. Cargo generates it for you because amain.rsfile is the conventional entry point for a binary crate.
Open Cargo.toml in your editor:
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
[dependencies]
Every [package] field matters:
name— the name of your package. It determines the name of the final binary (or library) that Cargo produces.version— follows Semantic Versioning. You increment it as you make releases.edition— specifies which Rust edition your code uses (2015, 2018, 2021, etc.). Editions let the language evolve while keeping old code working. The2021edition is the current default at the time of writing.[dependencies]— currently empty; you will list external crates here later.
After the first build, Cargo also creates a Cargo.lock file alongside Cargo.toml. This file records the exact versions of every dependency and transitive dependency that were resolved. You should never edit Cargo.lock by hand. For binary applications (like this one), commit it to version control so that every team member and CI environment gets identical builds. For libraries, the convention is to omit it — Cargo will still generate it locally, but it is not checked in.
Cargo.toml compared to other ecosystems:
If you have worked with package.json (Node.js), requirements.txt (Python), or pom.xml (Java), Cargo.toml serves a similar role. The Cargo.lock is analogous to package-lock.json or yarn.lock: it pins exact versions for reproducible builds.
Step 2: Build and run the project
Navigate into the project directory:
cd hello_cargo
The simplest way to compile and execute your program is a single command:
cargo run
You will see output similar to:
Compiling hello_cargo v0.1.0 (/path/to/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.12s
Running `target/debug/hello_cargo`
Hello, world!
cargo run performs three actions in sequence: it checks whether the source has changed, re‑compiles if necessary, and then launches the resulting binary. If you run it again without modifying main.rs, Cargo skips the compilation step entirely and prints only the Hello, world! line.
Two other commands are worth knowing now:
cargo build— compiles the project but does not run the binary. The executable is placed attarget/debug/hello_cargo(orhello_cargo.exeon Windows). Use this when you only want to verify that the code compiles.cargo check— performs even less work: it runs the compiler’s syntax and type‑checking passes but does not produce a final binary. This is much faster thancargo buildand is ideal for quickly catching errors during development.
Run cargo from the project root:
If you invoke cargo run outside a directory that contains a Cargo.toml file, Cargo will print an error like failed to find any projects. Always run Cargo commands from the root of your project (or a subdirectory, but the root is the clearest).
First Cargo success:
Once you see “Hello, world!” appear, your Rust toolchain and Cargo are fully operational. This confirms that the compiler, linker, and standard library are all in place.
Step 3: Build for release
The default development build prioritizes fast compilation over runtime speed and binary size. When you are ready to ship or benchmark your program, switch to release mode:
cargo build --release
You can also run the release binary directly with:
cargo run --release
Release mode applies several optimizations:
- The compiler’s optimisation level is set to
opt-level=3, which enables aggressive inlining, loop unrolling, and other transformations. - Debug assertions (the
debug_assert!macro) are stripped. - The binary is linked with fewer debug symbols, dramatically reducing its size.
The release executable appears at target/release/hello_cargo instead of target/debug/hello_cargo. On a tiny program like this, the size difference is pronounced — a debug build may be around 4 MB, while the release binary often shrinks to a few hundred kilobytes.
Release binary behaviour:
While the output “Hello, world!” is identical, release builds skip integer overflow checks that debug builds include. If you are writing arithmetic‑heavy code, be aware that a panic that showed up in development might silently wrap in release. Test your application in both modes before distributing.
Use release mode for:
- Performance measurements and benchmarks
- The final binary you give to users or deploy to a server
- Any scenario where compile time is less important than runtime efficiency
During everyday coding, stay in debug mode — cargo run and cargo check are faster, and the extra debug checks can catch bugs earlier.
Common Pitfalls When Working with Cargo
Even a “Hello, World!” project can expose a few early mistakes. Here are the ones that trip up newcomers most often.
cargo: command not found— your terminal session doesn’t know wherecargolives. Usually this means the installer’s PATH changes haven’t taken effect. Close and reopen the terminal, or restart VS Code, and try again.- Running
cargo runin the wrong directory — Cargo looks forCargo.tomlin the current directory. If you’re outside the project root, you’ll seefailed to find any projects. Alwayscdinto the folder that containsCargo.toml. - Editing
Cargo.lockmanually — the lock file is generated. If you want to upgrade a dependency, modifyCargo.tomland runcargo update; Cargo will regenerate the lock. EditingCargo.lockby hand almost always leads to confusion. - Using
rustcon a Cargo project —rustcdoes not know aboutCargo.tomlor dependencies. If you runrustc src/main.rsinside a Cargo project, it may fail to find crates you’ve listed. Stick tocargo buildorcargo run. - Forgetting the
src/directory — Cargo expects themain.rsfile to be insidesrc/. If you movemain.rsto the project root and delete thesrcfolder, Cargo will report that it “can’t findmaininsrc”. The layout matters.
Semantic version note:
The version in Cargo.toml is your package’s public version, not the Rust compiler version. Rust editions are set with the edition key, and the compiler version is printed by rustc --version. These are three separate numbers that beginners sometimes conflate.
Summary
You have written, compiled, and run a Rust program twice — first with the bare compiler, then with Cargo. The rustc path demystifies what Cargo does under the hood; the Cargo path demonstrates the professional workflow you will use for every real Rust project.
The key takeaway: Cargo is not an optional add‑on; it is the standard front door to the Rust ecosystem. Every new project starts with cargo new, every build goes through cargo build or cargo run, and every dependency is declared in Cargo.toml. Learning these four commands — cargo new, cargo build, cargo run, and cargo check — gives you a repeatable foundation for everything that follows.