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 named main that takes no arguments. In a Rust binary (as opposed to a library), main is 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. The println! 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 treat println! 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.

1

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 a main.rs file 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. The 2021 edition 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.

2

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 at target/debug/hello_cargo (or hello_cargo.exe on 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 than cargo build and 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.

3

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 where cargo lives. 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 run in the wrong directory — Cargo looks for Cargo.toml in the current directory. If you’re outside the project root, you’ll see failed to find any projects. Always cd into the folder that contains Cargo.toml.
  • Editing Cargo.lock manually — the lock file is generated. If you want to upgrade a dependency, modify Cargo.toml and run cargo update; Cargo will regenerate the lock. Editing Cargo.lock by hand almost always leads to confusion.
  • Using rustc on a Cargo projectrustc does not know about Cargo.toml or dependencies. If you run rustc src/main.rs inside a Cargo project, it may fail to find crates you’ve listed. Stick to cargo build or cargo run.
  • Forgetting the src/ directory — Cargo expects the main.rs file to be inside src/. If you move main.rs to the project root and delete the src folder, Cargo will report that it “can’t find main in src”. 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.