@@ -8,13 +8,15 @@ use std::process::Command;
88
99use anyhow:: { bail, Context as AnyhowContext , Result } ;
1010use hex:: ToHex ;
11- use serde:: { Deserialize , Serialize } ;
11+ use serde:: ser:: SerializeMap ;
12+ use serde:: { Deserialize , Serialize , Serializer } ;
1213use sha2:: { Digest as Sha2Digest , Sha256 } ;
1314
1415use crate :: config:: Config ;
1516use crate :: context:: Context ;
1617use crate :: metadata:: Cargo ;
1718use crate :: splicing:: { SplicingManifest , SplicingMetadata } ;
19+ use crate :: utils:: starlark:: { Label , Repository } ;
1820
1921pub ( 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+
212296impl PartialEq < str > for Digest {
213297 fn eq ( & self , other : & str ) -> bool {
214298 self . 0 == other
@@ -225,11 +309,13 @@ impl PartialEq<String> for Digest {
225309mod 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,
0 commit comments