Mehdi Akiki
Rust Failure Atlas / Cargo and dependencies

RFA-104 · Case file with fixtures · Case 76 of 694 · Cargo workspace evidence

How Cargo's links Conflict Reveals Two Owners of One Native Library

The Cargo links key gives one package ownership of one native library in a final dependency graph. Align the sys crate and let wrappers share it; do not hide duplicate native ownership under renamed packages.

Reviewed
Rust
Rust 1.98.1, Cargo 1.98.1
Targets
native targets using the linked library
Profiles
check, dev, release, test

Direct answer

What this Rust failure means

Why it happens
Two sys crates independently claim ownership of one native library, which could produce competing link directives and incompatible ABIs.
First discriminating check
Find both packages declaring the repeated links value and trace the inverse dependency path that introduces each one.

Two Rust packages can often exist at two versions in one graph. Native libraries make this less simple. When two packages both declare links = "rfa_native", Cargo rejects the graph before rustc compiles the application.

The failing workspace contains two different sys crates, each claiming the same links value. Cargo 1.98.1 reports that it is attempting to resolve more than one crate with links=rfa_native.

The links manifest documentation says the value names the native library being linked. A package with this key must have a build script. Its build script can emit link search paths, library names, cfg values, and metadata for immediate dependents.

Cargo permits only one package with a given links value in a dependency graph. The rule prevents two build scripts from independently configuring what is supposed to be one native library. Without it, the final link could mix headers, versions, search paths, or static archives in an order-dependent way.

The package names can be different. Renaming dependency aliases does not help. The conflict concerns the native ownership key, not the Rust import spelling.

Why normal version duplication is unsafe here

Imagine wrapper A expects native library version 1 and wrapper B expects version 2. Loading both may be valid if the operating system and symbol versioning support that exact arrangement. It may also bind both wrappers to one set of unversioned symbols, producing ABI mismatch without a Rust type error.

Cargo chooses the conservative contract. The resolver documentation for links treats this uniqueness while constructing the graph rather than waiting for a platform linker to produce a less helpful result.

This error therefore contains architecture information: two parts of the application disagree about who configures one native dependency.

Share one sys crate when it is one library

The repaired workspace has one rfa-native-sys package. Both safe wrappers depend on it. One build script owns discovery and linkage; the wrappers own different Rust APIs.

In a real project I align the sys-crate versions through upgrades, patches, or coordinated releases. Then I verify that both high-level crates accept the shared ABI version. Forcing the graph to resolve is not enough if one wrapper assumes functions or struct layouts absent from that native version.

If the native libraries truly are separate, they need distinct link identities and normally distinct symbols. Merely changing one links string to silence Cargo is dishonest when both still link the same unversioned library.

Inspect beyond cargo tree

I start with inverse dependency trees for both sys crates. Then I inspect:

  • package versions and sources;
  • each links value;
  • build script outputs with verbose Cargo logging;
  • static versus dynamic linking;
  • environment variables used for discovery;
  • native ABI and header versions;
  • target-specific dependencies.

On CI, pkg-config, vendored builds, and system packages may select different native installations. A graph that resolves locally can still link a different ABI elsewhere. I record the native version in build logs when this is operationally important.

Patches have a narrow role

A [patch] section can redirect a package source or test an unreleased compatible change. It cannot prove two sys crates are ABI-compatible. I use it when I understand the version requirements and own the compatibility decision, not as a random resolver lever.

Similarly, deleting the lockfile may choose a shared sys version if requirements permit it. That can be a useful confirmation, but the resulting dependency changes must be reviewed. The core outcome is one agreed owner, not simply a fresh lockfile.

My debugging sequence

When Cargo reports a links conflict, I use this order:

  1. Extract the repeated links value from the message.
  2. Find every package declaring it and every inverse path that brings those packages in.
  3. Determine whether they represent one native ABI or intentionally distinct libraries.
  4. Align high-level dependencies around one compatible sys crate when it is one ABI.
  5. Validate headers, symbols, and native versions on supported targets.
  6. Keep one build script responsible for link directives and metadata.

This is not Cargo being needlessly strict. It is stopping two build scripts from making competing claims about one native namespace. Solving that ownership conflict before the linker runs gives me a dependency graph I can explain and reproduce.