Rust

Multi-Crate Dependency Management

Strategies for managing dependencies across a family of related Rust crates — version bumps, cascade order, and consistency checks.

The Problem

When you maintain multiple crates that depend on each other, a version bump in one crate cascades through all downstream consumers. Without a system, you end up with version mismatches, stale lockfiles, and broken publishes.

rfconversions  ← foundational (no internal deps)
  ├── touchstone (depends on rfconversions)
  ├── gainlineup (depends on rfconversions + touchstone)
  └── linkbudget (depends on rfconversions)

Bumping rfconversions means updating three downstream crates. Miss one and your users hit version conflicts.

Publish Order Matters

Always publish bottom-up through the dependency graph:

  1. Foundation crate — no internal dependencies
  2. Mid-level crates — depend only on the foundation
  3. Top-level crates — depend on multiple lower crates
Terminal
# 1. Publish the foundation
cd rfconversions && cargo publish

# 2. Wait for crates.io index to update (~60 seconds)
sleep 60

# 3. Update downstream Cargo.toml files, then publish each
cd ../touchstone && cargo publish
cd ../gainlineup && cargo publish
cd ../linkbudget && cargo publish
crates.io has a short index propagation delay. If you publish too fast, downstream crates may fail to resolve the new version. Wait about a minute between levels.

Bump Strategy

When you bump a dependency, the consuming crate needs its own version bump too — even if its code didn't change. Your users need a new version to pick up the updated transitive dependency.

Upstream ChangeDownstream BumpRationale
Patch (bug fix)PatchTransitive fix, no new API surface
Minor (new feature)Patch or MinorPatch if you don't re-export; Minor if you expose new API
Major (breaking)MajorBreaking changes propagate

For pre-1.0 crates, be pragmatic — a patch bump for dependency updates is usually fine.

Version Consistency Script

Automate mismatch detection across your workspace:

Terminal
#!/bin/bash
# dep-check.sh — find version mismatches across crates
CRATES="rfconversions touchstone gainlineup linkbudget"

for crate in $CRATES; do
  LOCAL=$(grep "^version" "$crate/Cargo.toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
  echo "📦 $crate v$LOCAL"

  # Check what version other crates depend on
  for consumer in $CRATES; do
    [ "$consumer" = "$crate" ] && continue
    DEP_VER=$(grep "^${crate}" "$consumer/Cargo.toml" 2>/dev/null \
      | sed 's/.*"\(.*\)"/\1/')
    [ -z "$DEP_VER" ] && continue
    if [ "$DEP_VER" != "$LOCAL" ]; then
      echo "  ⚠️  $consumer depends on $crate $DEP_VER (stale)"
    else
      echo "$consumer$DEP_VER"
    fi
  done
done

Run this before every release cycle to catch drift early.

Cargo.lock Hygiene

Each crate has its own Cargo.lock. After bumping a dependency:

Terminal
# Regenerate the lockfile
cargo check

# Verify it resolves correctly
cargo tree | grep <dependency-name>

# Commit the updated lockfile
git add Cargo.lock Cargo.toml
git commit -m "bump <dependency> to v<new>"
Never use cargo update blindly — it updates all dependencies to latest semver-compatible versions. Use cargo update -p <crate> to target a specific dependency.

Workspace vs Separate Repos

Cargo workspaces ([workspace] in a root Cargo.toml) solve many of these problems automatically — shared Cargo.lock, unified cargo test, path dependencies during development. But they force all crates into one repo.

Separate repos work better when:

  • Crates have different release cadences
  • You want independent CI pipelines
  • Crates serve different audiences
  • You prefer smaller, focused repos

Workspaces work better when:

  • Crates are tightly coupled and always release together
  • You want atomic cross-crate refactors
  • Single CI pipeline is acceptable

For loosely-coupled crate families (like RF engineering tools), separate repos with a consistency script is a good middle ground.

Release Day Checklist

When publishing a cascade of updates:

  1. Run consistency check — identify all stale dependencies
  2. Publish foundation crate — tests pass, dry run, publish, GitHub release
  3. Wait for index — 60 seconds minimum
  4. Bump downstream crates — update Cargo.toml, cargo check, run tests
  5. Publish each downstream — in dependency order
  6. Verifycargo search <crate> shows new versions
  7. Re-run consistency check — should show all green

Path Dependencies for Development

While developing across crates locally, use path overrides to test changes before publishing:

Cargo.toml
# Temporary: point to local checkout
[dependencies]
rfconversions = { path = "../rfconversions" }

# Release: switch back to crates.io version
[dependencies]
rfconversions = "0.7.2"
Never publish with path dependencies — cargo publish will reject them. Always switch back to version strings before publishing.

Automating the Cascade

For frequent releases, a shell script can orchestrate the full cascade:

Terminal
#!/bin/bash
set -e

FOUNDATION="rfconversions"
DOWNSTREAM=("touchstone" "gainlineup" "linkbudget")
NEW_VERSION=$1  # e.g., "0.7.2"

# Publish foundation
cd "$FOUNDATION"
cargo test && cargo publish
cd ..
echo "⏳ Waiting for crates.io index..."
sleep 90

# Update and publish each downstream crate
for crate in "${DOWNSTREAM[@]}"; do
  cd "$crate"
  # Update dependency version in Cargo.toml
  sed -i '' "s/${FOUNDATION} = \".*\"/${FOUNDATION} = \"${NEW_VERSION}\"/" Cargo.toml
  cargo check && cargo test
  # Bump patch version
  cargo publish
  cd ..
  sleep 60
done

echo "✅ All crates published"

This is a starting point — adjust for your exact dependency graph and versioning policy.