Fork Maintenance in Large Rust Codebases

Fork maintenance at scale is a special kind of hell.

I learned this building the OpenZeppelin Polkadot Runtime Templates. The project maintains 8 separate forks of upstream projects and synchronizes polkadot-sdk versions between all of them. When Parity ships a new polkadot-sdk version, I have to update all 8 forks, ensure their dependencies align, and make sure nothing breaks across the matrix.

Eight forks of different upstream projects. Dozens of crates per fork. Transitive dependencies that conflict in subtle ways. Upstream breaking changes that cascade unpredictably. Version alignment across forks becomes a full-time job if you don’t have the right tooling and practices.

This post is about what actually works when you’re maintaining forks at this scale.

Workspace Pattern: Your Foundation for Fork Sanity

Use a Cargo workspace. One root Cargo.toml, multiple member crates.

[workspace]
members = [
    "core",
    "api",
    "cli",
    "utils",
]

Benefits:

  • Single Cargo.lock for the whole project
  • Shared build artifacts
  • Consistent versions across crates

Every large Rust project should be a workspace. No exceptions.

Workspace Dependencies: Single Source of Truth Across Forks

Cargo 1.64 added workspace dependencies. Define versions once at the root:

[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }

Member crates reference them:

[dependencies]
serde = { workspace = true }
tokio = { workspace = true }

One place to update versions. One place to add features. No more hunting through 50 files.

Version Pinning Strategy

Pin major versions loosely. Pin security-sensitive dependencies tightly.

# Loose - minor updates are fine
serde = "1"

# Tight - cryptographic code, want explicit updates
ring = "=0.16.20"

Run cargo update regularly to get compatible updates. Review the diff before committing.

Dealing with Duplicates: The Fork Maintenance Nightmare

Cargo allows multiple versions of the same crate. Sometimes this is necessary. Often it’s waste. When you’re maintaining forks, duplicates multiply your problems.

Check for duplicates:

cargo tree -d

Fix them by:

  1. Updating your direct dependencies
  2. Using [patch] to override transitive dependencies
  3. Asking upstream to update their deps

Duplicate crates mean duplicate compilation. In large projects, this adds up.

Feature Unification

Features are additive across the workspace. If any crate enables a feature, it’s enabled for all.

This causes surprises. Crate A works alone. Add crate B, suddenly A behaves differently because B enabled a feature.

Be explicit about features:

[dependencies]
tokio = { version = "1", default-features = false, features = ["rt"] }

Don’t rely on defaults. State what you need.

Build Times

Large projects compile slowly. Attack this from multiple angles:

Split crates strategically: Put slow-to-compile dependencies in their own crate. Changes to other crates won’t retrigger the slow compilation.

Minimize proc macros: Each proc macro crate compiles serially. Use them sparingly.

Consider sccache: Shared compilation cache. Especially useful in CI.

Profile compilation:

cargo build --timings

Find what’s slow, then decide if you can remove or isolate it.

Security Updates

Use cargo audit in CI. It checks for known vulnerabilities in your dependency tree.

- name: Security audit
  run: cargo audit

When vulnerabilities appear:

  1. Check if the vulnerable code path affects you
  2. Update if you can
  3. Use [patch] if upstream is slow
  4. Document exceptions explicitly

PSVM: Polkadot SDK Version Manager

If you’re working in the Polkadot ecosystem, there’s a tool that makes dependency synchronization dramatically easier: PSVM (Polkadot SDK Version Manager).

PSVM auto-updates your polkadot-sdk dependencies to the correct crates.io versions. It fetches the Plan.toml from release branches and generates the crate-to-version mapping automatically. No more manually hunting down which version of sp-runtime corresponds to which polkadot-sdk release.

It also supports ORML crate updates, which is essential if you’re building on common Substrate pallets outside the core SDK.

For the OpenZeppelin fork management workflow, PSVM reduced what used to be hours of manual version alignment down to a single command. When you’re synchronizing polkadot-sdk versions across 8 different forks, that time savings compounds quickly.

The Meta-Lesson: Fork Maintenance Is a Discipline

Fork maintenance is maintenance work. It’s not glamorous. It doesn’t ship features. Nobody celebrates the engineer who kept 8 forks synchronized with upstream.

But neglected forks become unmergeable. Upstream changes accumulate. Patch conflicts multiply. Eventually you’re so far behind that catching up is a rewrite.

Invest a little time regularly. Sync with upstream monthly. Review the dependency tree quarterly. When upstream ships a breaking release, prioritize the update before your patches drift further.

The alternative is a painful reckoning later. I’ve seen teams abandon forks because the maintenance debt became insurmountable. Don’t be that team.