Description
Problem
When you have a direct dependency on a workspace member and an indirect dependency on a copy from the registry, when publishing they aren't unified and the process fails.
A simplified version of when I ran into this
clap_complete_nushell (local) -> clap (local)
clap_complete_nushell (local) -> nu (dev)
nu -> clap
If the version requirement on clap
is too high to use the registry version, then it fails to resolve because it is locked to the lower version.
The user has to manually publish one package, cargo update
, and then publish the other package.
Originally reported at #1169 (comment)
Steps
As a test case:
#[cargo_test]
fn unify_local_and_regitry_dependency() {
let registry = RegistryBuilder::new().http_api().http_index().build();
Package::new("dep", "1.0.0+registry").publish();
Package::new("transitive", "1.0.0")
.dep("dep", "1.0.0+registry")
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["parent", "dep"]
"#,
)
.file(
"parent/Cargo.toml",
r#"
[package]
name = "parent"
version = "1.0.0"
edition = "2015"
license = "MIT"
description = "foo"
repository = "foo"
[dependencies]
transitive = "1.0.0"
dep = { version = "1.100", path = "../dep" }
"#,
)
.file("parent/src/lib.rs", "")
.file(
"dep/Cargo.toml",
r#"
[package]
name = "dep"
version = "1.100.0+local"
edition = "2015"
license = "MIT"
description = "foo"
repository = "foo"
"#,
)
.file("dep/src/lib.rs", "")
.build();
p.cargo("generate-lockfile").run();
p.cargo("package --workspace -Zpackage-workspace")
.masquerade_as_nightly_cargo(&["package-workspace"])
.replace_crates_io(registry.index_url())
.with_status(101)
.with_stderr_data(str![[r#"
[PACKAGING] dep v1.100.0+local ([ROOT]/foo/dep)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[PACKAGING] parent v1.0.0 ([ROOT]/foo/parent)
[UPDATING] crates.io index
[ERROR] failed to prepare local package for uploading
Caused by:
failed to select a version for `dep`.
... required by package `transitive v1.0.0`
... which satisfies dependency `transitive = "^1.0.0"` (locked to 1.0.0) of package `parent v1.0.0 ([ROOT]/foo/parent)`
versions that meet the requirements `^1.0.0` (locked to 1.0.0+registry) are: 1.0.0+registry
all possible versions conflict with previously selected packages.
previously selected package `dep v1.100.0+local`
... which satisfies dependency `dep = "^1.100"` of package `parent v1.0.0 ([ROOT]/foo/parent)`
failed to select a version for `dep` which could resolve this conflict
"#]])
.run();
p.cargo("publish --package dep")
.replace_crates_io(registry.index_url())
.with_stderr_data(str![[r#"
[UPDATING] crates.io index
[PACKAGING] dep v1.100.0+local ([ROOT]/foo/dep)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[VERIFYING] dep v1.100.0+local ([ROOT]/foo/dep)
[COMPILING] dep v1.100.0+local ([ROOT]/foo/target/package/dep-1.100.0+local)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
[UPLOADING] dep v1.100.0+local ([ROOT]/foo/dep)
[UPLOADED] dep v1.100.0+local to registry `crates-io`
[NOTE] waiting for `dep v1.100.0+local` to be available at registry `crates-io`.
You may press ctrl-c to skip waiting; the crate should be available shortly.
[PUBLISHED] dep v1.100.0+local at registry `crates-io`
"#]])
.run();
p.cargo("package --package parent")
.replace_crates_io(registry.index_url())
.with_status(101)
.with_stderr_data(str![[r#"
[PACKAGING] parent v1.0.0 ([ROOT]/foo/parent)
[UPDATING] crates.io index
[ERROR] failed to prepare local package for uploading
Caused by:
failed to select a version for `dep`.
... required by package `transitive v1.0.0`
... which satisfies dependency `transitive = "^1.0.0"` (locked to 1.0.0) of package `parent v1.0.0 ([ROOT]/foo/parent)`
versions that meet the requirements `^1.0.0` (locked to 1.0.0+registry) are: 1.0.0+registry
all possible versions conflict with previously selected packages.
previously selected package `dep v1.100.0+local`
... which satisfies dependency `dep = "^1.100"` of package `parent v1.0.0 ([ROOT]/foo/parent)`
failed to select a version for `dep` which could resolve this conflict
"#]])
.run();
p.cargo("update dep@1.0").run();
p.cargo("package --package parent")
.replace_crates_io(registry.index_url())
.with_stderr_data(str![[r#"
[PACKAGING] parent v1.0.0 ([ROOT]/foo/parent)
[UPDATING] crates.io index
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[VERIFYING] parent v1.0.0 ([ROOT]/foo/parent)
[DOWNLOADING] crates ...
[DOWNLOADED] dep v1.100.0+local
[DOWNLOADED] transitive v1.0.0
[COMPILING] dep v1.100.0+local
[COMPILING] transitive v1.0.0
[COMPILING] parent v1.0.0 ([ROOT]/foo/target/package/parent-1.0.0)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
"#]])
.run();
}
Note: cargo package --exclude-lockfile
allows the package to be published
Possible Solution(s)
Update the lockfile in the same way that a hand edit to a Cargo.toml
would work: updates that one package and nothing else. Even better if we treated it as a --precise
call
Notes
No response
Version
❯ cargo -V --verbose
cargo 1.87.0 (99624be96 2025-05-06)
release: 1.87.0
commit-hash: 99624be96e9d213b0e9b1e36451271f24e4a41d8
commit-date: 2025-05-06
host: x86_64-unknown-linux-gnu
libgit2: 1.9.0 (sys:0.20.0 vendored)
libcurl: 8.12.1-DEV (sys:0.4.80+curl-8.12.1 vendored ssl:OpenSSL/3.4.1)
ssl: OpenSSL 3.4.1 11 Feb 2025
os: Pop!_OS 22.4.0 (jammy) [64-bit]