Skip to content

Commit 1b3fded

Browse files
committed
fix: canonicalize manifest labels in lockfile digests
Ignore repository names when hashing splicing metadata so cargo-bazel lockfiles remain portable between root and dependency contexts. Reject canonicalization collisions explicitly and cover both behaviors with unit tests.
1 parent f5b7135 commit 1b3fded

6 files changed

Lines changed: 237 additions & 19 deletions

File tree

crate_universe/src/lockfile.rs

Lines changed: 232 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ use std::process::Command;
88

99
use anyhow::{bail, Context as AnyhowContext, Result};
1010
use hex::ToHex;
11-
use serde::{Deserialize, Serialize};
11+
use serde::ser::SerializeMap;
12+
use serde::{Deserialize, Serialize, Serializer};
1213
use sha2::{Digest as Sha2Digest, Sha256};
1314

1415
use crate::config::Config;
1516
use crate::context::Context;
1617
use crate::metadata::Cargo;
1718
use crate::splicing::{SplicingManifest, SplicingMetadata};
19+
use crate::utils::starlark::{Label, Repository};
1820

1921
pub(crate) fn lock_context(
2022
mut context: Context,
@@ -81,15 +83,15 @@ impl Digest {
8183
cargo_bazel_version,
8284
&cargo_version,
8385
&rustc_version,
84-
),
86+
)?,
8587
None => Self::compute(
8688
context,
8789
config,
8890
&splicing_metadata,
8991
cargo_bazel_version,
9092
&cargo_version,
9193
&rustc_version,
92-
),
94+
)?,
9395
})
9496
}
9597

@@ -110,10 +112,9 @@ impl Digest {
110112
cargo_bazel_version: &str,
111113
cargo_version: &str,
112114
rustc_version: &str,
113-
) -> Self {
115+
) -> Result<Self> {
114116
// Since this method is private, it should be expected that context is
115-
// always None. This then allows us to have this method not return a
116-
// Result.
117+
// always checksum-free.
117118
debug_assert!(context.checksum.is_none());
118119

119120
let mut hasher = Sha256::new();
@@ -140,8 +141,15 @@ impl Digest {
140141

141142
// Data collected about Cargo manifests and configs that feed into dependency generation. This file
142143
// is also generated by Bazel behind the scenes based on user inputs.
144+
//
145+
// Manifest labels can differ between root and dependency contexts
146+
// (`//pkg:Cargo.toml` vs `@@repo+//pkg:Cargo.toml`) even when the
147+
// Cargo inputs are otherwise identical. Canonicalize labels only for
148+
// hashing so a checked-in cargo-bazel lockfile stays portable across
149+
// those contexts without changing runtime splicing behavior.
150+
let digest_splicing_metadata = DigestSplicingMetadata::new(splicing_metadata)?;
143151
hasher.update(Digest::compute_single_hash(
144-
&serde_json::to_string(splicing_metadata).unwrap(),
152+
&serde_json::to_string(&digest_splicing_metadata).unwrap(),
145153
"splicing manifest",
146154
));
147155
hasher.update(b"\0");
@@ -155,7 +163,7 @@ impl Digest {
155163
let hash = hasher.finalize().encode_hex::<String>();
156164
tracing::debug!("Digest hash: {}", hash);
157165

158-
Self(hash)
166+
Ok(Self(hash))
159167
}
160168

161169
pub(crate) fn bin_version(binary: &Path) -> Result<String> {
@@ -209,6 +217,82 @@ impl Digest {
209217
}
210218
}
211219

220+
#[derive(Serialize)]
221+
struct DigestSplicingMetadata<'a> {
222+
direct_packages: &'a BTreeMap<String, cargo_toml::DependencyDetail>,
223+
// Keep the serialized shape aligned with SplicingMetadata so existing
224+
// digests remain stable when no manifest labels require canonicalization.
225+
// If multiple manifests collapse to the same repo-neutral label, serialize
226+
// them as a stable list for that key instead of discarding any entries.
227+
manifests: DigestManifestMap<'a>,
228+
cargo_config: &'a Option<crate::splicing::cargo_config::CargoConfig>,
229+
}
230+
231+
impl<'a> DigestSplicingMetadata<'a> {
232+
fn new(splicing_metadata: &'a SplicingMetadata) -> Result<Self> {
233+
let mut manifests: BTreeMap<Label, Vec<&'a cargo_toml::Manifest>> = BTreeMap::new();
234+
235+
for (label, manifest) in &splicing_metadata.manifests {
236+
// Repository identity is not semantically relevant to the Cargo
237+
// graph encoded by the lockfile, but package/target paths still
238+
// are, so only drop the repo portion here.
239+
let canonical_label = canonical_manifest_label(label);
240+
manifests.entry(canonical_label).or_default().push(manifest);
241+
}
242+
243+
Ok(Self {
244+
direct_packages: &splicing_metadata.direct_packages,
245+
manifests: DigestManifestMap(manifests),
246+
cargo_config: &splicing_metadata.cargo_config,
247+
})
248+
}
249+
}
250+
251+
struct DigestManifestMap<'a>(BTreeMap<Label, Vec<&'a cargo_toml::Manifest>>);
252+
253+
impl Serialize for DigestManifestMap<'_> {
254+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
255+
where
256+
S: Serializer,
257+
{
258+
let mut map = serializer.serialize_map(Some(self.0.len()))?;
259+
260+
for (label, manifests) in &self.0 {
261+
if manifests.len() == 1 {
262+
map.serialize_entry(label, manifests[0])?;
263+
continue;
264+
}
265+
266+
let mut sorted_manifests = manifests.clone();
267+
sorted_manifests.sort_by(|left, right| {
268+
serde_json::to_string(left)
269+
.unwrap()
270+
.cmp(&serde_json::to_string(right).unwrap())
271+
});
272+
map.serialize_entry(label, &sorted_manifests)?;
273+
}
274+
275+
map.end()
276+
}
277+
}
278+
279+
fn canonical_manifest_label(label: &Label) -> Label {
280+
match label {
281+
Label::Relative { target } => Label::Relative {
282+
target: target.clone(),
283+
},
284+
Label::Absolute {
285+
package, target, ..
286+
} => Label::Absolute {
287+
// Preserve package and target so distinct workspace/package paths
288+
// still hash differently; only canonical repo names are removed.
289+
repository: Repository::Local,
290+
package: package.clone(),
291+
target: target.clone(),
292+
},
293+
}
294+
}
295+
212296
impl PartialEq<str> for Digest {
213297
fn eq(&self, other: &str) -> bool {
214298
self.0 == other
@@ -225,11 +309,13 @@ impl PartialEq<String> for Digest {
225309
mod test {
226310
use crate::config::{CrateAnnotations, CrateNameAndVersionReq};
227311
use crate::splicing::cargo_config::{AdditionalRegistry, CargoConfig, Registry};
312+
use crate::utils::starlark::Label;
228313
use crate::utils::target_triple::TargetTriple;
229314

230315
use super::*;
231316

232317
use std::collections::BTreeSet;
318+
use std::str::FromStr;
233319

234320
#[test]
235321
fn simple_digest() {
@@ -244,7 +330,8 @@ mod test {
244330
"0.1.0",
245331
"cargo 1.57.0 (b2e52d7ca 2021-10-21)",
246332
"rustc 1.57.0 (f1edd0429 2021-11-29)",
247-
);
333+
)
334+
.unwrap();
248335

249336
assert_eq!(
250337
Digest("edd73970897c01af3bb0e6c9d62f572203dd38a03c189dcca555d463990aa086".to_owned()),
@@ -289,7 +376,8 @@ mod test {
289376
"0.1.0",
290377
"cargo 1.57.0 (b2e52d7ca 2021-10-21)",
291378
"rustc 1.57.0 (f1edd0429 2021-11-29)",
292-
);
379+
)
380+
.unwrap();
293381

294382
assert_eq!(
295383
Digest("8a4c1b3bb4c2d6c36e27565e71a13d54cff9490696a492c66a3a37bdd3893edf".to_owned()),
@@ -320,7 +408,8 @@ mod test {
320408
"0.1.0",
321409
"cargo 1.57.0 (b2e52d7ca 2021-10-21)",
322410
"rustc 1.57.0 (f1edd0429 2021-11-29)",
323-
);
411+
)
412+
.unwrap();
324413

325414
assert_eq!(
326415
Digest("1e01331686ba1f26f707dc098cd9d21c39d6ccd8e46be03329bb2470d3833e15".to_owned()),
@@ -369,14 +458,141 @@ mod test {
369458
"0.1.0",
370459
"cargo 1.57.0 (b2e52d7ca 2021-10-21)",
371460
"rustc 1.57.0 (f1edd0429 2021-11-29)",
372-
);
461+
)
462+
.unwrap();
373463

374464
assert_eq!(
375465
Digest("45ccf7109db2d274420fac521f4736a1fb55450ec60e6df698e1be4dc2c89fad".to_owned()),
376466
digest,
377467
);
378468
}
379469

470+
#[test]
471+
fn digest_ignores_manifest_repository_names() {
472+
let context = Context::default();
473+
let config = Config::default();
474+
let manifest = cargo_toml::Manifest::from_str(
475+
r#"
476+
[package]
477+
name = "lockfile-repro"
478+
version = "0.0.1"
479+
edition = "2021"
480+
"#,
481+
)
482+
.unwrap();
483+
484+
// Same manifest content, but one label is evaluated from the root
485+
// workspace and the other from a dependent module's external repo.
486+
let local = SplicingMetadata {
487+
manifests: BTreeMap::from([(
488+
Label::from_str("//:Cargo.toml").unwrap(),
489+
manifest.clone(),
490+
)]),
491+
..SplicingMetadata::default()
492+
};
493+
let dependency = SplicingMetadata {
494+
manifests: BTreeMap::from([(
495+
Label::from_str("@@published_ruleset+//:Cargo.toml").unwrap(),
496+
manifest,
497+
)]),
498+
..SplicingMetadata::default()
499+
};
500+
501+
let local_digest = Digest::compute(
502+
&context,
503+
&config,
504+
&local,
505+
"0.1.0",
506+
"cargo 1.57.0 (b2e52d7ca 2021-10-21)",
507+
"rustc 1.57.0 (f1edd0429 2021-11-29)",
508+
)
509+
.unwrap();
510+
511+
let dependency_digest = Digest::compute(
512+
&context,
513+
&config,
514+
&dependency,
515+
"0.1.0",
516+
"cargo 1.57.0 (b2e52d7ca 2021-10-21)",
517+
"rustc 1.57.0 (f1edd0429 2021-11-29)",
518+
)
519+
.unwrap();
520+
521+
assert_eq!(local_digest, dependency_digest);
522+
}
523+
524+
#[test]
525+
fn digest_stable_with_manifest_label_collisions() {
526+
let context = Context::default();
527+
let config = Config::default();
528+
let first_manifest = cargo_toml::Manifest::from_str(
529+
r#"
530+
[package]
531+
name = "collision-check-a"
532+
version = "0.0.1"
533+
edition = "2021"
534+
"#,
535+
)
536+
.unwrap();
537+
let second_manifest = cargo_toml::Manifest::from_str(
538+
r#"
539+
[package]
540+
name = "collision-check-b"
541+
version = "0.0.2"
542+
edition = "2021"
543+
"#,
544+
)
545+
.unwrap();
546+
547+
let splicing_metadata = SplicingMetadata {
548+
manifests: BTreeMap::from([
549+
(
550+
Label::from_str("@first_workspace//pkg:Cargo.toml").unwrap(),
551+
first_manifest.clone(),
552+
),
553+
(
554+
Label::from_str("@@second_workspace+//pkg:Cargo.toml").unwrap(),
555+
second_manifest.clone(),
556+
),
557+
]),
558+
..SplicingMetadata::default()
559+
};
560+
let reversed_splicing_metadata = SplicingMetadata {
561+
manifests: BTreeMap::from([
562+
(
563+
Label::from_str("@@second_workspace+//pkg:Cargo.toml").unwrap(),
564+
second_manifest,
565+
),
566+
(
567+
Label::from_str("@first_workspace//pkg:Cargo.toml").unwrap(),
568+
first_manifest,
569+
),
570+
]),
571+
..SplicingMetadata::default()
572+
};
573+
574+
let digest = Digest::compute(
575+
&context,
576+
&config,
577+
&splicing_metadata,
578+
"0.1.0",
579+
"cargo 1.57.0 (b2e52d7ca 2021-10-21)",
580+
"rustc 1.57.0 (f1edd0429 2021-11-29)",
581+
)
582+
.unwrap();
583+
let reversed_digest = Digest::compute(
584+
&context,
585+
&config,
586+
&reversed_splicing_metadata,
587+
"0.1.0",
588+
"cargo 1.57.0 (b2e52d7ca 2021-10-21)",
589+
"rustc 1.57.0 (f1edd0429 2021-11-29)",
590+
)
591+
.unwrap();
592+
593+
assert_eq!(digest, reversed_digest);
594+
}
595+
380596
#[test]
381597
fn digest_stable_with_crlf_cargo_config() {
382598
let context = Context::default();
@@ -413,7 +629,8 @@ mod test {
413629
"0.1.0",
414630
"cargo 1.57.0 (b2e52d7ca 2021-10-21)",
415631
"rustc 1.57.0 (f1edd0429 2021-11-29)",
416-
);
632+
)
633+
.unwrap();
417634

418635
let digest_lf = Digest::compute(
419636
&context,
@@ -422,7 +639,8 @@ mod test {
422639
"0.1.0",
423640
"cargo 1.57.0 (b2e52d7ca 2021-10-21)",
424641
"rustc 1.57.0 (f1edd0429 2021-11-29)",
425-
);
642+
)
643+
.unwrap();
426644

427645
assert_eq!(
428646
digest_crlf, digest_lf,

examples/crate_universe/cargo_aliases/cargo-bazel-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/crate_universe/cargo_conditional_deps/cargo-bazel-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/crate_universe/cargo_workspace/cargo-bazel-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/crate_universe/multi_package/cargo-bazel-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/crate_universe/using_cxx/cxxbridge-cmd.cargo-bazel-lock.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"checksum": "ab8b093d4ba7e3bca34a3fc81413c70271914e1b01ed42d22db41c6ea5e9dab6",
2+
"checksum": "1eeb80f1d0fa732cc8155ca4d18d9338651532ba8437ce17eb9422bf99d437f4",
33
"crates": {
44
"anstyle 1.0.1": {
55
"name": "anstyle",

0 commit comments

Comments
 (0)