Versioning and dependency strategies
Dependency management in Rust involves selecting, versioning, and maintaining external crates. Good practices ensure security, reproducibility, and minimal bloat.
Poor dependency management leads to:
# Cargo.toml - Best practices for dependencies
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
[dependencies]
# Pin major version - allows compatible updates
tokio = "1"
serde = "1"
# Pin minor version - more conservative
sqlx = "0.7"
# Pin exact version - for critical dependencies
ring = "=0.17.7"
# Use caret (^) explicitly - same as default
regex = "^1.10"
# Minimum version - at least this version
# (rarely used, usually for bug fixes)
uuid = ">=1.6"
# Git dependencies - for unreleased fixes
# my-fork = { git = "https://github.com/me/crate", branch = "fix" }
# Path dependencies - for local development
# my-lib = { path = "../my-lib" }
# Workspace dependencies - centralized management
[dependencies.common]
workspace = true
# Feature selection - only enable what you need
[dependencies.reqwest]
version = "0.11"
default-features = false
features = ["json", "rustls-tls"]
# Optional dependencies for features
[dependencies.tracing]
version = "0.1"
optional = true
[dev-dependencies]
# Test dependencies - only for tests
tokio-test = "0.4"
mockall = "0.12"
criterion = "0.5"
[build-dependencies]
# Build script dependencies
tonic-build = "0.10"
[features]
default = []
tracing = ["dep:tracing"]
# Cargo.lock - Commit for applications, not for libraries
# This file is auto-generated
[[package]]
name = "tokio"
version = "1.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abc123..."
dependencies = [
"bytes",
"mio",
"parking_lot",
]
// src/lib.rs - Dependency usage patterns
//! Library demonstrating good dependency practices
// Re-export key types users will need
pub use uuid::Uuid;
// Hide internal dependency details
mod http {
use reqwest::Client;
pub struct HttpClient {
inner: Client,
}
impl HttpClient {
pub fn new() -> Self {
HttpClient {
inner: Client::new(),
}
}
}
impl Default for HttpClient {
fn default() -> Self {
Self::new()
}
}
}
// Use dependency types internally, expose your own types
pub mod api {
use serde::{Deserialize, Serialize};
/// Your public type, not serde's
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: u64,
pub name: String,
}
}
# .cargo/config.toml - Project-wide cargo configuration
[build]
# Use all CPU cores
jobs = 0
[net]
# Retry network operations
retry = 3
[registries]
# Private registry configuration
my-company = { index = "https://cargo.mycompany.com/index" }
[source.crates-io]
# Mirror for reliability
replace-with = "mirror"
[source.mirror]
registry = "https://mirror.example.com/crates.io-index"
# .github/workflows/audit.yml - Security auditing
name: Security Audit
on:
push:
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
schedule:
- cron: '0 0 * * *' # Daily
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Run security audit
run: cargo audit
- name: Check for outdated dependencies
run: |
cargo install cargo-outdated
cargo outdated --exit-code 1
// build.rs - Version checking at build time
fn main() {
// Verify minimum rustc version
let version = rustc_version::version().unwrap();
assert!(
version >= rustc_version::Version::new(1, 70, 0),
"Rust 1.70+ required"
);
// Generate version info
println!(
"cargo:rustc-env=BUILD_VERSION={}",
std::env::var("CARGO_PKG_VERSION").unwrap()
);
}
// Conditional compilation based on dependency features
#[cfg(feature = "tracing")]
use tracing::{info, instrument};
pub struct Service {
name: String,
}
impl Service {
#[cfg_attr(feature = "tracing", instrument)]
pub fn process(&self, input: &str) -> String {
#[cfg(feature = "tracing")]
info!("Processing input");
format!("{}: {}", self.name, input)
}
}
#!/bin/bash
# scripts/dep-check.sh - Dependency health check
set -e
echo "=== Dependency Health Check ==="
# Check for security vulnerabilities
echo "Checking security vulnerabilities..."
cargo audit
# Check for outdated dependencies
echo "Checking for outdated dependencies..."
cargo outdated
# Check for unused dependencies
echo "Checking for unused dependencies..."
cargo +nightly udeps
# Check dependency tree for duplicates
echo "Checking for duplicate dependencies..."
cargo tree -d
# Check total dependency count
echo "Dependency statistics:"
cargo tree --depth 0 | wc -l
echo "=== Check Complete ==="
# Add dependency
cargo add tokio --features full
# Add dev dependency
cargo add mockall --dev
# Update to latest compatible
cargo update
# Update specific crate
cargo update -p tokio
# Show dependency tree
cargo tree
# Show duplicates
cargo tree -d
# Show why a crate is included
cargo tree -i ring
# Check for vulnerabilities
cargo audit
# Check for outdated
cargo outdated
| Syntax | Meaning | Example |
|--------|---------|---------|
| 1.2.3 | ^1.2.3 (default) | >= 1.2.3, < 2.0.0 |
| ^1.2.3 | Compatible | >= 1.2.3, < 2.0.0 |
| ~1.2.3 | Patch only | >= 1.2.3, < 1.3.0 |
| =1.2.3 | Exact | = 1.2.3 only |
| >=1.2.3 | Minimum | >= 1.2.3 |
| * | Any | (avoid!) |
# DON'T: Wildcard versions
tokio = "*" # Unpredictable!
# DON'T: Overly restrictive
serde = "=1.0.193" # Can't get security fixes
# DON'T: Git deps in published crates
# my-crate = { git = "..." } # Won't publish!
# DON'T: Enable all features by default
reqwest = { version = "0.11", features = ["full"] }
# DO: Specify only needed features
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
// DON'T: Expose dependency types in your API
pub fn get_client() -> reqwest::Client { } // Leaks dependency!
// DO: Wrap or re-export explicitly
pub struct Client(reqwest::Client);
pub fn get_client() -> Client { }
// Or if intentional, re-export
pub use reqwest::Client; // Explicit re-export
Run this code in the official Rust Playground