Skip to content

Commit 402706f

Browse files
committed
Added support for --incompatible_compact_repo_mapping_manifest
1 parent 76a0b8f commit 402706f

File tree

1 file changed

+186
-10
lines changed

1 file changed

+186
-10
lines changed

rust/runfiles/runfiles.rs

Lines changed: 186 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
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).
143145
type 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

253315
fn 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

Comments
 (0)