Override Cargo.lock checksums when doing a dry-run publish (#15711)

Fixes #15647.

When dry-run publishing workspace without bumping versions first, the
package-verification step would fail because it would see checksum
mismatches between the old lock file (that saw index deps) and the new
lock file where those index deps got replaced by local packages with the
same version.

In this PR, the packaging step modifies the old lock file's checksums
before re-resolving, but only in dry-run mode.
This commit is contained in:
Ed Page 2025-06-30 20:20:41 +00:00 committed by GitHub
commit f013ef54bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 199 additions and 4 deletions

View File

@ -108,6 +108,7 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
keep_going: args.keep_going(),
cli_features: args.cli_features()?,
reg_or_index,
dry_run: false,
},
)?;

View File

@ -390,6 +390,10 @@ unable to verify that `{0}` is the same as when the lockfile was generated
&self.checksums
}
pub fn set_checksum(&mut self, pkg_id: PackageId, checksum: String) {
self.checksums.insert(pkg_id, Some(checksum));
}
pub fn metadata(&self) -> &Metadata {
&self.metadata
}

View File

@ -86,6 +86,19 @@ pub struct PackageOpts<'gctx> {
pub targets: Vec<String>,
pub cli_features: CliFeatures,
pub reg_or_index: Option<ops::RegistryOrIndex>,
/// Whether this packaging job is meant for a publishing dry-run.
///
/// Packaging on its own has no side effects, so a dry-run doesn't
/// make sense from that point of view. But dry-run publishing needs
/// special packaging behavior, which this flag turns on.
///
/// Specifically, we want dry-run packaging to work even if versions
/// have not yet been bumped. But then if you dry-run packaging in
/// a workspace with some declared versions that are already published,
/// the package verification step can fail with checksum mismatches.
/// So when dry-run is true, the verification step does some extra
/// checksum fudging in the lock file.
pub dry_run: bool,
}
const ORIGINAL_MANIFEST_FILE: &str = "Cargo.toml.orig";
@ -125,6 +138,7 @@ enum GeneratedFile {
#[tracing::instrument(skip_all)]
fn create_package(
ws: &Workspace<'_>,
opts: &PackageOpts<'_>,
pkg: &Package,
ar_files: Vec<ArchiveFile>,
local_reg: Option<&TmpRegistry<'_>>,
@ -159,7 +173,7 @@ fn create_package(
gctx.shell()
.status("Packaging", pkg.package_id().to_string())?;
dst.file().set_len(0)?;
let uncompressed_size = tar(ws, pkg, local_reg, ar_files, dst.file(), &filename)
let uncompressed_size = tar(ws, opts, pkg, local_reg, ar_files, dst.file(), &filename)
.context("failed to prepare local package for uploading")?;
dst.seek(SeekFrom::Start(0))?;
@ -311,7 +325,7 @@ fn do_package<'a>(
}
}
} else {
let tarball = create_package(ws, &pkg, ar_files, local_reg.as_ref())?;
let tarball = create_package(ws, &opts, &pkg, ar_files, local_reg.as_ref())?;
if let Some(local_reg) = local_reg.as_mut() {
if pkg.publish() != &Some(Vec::new()) {
local_reg.add_package(ws, &pkg, &tarball)?;
@ -720,11 +734,12 @@ fn error_custom_build_file_not_in_package(
/// Construct `Cargo.lock` for the package to be published.
fn build_lock(
ws: &Workspace<'_>,
opts: &PackageOpts<'_>,
publish_pkg: &Package,
local_reg: Option<&TmpRegistry<'_>>,
) -> CargoResult<String> {
let gctx = ws.gctx();
let orig_resolve = ops::load_pkg_lockfile(ws)?;
let mut orig_resolve = ops::load_pkg_lockfile(ws)?;
let mut tmp_ws = Workspace::ephemeral(publish_pkg.clone(), ws.gctx(), None, true)?;
@ -736,6 +751,18 @@ fn build_lock(
local_reg.upstream,
local_reg.root.as_path_unlocked().to_owned(),
);
if opts.dry_run {
if let Some(orig_resolve) = orig_resolve.as_mut() {
let upstream_in_lock = if local_reg.upstream.is_crates_io() {
SourceId::crates_io(gctx)?
} else {
local_reg.upstream
};
for (p, s) in local_reg.checksums() {
orig_resolve.set_checksum(p.with_source_id(upstream_in_lock), s.to_owned());
}
}
}
}
let mut tmp_reg = tmp_ws.package_registry()?;
@ -811,6 +838,7 @@ fn check_metadata(pkg: &Package, gctx: &GlobalContext) -> CargoResult<()> {
/// Returns the uncompressed size of the contents of the new archive file.
fn tar(
ws: &Workspace<'_>,
opts: &PackageOpts<'_>,
pkg: &Package,
local_reg: Option<&TmpRegistry<'_>>,
ar_files: Vec<ArchiveFile>,
@ -868,7 +896,7 @@ fn tar(
GeneratedFile::Manifest(_) => {
publish_pkg.manifest().to_normalized_contents()?
}
GeneratedFile::Lockfile(_) => build_lock(ws, &publish_pkg, local_reg)?,
GeneratedFile::Lockfile(_) => build_lock(ws, opts, &publish_pkg, local_reg)?,
GeneratedFile::VcsInfo(ref s) => serde_json::to_string_pretty(s)?,
};
header.set_entry_type(EntryType::file());
@ -1062,6 +1090,7 @@ struct TmpRegistry<'a> {
gctx: &'a GlobalContext,
upstream: SourceId,
root: Filesystem,
checksums: HashMap<PackageId, String>,
_lock: FileLock,
}
@ -1073,6 +1102,7 @@ impl<'a> TmpRegistry<'a> {
gctx,
root,
upstream,
checksums: HashMap::new(),
_lock,
};
// If there's an old temporary registry, delete it.
@ -1118,6 +1148,8 @@ impl<'a> TmpRegistry<'a> {
.update_file(tar.file())?
.finish_hex();
self.checksums.insert(package.package_id(), cksum.clone());
let deps: Vec<_> = new_crate
.deps
.into_iter()
@ -1178,4 +1210,8 @@ impl<'a> TmpRegistry<'a> {
dst.write_all(index_line.as_bytes())?;
Ok(())
}
fn checksums(&self) -> impl Iterator<Item = (PackageId, &str)> {
self.checksums.iter().map(|(p, s)| (*p, s.as_str()))
}
}

View File

@ -202,6 +202,7 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
keep_going: opts.keep_going,
cli_features: opts.cli_features.clone(),
reg_or_index: reg_or_index.clone(),
dry_run: opts.dry_run,
},
pkgs,
)?;

View File

@ -8082,3 +8082,80 @@ fn unpublished_dependency() {
(),
);
}
// This is a companion to `publish::checksum_changed`, but because this one
// is packaging without dry-run, it should fail.
#[cargo_test]
fn checksum_changed() {
let registry = registry::RegistryBuilder::new()
.http_api()
.http_index()
.build();
Package::new("dep", "1.0.0").publish();
Package::new("transitive", "1.0.0")
.dep("dep", "1.0.0")
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["dep"]
[package]
name = "foo"
version = "0.0.1"
edition = "2015"
authors = []
license = "MIT"
description = "foo"
documentation = "foo"
[dependencies]
dep = { path = "./dep", version = "1.0.0" }
transitive = "1.0.0"
"#,
)
.file("src/lib.rs", "")
.file(
"dep/Cargo.toml",
r#"
[package]
name = "dep"
version = "1.0.0"
edition = "2015"
"#,
)
.file("dep/src/lib.rs", "")
.build();
p.cargo("check").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#"
[WARNING] manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
[PACKAGING] dep v1.0.0 ([ROOT]/foo/dep)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[PACKAGING] foo v0.0.1 ([ROOT]/foo)
[ERROR] failed to prepare local package for uploading
Caused by:
checksum for `dep v1.0.0` changed between lock files
this could be indicative of a few possible errors:
* the lock file is corrupt
* a replacement source in use (e.g., a mirror) returned a different checksum
* the source itself may be corrupt in one way or another
unable to verify that `dep v1.0.0` is the same as when the lockfile was generated
"#]])
.run();
}

View File

@ -4378,3 +4378,79 @@ fn all_unpublishable_packages() {
"#]])
.run();
}
#[cargo_test]
fn checksum_changed() {
let registry = RegistryBuilder::new().http_api().http_index().build();
Package::new("dep", "1.0.0").publish();
Package::new("transitive", "1.0.0")
.dep("dep", "1.0.0")
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["dep"]
[package]
name = "foo"
version = "0.0.1"
edition = "2015"
authors = []
license = "MIT"
description = "foo"
documentation = "foo"
[dependencies]
dep = { path = "./dep", version = "1.0.0" }
transitive = "1.0.0"
"#,
)
.file("src/lib.rs", "")
.file(
"dep/Cargo.toml",
r#"
[package]
name = "dep"
version = "1.0.0"
edition = "2015"
"#,
)
.file("dep/src/lib.rs", "")
.build();
p.cargo("check").run();
p.cargo("publish --dry-run --workspace -Zpackage-workspace")
.masquerade_as_nightly_cargo(&["package-workspace"])
.replace_crates_io(registry.index_url())
.with_stderr_data(str![[r#"
[UPDATING] crates.io index
[WARNING] crate dep@1.0.0 already exists on crates.io index
[WARNING] manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
[PACKAGING] dep v1.0.0 ([ROOT]/foo/dep)
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[PACKAGING] foo v0.0.1 ([ROOT]/foo)
[UPDATING] crates.io index
[PACKAGED] 4 files, [FILE_SIZE]B ([FILE_SIZE]B compressed)
[VERIFYING] dep v1.0.0 ([ROOT]/foo/dep)
[COMPILING] dep v1.0.0 ([ROOT]/foo/target/package/dep-1.0.0)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
[VERIFYING] foo v0.0.1 ([ROOT]/foo)
[UNPACKING] dep v1.0.0 (registry `[ROOT]/foo/target/package/tmp-registry`)
[COMPILING] dep v1.0.0
[COMPILING] transitive v1.0.0
[COMPILING] foo v0.0.1 ([ROOT]/foo/target/package/foo-0.0.1)
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
[UPLOADING] dep v1.0.0 ([ROOT]/foo/dep)
[WARNING] aborting upload due to dry run
[UPLOADING] foo v0.0.1 ([ROOT]/foo)
[WARNING] aborting upload due to dry run
"#]])
.run();
}