RFA-109 · Case file with fixtures · Case 81 of 694 · Cargo workspace evidence
Why Cargo's dep: Feature Syntax Requires an Optional Dependency
The dep: prefix means this feature activates an optional dependency without exposing its package name as an implicit feature. It cannot toggle a dependency that is already unconditionally present.
- Reviewed
- Rust
- Rust 1.98.1, Cargo 1.98.1
- Targets
- all targets
- Profiles
- metadata, check, dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The dep: syntax is specifically an activation edge for an optional dependency, while a mandatory dependency is already present in every build.
- First discriminating check
- Decide whether the dependency is genuinely conditional, then either mark it optional or remove the meaningless dep: activation.
Some Cargo errors happen before dependency resolution and before rustc. The failing manifest contains:
[features]
tools = ["dep:helper"]
[dependencies]
helper = { path = "helper" }
Cargo says the tools feature includes dep:helper, but helper is not optional. This is not a missing package or a source-code error. The manifest asks a feature to activate something that is already always active.
dep: has one precise job
An optional dependency creates conditional graph membership. The optional dependency documentation explains that dep:helper explicitly activates that optional dependency.
Without dep:, Cargo can expose an implicit feature named after the optional dependency. Explicit dep: syntax lets a crate hide that implementation name behind a public feature with product meaning:
[features]
tools = ["dep:helper"]
[dependencies]
helper = { path = "helper", optional = true }
This is what the repaired manifest does.
If helper remains mandatory, the correct feature list simply does not need dep:helper. A mandatory dependency is in every relevant graph regardless of feature selection.
Dependency feature forwarding is different
Cargo also supports enabling a feature on a dependency. The dependency feature documentation uses syntax such as package-name/feature-name.
These two expressions are easy to confuse:
dep:helper activate the optional dependency itself
helper/tracing enable `tracing` on dependency `helper`
The ? form, such as helper?/tracing, enables the dependency feature only if something else activated the optional dependency. It avoids turning on the optional dependency as a side effect.
I spell these relationships out in review because a one-character difference changes the feature graph.
Name features after capabilities
The explicit syntax is useful when the public feature should be stable while internal packages change. A feature called database-postgres might activate a driver, TLS support, and an adapter module together. Consumers should not need to know every transitive package name.
This also reduces accidental public API. Once users depend on an implicit feature named helper, renaming or replacing that dependency can break their manifests. dep: lets me avoid creating that implicit feature and publish only the capability name.
Features remain additive. Activating tools should add tools, not switch unrelated behavior off. If two modes are exclusive, a runtime configuration or split crate may be a better model.
Defaults can conceal the graph
The repaired fixture enables tools by default so its normal binary can import the helper. A library does not have to do this. Keeping heavy capabilities out of defaults can reduce compile time and native prerequisites.
Whatever I choose, I test the intended states:
cargo check --no-default-features
cargo check --features tools
cargo check --all-features
The feature-off source must not mention the absent crate in active code. The feature-on state must activate both dependency and code. Tests and examples need the same attention.
Manifest validation is useful early feedback
Because Cargo rejects the invalid relationship during manifest loading, commands like cargo metadata fail too. Editors and workspace tools may report the error even if I am not building the affected package.
I read the full cause chain. “Failed to parse manifest” is only the outer layer; the final cause names the feature and dependency. Fixing unrelated TOML formatting does not help.
For generated manifests, I inspect the generated file rather than only the template. Workspace inheritance and automated dependency tools can change which table actually owns optional = true.
The lockfile cannot repair an invalid feature declaration
Because this relationship is checked while Cargo parses the manifest, updating or deleting Cargo.lock has no effect. The resolver has not started selecting versions yet. This distinction saves time: parser failures need a coherent manifest; resolver failures need inspection of the package graph.
I also run cargo metadata --no-deps after the edit. It gives fast evidence that workspace tooling can understand the feature declaration before I move to the larger compilation matrix.
My debugging sequence
When Cargo rejects a dep: entry, I follow this order:
- Find the exact
[features]entry and dependency declaration. - Decide whether the dependency is truly conditional.
- If conditional, add
optional = trueand gate all dependent code. - If mandatory, remove
dep:from the feature because there is nothing to activate. - Distinguish dependency activation from dependency feature forwarding.
- Test default, minimal, named, and maximal feature sets.
dep: is not a generic reference to a dependency. It is the explicit activation edge for an optional dependency. Once that rule is clear, the parser error is direct and the public feature design becomes easier to explain.