Dependency Management and Cargo Machete#
Lead Section#
Dependency Management and Cargo Machete is a comprehensive approach to maintaining clean and secure Rust project dependencies, particularly in workspace environments with multiple crates. cargo-machete is a static analysis tool that identifies unused dependencies in Cargo.toml files by scanning source code for actual usage. In the dbsurveyor project, cargo-machete is integrated as a pre-commit hook to automatically detect unused dependencies before code is committed, ensuring continuous dependency hygiene.
Effective dependency management requires understanding feature-gated dependencies and optional dependencies in Rust. Feature gates control which dependencies are compiled into a binary, enabling minimal builds for specific use cases. However, this creates complexity for dependency analysis tools: a dependency may be unused in one feature configuration but essential in another. The key challenge is distinguishing truly unused dependencies from those that are feature-gated or conditionally compiled using #[cfg(feature)] attributes.
In March 2026, the dbsurveyor project removed eight unused dependencies from dbsurveyor-core (anyhow, serde, tracing-subscriber, askama, uuid, rand, tiberius, and zstd) after cargo-machete analysis. This cleanup demonstrated the importance of careful verification: while cargo-machete correctly identified these as unused in the core crate, some dependencies like tiberius and zstd remained necessary in binary crates where they were used behind feature gates. This article documents best practices for dependency cleanup using cargo-machete, handling false positives with feature-gated code, and maintaining Cargo.toml files across multi-crate workspaces.
Cargo Machete Overview#
What is Cargo Machete?#
cargo-machete is a static analysis tool for Rust projects that identifies dependencies declared in Cargo.toml files but never imported or used in source code. Unlike cargo tree which shows dependency relationships, or cargo audit which finds security vulnerabilities, cargo-machete specifically targets unused dependencies — crates listed in [dependencies] or [dev-dependencies] sections that have no corresponding use statements in the codebase.
The tool works by parsing Cargo.toml to extract declared dependencies, then scanning all Rust source files (.rs) for import statements. Dependencies that are declared but never imported are flagged as potentially unused. This static analysis approach makes cargo-machete fast and suitable for continuous integration workflows.
Integration as Pre-Commit Hook#
In dbsurveyor, cargo-machete is configured in .pre-commit-config.yaml:
- repo: https://github.com/bnjbvr/cargo-machete
rev: v0.9.2
hooks:
- id: cargo-machete
This integration ensures cargo-machete runs automatically when developers commit code using the pre-commit framework. The hook executes before the commit is finalized, preventing unused dependencies from being introduced into the codebase.
Developers can also run pre-commit checks manually via the justfile command: just pre-commit, which runs cargo-machete alongside other quality checks like cargo fmt, cargo clippy, and cargo audit.
Benefits and Limitations#
Benefits:
- Security: Fewer dependencies mean a smaller attack surface and fewer potential vulnerabilities
- Build Performance: Removing unused dependencies reduces compile times
- Maintenance: Simplifies dependency updates and reduces noise in Cargo.lock
- Code Clarity: Eliminates confusion about which dependencies are actually used
Limitations:
- False Positives with Feature Gates: cargo-machete may flag dependencies used only behind
#[cfg(feature)]attributes as unused - Procedural Macros: Dependencies that are never explicitly imported (e.g., derive macros) may be incorrectly flagged
- Build Scripts: Dependencies used only in
build.rsmay not be detected - Workspace Analysis: In multi-crate workspaces, a dependency may be unused in one crate but essential in another
Feature-Gated Dependencies#
Understanding Cargo Features#
Cargo features are a mechanism for conditional compilation in Rust. They allow crates to define optional functionality that can be enabled at build time using the --features flag. In dbsurveyor-core, features control database driver inclusion:
[features]
default = ["postgresql", "sqlite"]
# Database driver features
postgresql = ["sqlx", "sqlx/postgres"]
mysql = ["sqlx", "sqlx/mysql"]
sqlite = ["sqlx", "sqlx/sqlite"]
mongodb = ["dep:mongodb"]
mssql = []
# Optional features
compression = []
encryption = ["dep:aes-gcm", "dep:argon2", "dep:password-hash"]
The dep: syntax indicates optional dependencies that are only compiled when the feature is enabled. For example, mongodb = ["dep:mongodb"] means the mongodb crate is only included when building with --features mongodb.
Optional Dependencies#
Optional dependencies are declared in Cargo.toml with optional = true:
[dependencies]
sqlx = { workspace = true, optional = true }
mongodb = { workspace = true, optional = true }
aes-gcm = { workspace = true, optional = true }
argon2 = { workspace = true, optional = true }
password-hash = { workspace = true, optional = true }
These dependencies are excluded from the default build unless a feature that references them is enabled. This pattern allows minimal binary size while supporting extensive functionality when needed.
Empty Feature Gates#
A unique pattern in dbsurveyor is empty feature gates — features defined with no associated dependencies:
These empty features enable conditional compilation with #[cfg(feature = "mssql")] attributes in the code without pulling in dependencies at the core library level. The actual dependencies (like tiberius for SQL Server or zstd for compression) are only included in the binary crates that directly use them.
This pattern is important for cargo-machete analysis because the dependency appears unused in the core crate (correctly flagged) but is essential in the binary crate where it's actually imported.
Conditional Compilation Patterns#
Module-Level Gating#
Database adapter modules in dbsurveyor-core are conditionally compiled:
#[cfg(feature = "postgresql")]
pub mod postgres;
#[cfg(feature = "mysql")]
pub mod mysql;
#[cfg(feature = "sqlite")]
pub mod sqlite;
#[cfg(feature = "mongodb")]
pub mod mongodb;
#[cfg(feature = "mssql")]
pub mod mssql;
When a feature is disabled (e.g., building without --features mysql), the entire module is excluded from compilation. This reduces binary size and eliminates the need for the associated dependencies.
Function-Level Gating#
Functions handling optional features like compression are also feature-gated:
#[cfg(feature = "compression")]
async fn save_compressed(json_data: &str, output_path: &PathBuf) -> Result<()> {
use std::io::Write;
let mut encoder = zstd::Encoder::new(Vec::new(), 3)?;
encoder.write_all(json_data.as_bytes())?;
// ... implementation
}
Runtime Error Handling#
A critical pattern for user experience is paired feature gates with runtime error handling:
match database_type {
#[cfg(feature = "postgresql")]
DatabaseType::PostgreSQL => {
let adapter = postgres::PostgresAdapter::new(connection_string).await?;
Ok(Box::new(adapter))
}
#[cfg(not(feature = "postgresql"))]
DatabaseType::PostgreSQL => {
Err(DbSurveyorError::unsupported_feature(
"PostgreSQL adapter",
"Compile with --features postgresql to enable PostgreSQL support",
))
}
// ... similar pairs for other databases
}
This pattern ensures users receive clear, actionable error messages when attempting to use features not compiled into their binary, rather than cryptic compiler errors or panics.
March 2026 Dependency Cleanup#
Identified Unused Dependencies#
In PR #107 merged on March 5, 2026, cargo-machete identified eight unused dependencies in dbsurveyor-core:
- anyhow - Error handling library
- serde - Serialization framework
- tracing-subscriber - Logging subscriber
- askama - Template engine
- uuid - UUID generation
- rand - Random number generation
- tiberius - SQL Server driver
- zstd - Compression library
Verification Process#
Before removing flagged dependencies, the dbsurveyor team verified:
- No direct imports: Searched source code for
usestatements referencing these crates - No feature-gated usage: Confirmed no
#[cfg(feature)]blocks in dbsurveyor-core imported these dependencies - Workspace context: Verified whether the dependency was used in other workspace crates (dbsurveyor-collect, dbsurveyor binary)
Removals vs. Relocations#
The cleanup revealed an important distinction:
Truly unused (removed completely from core):
- anyhow, serde, tracing-subscriber, askama, uuid, rand - These were never used in dbsurveyor-core and could be safely removed
Used in other crates (kept in workspace, removed from core):
- tiberius: Removed from dbsurveyor-core but kept in dbsurveyor-collect where it's used for SQL Server support behind the
mssqlfeature - zstd: Removed from dbsurveyor-core but kept in both binary crates for the
compressionfeature
This demonstrates why the empty feature gates (mssql = [], compression = []) exist in dbsurveyor-core: they enable conditional compilation in the core library without requiring the actual dependencies, which are only needed in the binaries.
Post-Cleanup Procedure#
After removing dependencies from Cargo.toml files, the recommended procedure is:
- Run
cargo generate-lockfileto regenerate Cargo.lock cleanly - Run
cargo buildto verify the project still compiles - Run
cargo testwith all feature combinations to ensure no broken feature gates - Verify pre-commit hooks pass (including cargo-machete)
Maintenance Procedures#
Regular Dependency Audits#
Best practices for ongoing dependency management:
- Run cargo-machete regularly: Either via pre-commit hooks or scheduled CI jobs
- Review flagged dependencies carefully: Don't blindly remove everything cargo-machete suggests
- Check feature-gated usage: Search for
#[cfg(feature)]blocks that might use the dependency - Test all feature combinations: Use
cargo test --all-featuresandcargo test --no-default-features
Cargo.toml Organization#
dbsurveyor uses workspace-level dependency management to centralize version control:
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.43", features = ["full"] }
sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio-rustls"] }
tiberius = { version = "0.12", default-features = false, features = ["native-tls"] }
mongodb = { version = "3.2", default-features = false, features = ["tokio-runtime"] }
zstd = { version = "0.13", default-features = false }
# ... more dependencies
Individual crates then reference these with { workspace = true }:
[dependencies]
sqlx = { workspace = true, optional = true }
mongodb = { workspace = true, optional = true }
This approach ensures version consistency across the workspace and makes it easier to identify which dependencies are actually used where.
Security Considerations#
The March 2026 cleanup also introduced cargo-audit configuration to track known security advisories:
- RUSTSEC-2023-0071: RSA timing attack in transitive dependency via sqlx-mysql
Removing unused dependencies reduces exposure to security vulnerabilities, even transitive ones. Regular security audits with cargo audit should complement cargo-machete analysis.
Handling False Positives#
Identifying False Positives#
cargo-machete may incorrectly flag dependencies as unused in several scenarios:
- Derive Macros: Crates like
serde_deriveorthiserrorare used via#[derive()]attributes and never explicitly imported - Proc Macros: Procedural macros used via attributes may not have visible
usestatements - Build Dependencies: Dependencies used in
build.rsscripts - Feature-Gated Code: Dependencies used only in specific feature configurations
- Reexported Types: Dependencies used only through reexports from another crate
Verification Techniques#
To verify a cargo-machete finding before removing a dependency:
- Search for imports:
rg "use.*<crate_name>" --type rust - Search for feature gates:
rg "#\[cfg\(feature.*<crate_name>" --type rust - Check derive attributes:
rg "#\[derive.*" --type rustand look for related macros - Review build scripts: Check
build.rsfiles for usage - Test without the dependency: Temporarily comment out the dependency and try to build
Configuring Exclusions#
If cargo-machete consistently reports false positives, consider documenting why the dependency is needed with comments in Cargo.toml:
[dependencies]
# Used by derive macros in models, not directly imported
serde = { version = "1.0", features = ["derive"] }
# Used only behind #[cfg(feature = "encryption")]
aes-gcm = { version = "0.10", optional = true }
Workspace-Specific Challenges#
Multi-Crate Analysis#
In a workspace like dbsurveyor with three crates (dbsurveyor-core, dbsurveyor-collect, dbsurveyor), cargo-machete analyzes each crate independently. A dependency may be:
- Globally unused: Not used in any crate (safe to remove from workspace dependencies)
- Locally unused: Used in some crates but not others (remove from specific Cargo.toml files)
- Feature-specific: Used only when certain features are enabled
Dependency Propagation#
Binary crates often propagate features to library crates:
postgresql = [
"dbsurveyor-core/postgresql", # Enable feature in dependency
"dep:sqlx", # Enable local optional dependency
"sqlx/postgres", # Enable sqlx feature
]
This pattern means:
- The binary enables its own feature (
postgresql) - This propagates the feature to dbsurveyor-core (
dbsurveyor-core/postgresql) - Both crates get the necessary dependencies (
sqlxwithpostgresfeature)
Understanding this propagation is critical for cargo-machete analysis: the dependency might appear in the core library's Cargo.toml but only be used when the binary crate enables the feature.
Testing Strategy#
For workspaces with complex feature dependencies, comprehensive testing is essential:
# Test default features
cargo test
# Test all features
cargo test --all-features
# Test no features
cargo test --no-default-features
# Test specific feature combinations
cargo test --no-default-features --features postgresql,encryption
cargo test --features mysql,compression
This ensures no feature combination is broken by dependency removal.
Best Practices#
Before Removing Dependencies#
- Run cargo-machete: Identify candidates for removal
- Verify with grep/ripgrep: Search for actual usage in source files
- Check feature gates: Look for
#[cfg(feature)]conditional compilation - Review workspace structure: Confirm the dependency isn't used in other crates
- Check build scripts: Verify not used in
build.rsfiles - Examine macros: Identify derive macros or procedural macros
After Removing Dependencies#
- Regenerate Cargo.lock: Run
cargo generate-lockfile - Build and test: Run
cargo buildandcargo test --all-features - Run pre-commit checks: Verify
just pre-commitor equivalent passes - Update documentation: Note dependency changes in CHANGELOG or commit messages
- Run security audit: Execute
cargo auditto check for new vulnerabilities
Continuous Integration#
Integrate cargo-machete into CI pipelines:
# Example GitHub Actions workflow
- name: Check for unused dependencies
run: |
cargo install cargo-machete
cargo machete
This catches unused dependencies before they reach the main branch.
Documentation#
Maintain clear documentation about:
- Which dependencies are feature-gated
- Why certain dependencies might appear unused but are actually necessary
- The relationship between workspace dependencies and crate-specific dependencies
Consider adding comments in Cargo.toml explaining non-obvious dependencies:
# 🔒 SECURITY GUARANTEES ENFORCED BY WORKSPACE:
# - Feature flags ensure minimal attack surface for airgap deployments
Relevant Code Files#
| File Path | Purpose | Key Features |
|---|---|---|
.pre-commit-config.yaml | Pre-commit hook configuration | cargo-machete v0.9.2 integration |
Cargo.toml | Workspace root manifest | Centralized dependency management, workspace member definitions |
dbsurveyor-core/Cargo.toml | Core library dependencies | Feature definitions, optional dependencies, empty feature gates |
dbsurveyor-collect/Cargo.toml | Collector binary dependencies | Feature propagation, database driver dependencies |
dbsurveyor/Cargo.toml | Postprocessor binary dependencies | Minimal features (compression, encryption only) |
dbsurveyor-core/src/adapters/mod.rs | Database adapter factory | Feature-gated modules, runtime error handling |
dbsurveyor-collect/src/main.rs | Collector main binary | Feature-gated compression/encryption functions |
justfile | Development automation | Pre-commit check orchestration |
Related Topics#
- Cargo Features: Rust's conditional compilation mechanism for optional functionality
- Workspace Dependencies: Managing dependencies across multiple crates in a Cargo workspace
- Supply Chain Security: Using tools like
cargo auditandcargo denyto identify vulnerabilities - Binary Size Optimization: Reducing compiled binary size through feature selection
- Conditional Compilation: Using
#[cfg(feature)]attributes for platform- and feature-specific code - Pre-commit Hooks: Automating quality checks before code is committed to version control