44//!
55//! 1. Depend on this runfiles library from your build rule:
66//! ```python
7- //! rust_binary(
8- //! name = "my_binary",
9- //! ...
10- //! data = ["//path/to/my/data.txt"],
11- //! deps = ["@rules_rust//rust/runfiles"],
12- //! )
7+ //! rust_binary(
8+ //! name = "my_binary",
9+ //! ...
10+ //! data = ["//path/to/my/data.txt"],
11+ //! deps = ["@rules_rust//rust/runfiles"],
12+ //! )
1313//! ```
1414//!
1515//! 2. Import the runfiles library.
@@ -140,8 +140,64 @@ enum Mode {
140140 ManifestBased ( HashMap < PathBuf , PathBuf > ) ,
141141}
142142
143+ /// A pair of "source" (the workspace the mapping affects) and "target apparent name" (the
144+ /// non-bzlmod-generated/pretty name of a dependent workspace).
143145type RepoMappingKey = ( String , String ) ;
144- type RepoMapping = HashMap < RepoMappingKey , String > ;
146+
147+ /// The mapping of keys to "target canonical directory" (the bzlmod-generated workspace name).
148+ #[ derive( Debug , PartialEq , Eq ) ]
149+ enum RepoMapping {
150+ /// Implements `--incompatible_compact_repo_mapping_manifest`.
151+ /// <https://github.com/bazelbuild/bazel/issues/26262>
152+ ///
153+ /// Uses a HashMap for fast exact lookups, and a Vec for prefix-based fallback matching.
154+ Compact {
155+ exact : HashMap < RepoMappingKey , String > ,
156+ prefixes : Vec < ( RepoMappingKey , String ) > ,
157+ } ,
158+
159+ /// The canonical repo mapping file.
160+ Verbose ( HashMap < RepoMappingKey , String > ) ,
161+ }
162+
163+ impl RepoMapping {
164+ pub fn new ( ) -> Self {
165+ RepoMapping :: Compact {
166+ exact : HashMap :: new ( ) ,
167+ prefixes : Vec :: new ( ) ,
168+ }
169+ }
170+
171+ pub fn get ( & self , key : & RepoMappingKey ) -> Option < & String > {
172+ match self {
173+ RepoMapping :: Compact { exact, prefixes } => {
174+ // First try exact match with O(1) hash lookup
175+ if let Some ( value) = exact. get ( key) {
176+ return Some ( value) ;
177+ }
178+
179+ // Then try prefix match with O(n) iteration
180+ // In compact mode, entries with wildcards are stored with just the prefix.
181+ // We need to check if the lookup key's source_repo starts with any stored prefix.
182+ let ( source_repo, apparent_name) = key;
183+ for ( ( stored_source, stored_apparent) , value) in prefixes {
184+ if source_repo. starts_with ( stored_source) && apparent_name == stored_apparent {
185+ return Some ( value) ;
186+ }
187+ }
188+
189+ None
190+ }
191+ RepoMapping :: Verbose ( hash_map) => hash_map. get ( key) ,
192+ }
193+ }
194+ }
195+
196+ impl Default for RepoMapping {
197+ fn default ( ) -> Self {
198+ Self :: new ( )
199+ }
200+ }
145201
146202/// An interface for accessing to [Bazel runfiles](https://bazel.build/extending/rules#runfiles).
147203#[ derive( Debug ) ]
@@ -251,7 +307,8 @@ fn raw_rlocation(mode: &Mode, path: impl AsRef<Path>) -> Option<PathBuf> {
251307}
252308
253309fn parse_repo_mapping ( path : PathBuf ) -> Result < RepoMapping > {
254- let mut repo_mapping = RepoMapping :: new ( ) ;
310+ let mut exact = HashMap :: new ( ) ;
311+ let mut prefixes = Vec :: new ( ) ;
255312
256313 for line in std:: fs:: read_to_string ( path)
257314 . map_err ( RunfilesError :: RepoMappingIoError ) ?
@@ -261,10 +318,31 @@ fn parse_repo_mapping(path: PathBuf) -> Result<RepoMapping> {
261318 if parts. len ( ) < 3 {
262319 return Err ( RunfilesError :: RepoMappingInvalidFormat ) ;
263320 }
264- repo_mapping. insert ( ( parts[ 0 ] . into ( ) , parts[ 1 ] . into ( ) ) , parts[ 2 ] . into ( ) ) ;
321+
322+ let source_repo = parts[ 0 ] ;
323+ let apparent_name = parts[ 1 ] ;
324+ let target_repo = parts[ 2 ] ;
325+
326+ // Check if this is a prefix entry (ends with '*')
327+ // The '*' character is terminal and marks a prefix match entry
328+ if let Some ( prefix) = source_repo. strip_suffix ( '*' ) {
329+ prefixes. push ( (
330+ ( prefix. to_owned ( ) , apparent_name. to_owned ( ) ) ,
331+ target_repo. to_owned ( ) ,
332+ ) ) ;
333+ } else {
334+ exact. insert (
335+ ( source_repo. to_owned ( ) , apparent_name. to_owned ( ) ) ,
336+ target_repo. to_owned ( ) ,
337+ ) ;
338+ }
265339 }
266340
267- Ok ( repo_mapping)
341+ Ok ( if !prefixes. is_empty ( ) {
342+ RepoMapping :: Compact { exact, prefixes }
343+ } else {
344+ RepoMapping :: Verbose ( exact)
345+ } )
268346}
269347
270348/// Returns the .runfiles directory for the currently executing binary.
@@ -625,7 +703,7 @@ mod test {
625703
626704 assert_eq ! (
627705 parse_repo_mapping( valid) ,
628- Ok ( RepoMapping :: from( [
706+ Ok ( RepoMapping :: Verbose ( HashMap :: from( [
629707 (
630708 ( "local_config_xcode" . to_owned( ) , "rules_rust" . to_owned( ) ) ,
631709 "rules_rust" . to_owned( )
@@ -661,7 +739,7 @@ mod test {
661739 ( "" . to_owned( ) , "rules_rust" . to_owned( ) ) ,
662740 "rules_rust" . to_owned( )
663741 )
664- ] ) )
742+ ] ) ) )
665743 ) ;
666744 }
667745
@@ -684,4 +762,96 @@ mod test {
684762 Err ( RunfilesError :: RepoMappingInvalidFormat ) ,
685763 ) ;
686764 }
765+
766+ #[ test]
767+ fn test_parse_repo_mapping_with_wildcard ( ) {
768+ let temp_dir = PathBuf :: from ( std:: env:: var ( "TEST_TMPDIR" ) . unwrap ( ) ) ;
769+ std:: fs:: create_dir_all ( & temp_dir) . unwrap ( ) ;
770+
771+ let mapping_file = temp_dir. join ( "test_parse_repo_mapping_with_wildcard.txt" ) ;
772+ std:: fs:: write (
773+ & mapping_file,
774+ dedent (
775+ r#"+deps+*,aaa,_main
776+ +deps+*,dep,+deps+dep1
777+ +deps+*,dep1,+deps+dep1
778+ +deps+*,dep2,+deps+dep2
779+ +deps+*,dep3,+deps+dep3
780+ +other+exact,foo,bar
781+ "# ,
782+ ) ,
783+ )
784+ . unwrap ( ) ;
785+
786+ let repo_mapping = parse_repo_mapping ( mapping_file) . unwrap ( ) ;
787+
788+ // Verify it's compact mode (because wildcards were detected)
789+ assert ! ( matches!( repo_mapping, RepoMapping :: Compact { .. } ) ) ;
790+
791+ // Check exact match for non-wildcard entry
792+ assert_eq ! (
793+ repo_mapping. get( & ( "+other+exact" . to_owned( ) , "foo" . to_owned( ) ) ) ,
794+ Some ( & "bar" . to_owned( ) )
795+ ) ;
796+
797+ // Check prefix matches work correctly
798+ // When looking up with +deps+dep1 as source_repo, it should match entries with +deps+ prefix
799+ assert_eq ! (
800+ repo_mapping. get( & ( "+deps+dep1" . to_owned( ) , "aaa" . to_owned( ) ) ) ,
801+ Some ( & "_main" . to_owned( ) )
802+ ) ;
803+ assert_eq ! (
804+ repo_mapping. get( & ( "+deps+dep1" . to_owned( ) , "dep" . to_owned( ) ) ) ,
805+ Some ( & "+deps+dep1" . to_owned( ) )
806+ ) ;
807+ assert_eq ! (
808+ repo_mapping. get( & ( "+deps+dep2" . to_owned( ) , "dep2" . to_owned( ) ) ) ,
809+ Some ( & "+deps+dep2" . to_owned( ) )
810+ ) ;
811+ assert_eq ! (
812+ repo_mapping. get( & ( "+deps+dep3" . to_owned( ) , "dep3" . to_owned( ) ) ) ,
813+ Some ( & "+deps+dep3" . to_owned( ) )
814+ ) ;
815+ }
816+
817+ #[ test]
818+ fn test_rlocation_from_with_wildcard ( ) {
819+ let temp_dir = PathBuf :: from ( std:: env:: var ( "TEST_TMPDIR" ) . unwrap ( ) ) ;
820+ std:: fs:: create_dir_all ( & temp_dir) . unwrap ( ) ;
821+
822+ // Create a mock runfiles directory
823+ let runfiles_dir = temp_dir. join ( "test_rlocation_from_with_wildcard.runfiles" ) ;
824+ std:: fs:: create_dir_all ( & runfiles_dir) . unwrap ( ) ;
825+
826+ let r = Runfiles {
827+ mode : Mode :: DirectoryBased ( runfiles_dir. clone ( ) ) ,
828+ repo_mapping : RepoMapping :: Compact {
829+ exact : HashMap :: new ( ) ,
830+ prefixes : vec ! [
831+ ( ( "+deps+" . to_owned( ) , "aaa" . to_owned( ) ) , "_main" . to_owned( ) ) ,
832+ (
833+ ( "+deps+" . to_owned( ) , "dep" . to_owned( ) ) ,
834+ "+deps+dep1" . to_owned( ) ,
835+ ) ,
836+ ] ,
837+ } ,
838+ } ;
839+
840+ // Test prefix matching for +deps+dep1
841+ let result = r. rlocation_from ( "aaa/some/path" , "+deps+dep1" ) ;
842+ assert_eq ! ( result, Some ( runfiles_dir. join( "_main/some/path" ) ) ) ;
843+
844+ // Test prefix matching for +deps+dep2
845+ let result = r. rlocation_from ( "aaa/other/path" , "+deps+dep2" ) ;
846+ assert_eq ! ( result, Some ( runfiles_dir. join( "_main/other/path" ) ) ) ;
847+
848+ // Test prefix matching with different apparent name
849+ let result = r. rlocation_from ( "dep/foo/bar" , "+deps+dep3" ) ;
850+ assert_eq ! ( result, Some ( runfiles_dir. join( "+deps+dep1/foo/bar" ) ) ) ;
851+
852+ // Test non-matching source repo (doesn't start with +deps+)
853+ let result = r. rlocation_from ( "aaa/path" , "+other+repo" ) ;
854+ // Should fall back to the path as-is
855+ assert_eq ! ( result, Some ( runfiles_dir. join( "aaa/path" ) ) ) ;
856+ }
687857}
0 commit comments