The Cargo Guide

Manifest Target Sections: lib, bin, example, test, and bench

Why Target Sections Exist

Cargo can often discover targets by convention alone, but Cargo.toml also lets you describe targets explicitly. This becomes useful when you want to control names, paths, features, harness behavior, or crate output types.

The main target sections are:

  • [lib]
  • [[bin]]
  • [[example]]
  • [[test]]
  • [[bench]]

A useful mental model is:

  • implicit targets are fine for simple packages
  • explicit target sections are for packages that need precision

For example, this package relies entirely on convention:

my_app/
ā”œā”€ā”€ Cargo.toml
ā”œā”€ā”€ src/
│   ā”œā”€ā”€ lib.rs
│   ā”œā”€ā”€ main.rs
│   └── bin/
│       └── admin.rs
ā”œā”€ā”€ examples/
│   └── demo.rs
ā”œā”€ā”€ tests/
│   └── api_test.rs
└── benches/
    └── speed.rs

Cargo can discover all of that automatically. But once you need to override a name, move a file, disable auto-discovery, or require a feature for one target, the manifest target sections become important.

The Difference Between [lib] and [[bin]]-Style Tables

The library target section is written as a single table because a package can have at most one library target.

[lib]
name = "math_core"
path = "src/lib.rs"

Binary, example, test, and bench targets use array-of-table syntax because there can be many of them.

[[bin]]
name = "server"
path = "src/bin/server.rs"
 
[[bin]]
name = "client"
path = "src/bin/client.rs"

The same pattern applies to examples, tests, and benches.

This is a small TOML detail, but it helps reinforce a key Cargo rule: one package can expose many targets, but only one library target.

The [lib] Section

The [lib] section describes the package's library target.

Example:

[package]
name = "text_tools"
version = "0.1.0"
edition = "2024"
 
[lib]
name = "text_tools"
path = "src/lib.rs"

Library code:

// src/lib.rs
pub fn slugify(s: &str) -> String {
    s.trim().to_lowercase().replace(' ', "-")
}

In many packages you can omit [lib] entirely because Cargo already expects src/lib.rs. But making it explicit helps when:

  • the library source file is not in the default location
  • you want to control crate-type
  • you want to disable auto-discovery and declare the library intentionally

Library Name and Path

The most common fields in [lib] are name and path.

[lib]
name = "core_utils"
path = "src/core/mod.rs"

That tells Cargo that the library target is named core_utils and lives at a non-default path.

Example file:

// src/core/mod.rs
pub fn double(x: i32) -> i32 {
    x * 2
}

Then a binary target in the same package can use it:

// src/main.rs
fn main() {
    println!("{}", core_utils::double(21));
}

This kind of explicit path override is useful, but beginners should usually stick with src/lib.rs unless there is a real structural reason not to.

crate-type for Library Targets

The crate-type field controls what kind of artifact the library target produces.

Example:

[lib]
name = "ffi_demo"
path = "src/lib.rs"
crate-type = ["rlib", "cdylib"]

This is an advanced lever, but it matters when the package is meant for more than ordinary Rust-to-Rust reuse.

A simple Rust library usually works with the default behavior. crate-type becomes relevant when you need outputs such as dynamic libraries for FFI or other integration patterns.

Example Rust source:

#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

A beginner should take away one main idea: crate-type is about how the library is emitted, not about the internal Rust logic itself.

The [[bin]] Section

The [[bin]] section declares an executable target.

Example:

[[bin]]
name = "reporter"
path = "src/bin/reporter.rs"

Binary source:

fn main() {
    println!("Generating report...");
}

Run it with:

cargo run --bin reporter

This is useful when:

  • the binary is not in the default place
  • you want to give it a specific name
  • the package has multiple binaries and you want to be explicit

A package may have many binary targets, which is why [[bin]] can appear multiple times.

Multiple Binaries in One Package

A package can contain several executables.

Example manifest:

[[bin]]
name = "importer"
path = "src/bin/importer.rs"
 
[[bin]]
name = "exporter"
path = "src/bin/exporter.rs"

Source files:

// src/bin/importer.rs
fn main() {
    println!("Importing data");
}
// src/bin/exporter.rs
fn main() {
    println!("Exporting data");
}

Commands:

cargo run --bin importer
cargo run --bin exporter

This is a common pattern when one package groups several related command-line tools around shared library code.

The [[example]] Section

Example targets are runnable programs intended to show how a package is used.

Example manifest:

[[example]]
name = "quickstart"
path = "examples/quickstart.rs"

Example source:

use text_tools::slugify;
 
fn main() {
    println!("{}", slugify("Hello Cargo"));
}

Run it with:

cargo run --example quickstart

Examples are valuable because they serve as both learning material and executable documentation. Declaring them explicitly in the manifest is most useful when their path or configuration differs from the defaults.

The [[test]] Section

Integration tests can be declared explicitly with [[test]].

Example:

[[test]]
name = "api_contract"
path = "tests/api_contract.rs"

Test source:

use text_tools::slugify;
 
#[test]
fn spaces_become_hyphens() {
    assert_eq!(slugify("hello world"), "hello-world");
}

Run it with:

cargo test --test api_contract

Cargo usually auto-discovers integration tests in tests/, so explicit [[test]] entries are mainly for cases where you want more precise control.

The [[bench]] Section

Benchmark targets can be declared with [[bench]].

Example:

[[bench]]
name = "parsing_speed"
path = "benches/parsing_speed.rs"

Illustrative source:

fn main() {
    println!("Benchmark target placeholder");
}

Cargo can manage benchmark targets, but benchmarking details vary depending on toolchain and benchmark approach. At the manifest level, the important part is that benches are first-class targets with their own declaration form.

Target Names and Why They Matter

Every explicit target can be given a name.

Example:

[[bin]]
name = "data-clean"
path = "src/bin/cleanup.rs"

That name is how Cargo refers to the target:

cargo run --bin data-clean

The filename and the Cargo target name do not have to match exactly, although matching them is often clearer.

The broader lesson is that the target name is part of the package's command surface. It affects how users invoke binaries, tests, examples, and benches.

Target Paths and Non-Default Layouts

Each target can point at a custom source path.

Example:

[[example]]
name = "walkthrough"
path = "demo/walkthrough.rs"

Source:

fn main() {
    println!("This example lives outside examples/");
}

This flexibility is useful, but Cargo's defaults are strong enough that custom paths should be used deliberately rather than casually. Standard layouts are easier for both tools and humans to understand.

required-features Per Target

A target can be made available only when certain features are enabled.

Example manifest:

[features]
cli = []
admin = []
 
[[bin]]
name = "admin-tool"
path = "src/bin/admin.rs"
required-features = ["admin"]
 
[[example]]
name = "cli_demo"
path = "examples/cli_demo.rs"
required-features = ["cli"]

Source:

// src/bin/admin.rs
fn main() {
    println!("Admin tool enabled");
}

Trying to run a target without its required features will not work the same way as a normal always-available target.

Enable the feature explicitly:

cargo run --bin admin-tool --features admin
cargo run --example cli_demo --features cli

This pattern is useful when some targets depend on optional capabilities, heavier dependencies, or platform-specific behavior.

Why required-features Matters for Learning

Beginners often first encounter features as something attached to dependencies, but required-features shows that features can also shape which targets exist operationally.

A good mental model is:

  • features can change code paths inside a target
  • features can also gate whether a target should be built or run at all

This becomes especially useful for examples, optional binaries, and special test or benchmark targets.

Test and Bench Harness Flags

Cargo can control whether the standard test harness is used for test and benchmark-style targets.

Example test target with harness disabled:

[[test]]
name = "custom_runner"
path = "tests/custom_runner.rs"
harness = false

Source:

fn main() {
    println!("Running custom test logic");
}

Likewise for benchmarks:

[[bench]]
name = "manual_bench"
path = "benches/manual_bench.rs"
harness = false

Source:

fn main() {
    println!("Running custom benchmark logic");
}

The core idea is that the harness flag controls whether Cargo uses Rust's usual test harness machinery for that target. Most learners should keep the default behavior until they have a specific reason to manage execution themselves.

A Concrete Example with harness = false

Consider a package that wants a custom executable-style integration test.

Manifest:

[[test]]
name = "smoke"
path = "tests/smoke.rs"
harness = false

Test file:

fn main() {
    println!("Smoke test starting...");
 
    let result = 2 + 2;
    if result != 4 {
        std::process::exit(1);
    }
}

Run it with:

cargo test --test smoke

This is not the normal beginner path, but it helps make the harness concept concrete: with harness = false, the target behaves more like a manually controlled executable.

Disabling Auto-Discovery

Cargo can automatically discover targets in conventional locations, but you can disable this behavior at the package level.

The relevant package-level flags are:

  • autolib
  • autobins
  • autoexamples
  • autotests
  • autobenches

Example:

[package]
name = "explicit_targets"
version = "0.1.0"
edition = "2024"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false

Once you do this, you are opting into an explicit manifest-driven model where targets must be declared intentionally.

Why Disable Auto-Discovery

Disabling auto-discovery is useful when:

  • you want strict control over exactly which targets exist
  • your repository layout is unusual
  • you want to prevent accidental target pickup from conventional directories
  • you want the manifest to act as the authoritative list of targets

This is more common in larger or more specialized packages than in beginner learning projects.

A good beginner principle is:

  • use auto-discovery when the project is simple
  • disable it only when precision is more important than convenience

An Explicit-Only Package Example

Here is a package that disables auto-discovery and declares everything manually.

[package]
name = "explicit_demo"
version = "0.1.0"
edition = "2024"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
 
[features]
extra = []
 
[lib]
name = "explicit_demo"
path = "src/library.rs"
crate-type = ["rlib"]
 
[[bin]]
name = "main-tool"
path = "tools/main_tool.rs"
 
[[bin]]
name = "extra-tool"
path = "tools/extra_tool.rs"
required-features = ["extra"]
 
[[example]]
name = "intro"
path = "demo/intro.rs"
 
[[test]]
name = "api"
path = "qa/api.rs"
 
[[bench]]
name = "throughput"
path = "perf/throughput.rs"
harness = false

Illustrative layout:

explicit_demo/
ā”œā”€ā”€ Cargo.toml
ā”œā”€ā”€ src/
│   └── library.rs
ā”œā”€ā”€ tools/
│   ā”œā”€ā”€ main_tool.rs
│   └── extra_tool.rs
ā”œā”€ā”€ demo/
│   └── intro.rs
ā”œā”€ā”€ qa/
│   └── api.rs
└── perf/
    └── throughput.rs

This shows the full power of explicit target declarations: the package no longer depends on Cargo's normal file conventions.

Using an Explicit Library with Binaries

Suppose the explicit-only package above uses shared library code.

Library:

// src/library.rs
pub fn normalize(s: &str) -> String {
    s.trim().to_lowercase()
}

Binary:

// tools/main_tool.rs
fn main() {
    println!("{}", explicit_demo::normalize("  Hello  "));
}

Run it:

cargo run --bin main-tool

Feature-gated binary:

// tools/extra_tool.rs
fn main() {
    println!("{}", explicit_demo::normalize("  EXTRA  "));
}

Run it with its required feature:

cargo run --bin extra-tool --features extra

This helps make the explicit target model feel concrete rather than abstract.

Examples, Tests, and Benches in One Package

Here is a more ordinary package that still uses explicit target sections for clarity.

[package]
name = "format_lab"
version = "0.1.0"
edition = "2024"
 
[lib]
path = "src/lib.rs"
 
[[example]]
name = "basic"
path = "examples/basic.rs"
 
[[test]]
name = "integration"
path = "tests/integration.rs"
 
[[bench]]
name = "formatting"
path = "benches/formatting.rs"
harness = false

Library code:

// src/lib.rs
pub fn trim_upper(s: &str) -> String {
    s.trim().to_uppercase()
}

Example:

// examples/basic.rs
use format_lab::trim_upper;
 
fn main() {
    println!("{}", trim_upper("  hello  "));
}

Integration test:

// tests/integration.rs
use format_lab::trim_upper;
 
#[test]
fn trims_and_uppercases() {
    assert_eq!(trim_upper("  hello  "), "HELLO");
}

Commands:

cargo run --example basic
cargo test --test integration
cargo bench --bench formatting

Common Beginner Confusions

Confusion 1: "If a file is under src/bin/, I must declare it in Cargo.toml."

Not necessarily. Cargo usually auto-discovers it.

Confusion 2: "[lib] and [[bin]] use different syntax by accident."

No. The different syntax reflects one library target versus potentially many binary targets.

Confusion 3: "required-features changes library code automatically."

Not directly. It controls whether the target is available unless the named features are enabled.

Confusion 4: "harness = false is normal for tests."

Usually not. Most tests should use the standard harness.

Confusion 5: "Disabling auto-discovery is better because it is more explicit."

Only when you actually need that control. For many projects, the conventions are clearer and simpler.

A Hands-On Exercise

Create a package and first let Cargo discover targets automatically.

cargo new target_lab
cd target_lab
mkdir -p src/bin examples tests benches

Add files:

// src/lib.rs
pub fn square(x: i32) -> i32 {
    x * x
}
// src/bin/show.rs
fn main() {
    println!("{}", target_lab::square(5));
}
// examples/demo.rs
use target_lab::square;
 
fn main() {
    println!("{}", square(7));
}
// tests/basic.rs
use target_lab::square;
 
#[test]
fn squares() {
    assert_eq!(square(4), 16);
}

Try:

cargo run --bin show
cargo run --example demo
cargo test

Then convert the package to explicit target declarations by disabling auto-discovery and adding [lib], [[bin]], [[example]], and [[test]] sections to the manifest. That contrast will make the purpose of manifest target sections much easier to understand.

Mental Model Summary

A strong mental model for target sections is:

  • [lib] defines the package's single library target
  • [[bin]], [[example]], [[test]], and [[bench]] define repeatable target kinds
  • names and paths let you control how targets are identified and where they live
  • required-features can gate whether a target is available
  • harness changes whether the standard test harness is used for test or benchmark targets
  • crate-type controls what kind of output a library target produces
  • auto-discovery is the convenient default, while explicit target declarations give precision when needed

Once this model is stable, Cargo.toml becomes much easier to read as a map of everything the package can build and run.