From 95e961b4a0290965d71555fc13924d2fdac778c6 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Thu, 26 Jun 2025 17:00:49 -0500 Subject: [PATCH 1/2] Add a test showing the changed checksum. --- tests/testsuite/package.rs | 77 ++++++++++++++++++++++++++++++++++++++ tests/testsuite/publish.rs | 74 ++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/tests/testsuite/package.rs b/tests/testsuite/package.rs index c18080a428a..00554864fe6 100644 --- a/tests/testsuite/package.rs +++ b/tests/testsuite/package.rs @@ -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(); +} diff --git a/tests/testsuite/publish.rs b/tests/testsuite/publish.rs index bb280303dcb..1d86d1b1cfa 100644 --- a/tests/testsuite/publish.rs +++ b/tests/testsuite/publish.rs @@ -4378,3 +4378,77 @@ 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_status(101) + .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) +[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(); +} From 340a4f9bbd29b309defe583b800e99f657c27e25 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Thu, 26 Jun 2025 21:14:17 -0500 Subject: [PATCH 2/2] Add a dry-run packaging mode that munges checksums --- src/bin/cargo/commands/package.rs | 1 + src/cargo/core/resolver/resolve.rs | 4 +++ src/cargo/ops/cargo_package/mod.rs | 44 +++++++++++++++++++++++++++--- src/cargo/ops/registry/publish.rs | 1 + tests/testsuite/publish.rs | 28 ++++++++++--------- 5 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/bin/cargo/commands/package.rs b/src/bin/cargo/commands/package.rs index 9eb72f6b6ac..d48f32ca8a2 100644 --- a/src/bin/cargo/commands/package.rs +++ b/src/bin/cargo/commands/package.rs @@ -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, }, )?; diff --git a/src/cargo/core/resolver/resolve.rs b/src/cargo/core/resolver/resolve.rs index 2b532c6548d..a20c5d427f7 100644 --- a/src/cargo/core/resolver/resolve.rs +++ b/src/cargo/core/resolver/resolve.rs @@ -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 } diff --git a/src/cargo/ops/cargo_package/mod.rs b/src/cargo/ops/cargo_package/mod.rs index 1ad40899236..0a2cc6918a5 100644 --- a/src/cargo/ops/cargo_package/mod.rs +++ b/src/cargo/ops/cargo_package/mod.rs @@ -86,6 +86,19 @@ pub struct PackageOpts<'gctx> { pub targets: Vec, pub cli_features: CliFeatures, pub reg_or_index: Option, + /// 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, 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 { 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, @@ -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, _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 { + self.checksums.iter().map(|(p, s)| (*p, s.as_str())) + } } diff --git a/src/cargo/ops/registry/publish.rs b/src/cargo/ops/registry/publish.rs index e3832585a92..e91f6dd41ad 100644 --- a/src/cargo/ops/registry/publish.rs +++ b/src/cargo/ops/registry/publish.rs @@ -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, )?; diff --git a/tests/testsuite/publish.rs b/tests/testsuite/publish.rs index 1d86d1b1cfa..23b4fa02148 100644 --- a/tests/testsuite/publish.rs +++ b/tests/testsuite/publish.rs @@ -4427,7 +4427,6 @@ fn checksum_changed() { p.cargo("publish --dry-run --workspace -Zpackage-workspace") .masquerade_as_nightly_cargo(&["package-workspace"]) .replace_crates_io(registry.index_url()) - .with_status(101) .with_stderr_data(str![[r#" [UPDATING] crates.io index [WARNING] crate dep@1.0.0 already exists on crates.io index @@ -4436,18 +4435,21 @@ See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for [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 +[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();