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,70 @@ 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+ }
201+
202+ impl < const N : usize > From < [ ( RepoMappingKey , String ) ; N ] > for RepoMapping {
203+ fn from ( arr : [ ( RepoMappingKey , String ) ; N ] ) -> Self {
204+ RepoMapping :: Verbose ( HashMap :: from ( arr) )
205+ }
206+ }
145207
146208/// An interface for accessing to [Bazel runfiles](https://bazel.build/extending/rules#runfiles).
147209#[ derive( Debug ) ]
@@ -251,7 +313,8 @@ fn raw_rlocation(mode: &Mode, path: impl AsRef<Path>) -> Option<PathBuf> {
251313}
252314
253315fn parse_repo_mapping ( path : PathBuf ) -> Result < RepoMapping > {
254- let mut repo_mapping = RepoMapping :: new ( ) ;
316+ let mut exact = HashMap :: new ( ) ;
317+ let mut prefixes = Vec :: new ( ) ;
255318
256319 for line in std:: fs:: read_to_string ( path)
257320 . map_err ( RunfilesError :: RepoMappingIoError ) ?
@@ -261,10 +324,31 @@ fn parse_repo_mapping(path: PathBuf) -> Result<RepoMapping> {
261324 if parts. len ( ) < 3 {
262325 return Err ( RunfilesError :: RepoMappingInvalidFormat ) ;
263326 }
264- repo_mapping. insert ( ( parts[ 0 ] . into ( ) , parts[ 1 ] . into ( ) ) , parts[ 2 ] . into ( ) ) ;
327+
328+ let source_repo = parts[ 0 ] ;
329+ let apparent_name = parts[ 1 ] ;
330+ let target_repo = parts[ 2 ] ;
331+
332+ // Check if this is a prefix entry (ends with '*')
333+ // The '*' character is terminal and marks a prefix match entry
334+ if let Some ( prefix) = source_repo. strip_suffix ( '*' ) {
335+ prefixes. push ( (
336+ ( prefix. to_owned ( ) , apparent_name. to_owned ( ) ) ,
337+ target_repo. to_owned ( ) ,
338+ ) ) ;
339+ } else {
340+ exact. insert (
341+ ( source_repo. to_owned ( ) , apparent_name. to_owned ( ) ) ,
342+ target_repo. to_owned ( ) ,
343+ ) ;
344+ }
265345 }
266346
267- Ok ( repo_mapping)
347+ Ok ( if !prefixes. is_empty ( ) {
348+ RepoMapping :: Compact { exact, prefixes }
349+ } else {
350+ RepoMapping :: Verbose ( exact)
351+ } )
268352}
269353
270354/// Returns the .runfiles directory for the currently executing binary.
@@ -684,4 +768,96 @@ mod test {
684768 Err ( RunfilesError :: RepoMappingInvalidFormat ) ,
685769 ) ;
686770 }
771+
772+ #[ test]
773+ fn test_parse_repo_mapping_with_wildcard ( ) {
774+ let temp_dir = PathBuf :: from ( std:: env:: var ( "TEST_TMPDIR" ) . unwrap ( ) ) ;
775+ std:: fs:: create_dir_all ( & temp_dir) . unwrap ( ) ;
776+
777+ let mapping_file = temp_dir. join ( "test_parse_repo_mapping_with_wildcard.txt" ) ;
778+ std:: fs:: write (
779+ & mapping_file,
780+ dedent (
781+ r#"+deps+*,aaa,_main
782+ +deps+*,dep,+deps+dep1
783+ +deps+*,dep1,+deps+dep1
784+ +deps+*,dep2,+deps+dep2
785+ +deps+*,dep3,+deps+dep3
786+ +other+exact,foo,bar
787+ "# ,
788+ ) ,
789+ )
790+ . unwrap ( ) ;
791+
792+ let repo_mapping = parse_repo_mapping ( mapping_file) . unwrap ( ) ;
793+
794+ // Verify it's compact mode (because wildcards were detected)
795+ assert ! ( matches!( repo_mapping, RepoMapping :: Compact { .. } ) ) ;
796+
797+ // Check exact match for non-wildcard entry
798+ assert_eq ! (
799+ repo_mapping. get( & ( "+other+exact" . to_owned( ) , "foo" . to_owned( ) ) ) ,
800+ Some ( & "bar" . to_owned( ) )
801+ ) ;
802+
803+ // Check prefix matches work correctly
804+ // When looking up with +deps+dep1 as source_repo, it should match entries with +deps+ prefix
805+ assert_eq ! (
806+ repo_mapping. get( & ( "+deps+dep1" . to_owned( ) , "aaa" . to_owned( ) ) ) ,
807+ Some ( & "_main" . to_owned( ) )
808+ ) ;
809+ assert_eq ! (
810+ repo_mapping. get( & ( "+deps+dep1" . to_owned( ) , "dep" . to_owned( ) ) ) ,
811+ Some ( & "+deps+dep1" . to_owned( ) )
812+ ) ;
813+ assert_eq ! (
814+ repo_mapping. get( & ( "+deps+dep2" . to_owned( ) , "dep2" . to_owned( ) ) ) ,
815+ Some ( & "+deps+dep2" . to_owned( ) )
816+ ) ;
817+ assert_eq ! (
818+ repo_mapping. get( & ( "+deps+dep3" . to_owned( ) , "dep3" . to_owned( ) ) ) ,
819+ Some ( & "+deps+dep3" . to_owned( ) )
820+ ) ;
821+ }
822+
823+ #[ test]
824+ fn test_rlocation_from_with_wildcard ( ) {
825+ let temp_dir = PathBuf :: from ( std:: env:: var ( "TEST_TMPDIR" ) . unwrap ( ) ) ;
826+ std:: fs:: create_dir_all ( & temp_dir) . unwrap ( ) ;
827+
828+ // Create a mock runfiles directory
829+ let runfiles_dir = temp_dir. join ( "test_rlocation_from_with_wildcard.runfiles" ) ;
830+ std:: fs:: create_dir_all ( & runfiles_dir) . unwrap ( ) ;
831+
832+ let r = Runfiles {
833+ mode : Mode :: DirectoryBased ( runfiles_dir. clone ( ) ) ,
834+ repo_mapping : RepoMapping :: Compact {
835+ exact : HashMap :: new ( ) ,
836+ prefixes : vec ! [
837+ ( ( "+deps+" . to_owned( ) , "aaa" . to_owned( ) ) , "_main" . to_owned( ) ) ,
838+ (
839+ ( "+deps+" . to_owned( ) , "dep" . to_owned( ) ) ,
840+ "+deps+dep1" . to_owned( ) ,
841+ ) ,
842+ ] ,
843+ } ,
844+ } ;
845+
846+ // Test prefix matching for +deps+dep1
847+ let result = r. rlocation_from ( "aaa/some/path" , "+deps+dep1" ) ;
848+ assert_eq ! ( result, Some ( runfiles_dir. join( "_main/some/path" ) ) ) ;
849+
850+ // Test prefix matching for +deps+dep2
851+ let result = r. rlocation_from ( "aaa/other/path" , "+deps+dep2" ) ;
852+ assert_eq ! ( result, Some ( runfiles_dir. join( "_main/other/path" ) ) ) ;
853+
854+ // Test prefix matching with different apparent name
855+ let result = r. rlocation_from ( "dep/foo/bar" , "+deps+dep3" ) ;
856+ assert_eq ! ( result, Some ( runfiles_dir. join( "+deps+dep1/foo/bar" ) ) ) ;
857+
858+ // Test non-matching source repo (doesn't start with +deps+)
859+ let result = r. rlocation_from ( "aaa/path" , "+other+repo" ) ;
860+ // Should fall back to the path as-is
861+ assert_eq ! ( result, Some ( runfiles_dir. join( "aaa/path" ) ) ) ;
862+ }
687863}
0 commit comments