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.rsCargo 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 reporterThis 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 exporterThis 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 quickstartExamples 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_contractCargo 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-cleanThe 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 cliThis 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 = falseSource:
fn main() {
println!("Running custom test logic");
}Likewise for benchmarks:
[[bench]]
name = "manual_bench"
path = "benches/manual_bench.rs"
harness = falseSource:
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 = falseTest file:
fn main() {
println!("Smoke test starting...");
let result = 2 + 2;
if result != 4 {
std::process::exit(1);
}
}Run it with:
cargo test --test smokeThis 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:
autolibautobinsautoexamplesautotestsautobenches
Example:
[package]
name = "explicit_targets"
version = "0.1.0"
edition = "2024"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = falseOnce 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 = falseIllustrative layout:
explicit_demo/
āāā Cargo.toml
āāā src/
ā āāā library.rs
āāā tools/
ā āāā main_tool.rs
ā āāā extra_tool.rs
āāā demo/
ā āāā intro.rs
āāā qa/
ā āāā api.rs
āāā perf/
āāā throughput.rsThis 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-toolFeature-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 extraThis 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 = falseLibrary 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 formattingCommon 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 benchesAdd 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 testThen 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-featurescan gate whether a target is availableharnesschanges whether the standard test harness is used for test or benchmark targetscrate-typecontrols 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.
