Rust: Cargo: Profiles
Cargo has a concept called
profiles, mainly
dev
and release
(in the .NET/Windows world, these are equivalent to debug
and release
). Today, we’re not going to talk about the specific details of
these profiles, but instead focus on their interaction between a binary crate
and a library crate.
At work, we have a library crate that contains the core implementation of our
product, and a binary crate that pulls in this library. To guarantee
correctness, we wanted the library crate to use certain predetermined compiler
switches. That led me to wonder how the [profile.dev]
settings are actually
used. As part of that investigation, I discovered something surprising. Spoiler:
I didn’t find a solution to the original problem, but I did learn something
useful that’s worth sharing.
TL;DR: The [profile.*]
section in a library crate is mostly ignored for
normal library crates(excluding other types of library crates). When Cargo
builds the binary crate, it compiles all dependent library crates using the
binary crate’s profile settings.
Example Demonstration
To verify this, I ran a small experiment.
I created a simple library crate that intentionally causes an integer overflow.
Normally, Rust panics on overflow in debug builds. You can disable this behavior
by setting overflow-checks = false
in the profile section. That’s what I did
in the library crate. Meanwhile, the binary crate that depends on it has
overflow-checks = true
.
Now let’s observe what happens when overflow occurs.
// mylib/src/lib.rs
pub fn add(inc: u8) -> u8 {
let x: u8 = 250 + inc; // This can trigger a runtime panic
x
}
# mylib/Cargo.toml
[package]
name = "mylib"
version = "0.1.0"
edition = "2021"
[dependencies]
[profile.dev]
overflow-checks = false # Disable overflow checks
The binary crate looks like this:
// myexe/src/main.rs
use mylib::add;
fn main() {
let result = add(11); // 250 + 11 = 261, wraps to 5
assert_eq!(result, 5); // Should not panic if mylib settings are used
}
# myexe/Cargo.toml
[package]
name = "myexe"
version = "0.1.0"
edition = "2021"
[dependencies]
mylib = { path = "../mylib" }
[profile.dev]
overflow-checks = true # Enable overflow checks
Now let’s compile and run:
D:\repos\testing\myexe>cargo run
Compiling mylib v0.1.0 (D:\repos\testing\mylib)
Compiling myexe v0.1.0 (D:\repos\testing\myexe)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target\debug\myexe.exe`
thread 'main' panicked at D:\repos\testing\mylib\src\lib.rs:2:17:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` to display a backtrace
error: process didn't exit successfully: `target\debug\myexe.exe` (exit code: 101)
Despite the library disabling overflow checks, the binary still panics. This
confirms that the library’s [profile.dev]
settings were not respected.
If we flip the scenario — enabling overflow checks in the library and disabling them in the binary — the panic does not occur:
D:\repos\testing\myexe>cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target\debug\myexe.exe`
Digging Deeper with --verbose
To understand what’s happening under the hood, we can inspect the compiler flags using cargo build --verbose
.
cargo clean
cargo build --verbose
Here’s the relevant output:
Compiling mylib v0.1.0 (D:\repos\testing\mylib)
Running `...rustc.exe ... -C overflow-checks=off ...`
Compiling myexe v0.1.0 (D:\repos\testing\myexe)
Running `...rustc.exe ... -C overflow-checks=off ...`
Both mylib
and myexe
are compiled with -C overflow-checks=off
, even though
the binary crate’s Cargo.toml explicitly had overflow-checks=true
. This shows
that the binary crate’s profile settings are applied across the board, even
to dependent libraries.
What About Workspaces?
The behavior doesn’t change even if the library crate is inside the same workspace. The profile settings declared at the workspace level mainly apply to test targets, examples, benchmarks, and binaries. Regular libraries will still inherit the settings from the crate that depends on them.
Conclusion
Library-specific profile settings are mostly ignored during normal compilation. The consuming binary crate’s profile dictates how all dependencies are compiled. If you need strict guarantees for how a library is compiled (e.g., turning on or off overflow checks), you’ll have to enforce those via build scripts, CI, or external tooling — not just Cargo profiles.
Let me know if you’ve found any workarounds, because I’m still hunting for one!