Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 200 additions & 45 deletions rust/runfiles/runfiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
//!
//! 1. Depend on this runfiles library from your build rule:
//! ```python
//! rust_binary(
//! name = "my_binary",
//! ...
//! data = ["//path/to/my/data.txt"],
//! deps = ["@rules_rust//rust/runfiles"],
//! )
//! rust_binary(
//! name = "my_binary",
//! ...
//! data = ["//path/to/my/data.txt"],
//! deps = ["@rules_rust//rust/runfiles"],
//! )
//! ```
//!
//! 2. Import the runfiles library.
Expand All @@ -30,7 +30,7 @@
//! // ...
//! ```

use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::env;
use std::fs;
use std::io;
Expand Down Expand Up @@ -140,8 +140,53 @@ enum Mode {
ManifestBased(HashMap<PathBuf, PathBuf>),
}

/// A pair of "source" (the workspace the mapping affects) and "target apparent name" (the
/// non-bzlmod-generated/pretty name of a dependent workspace).
type RepoMappingKey = (String, String);
type RepoMapping = HashMap<RepoMappingKey, String>;

/// The mapping of keys to "target canonical directory" (the bzlmod-generated workspace name).
#[derive(Debug, PartialEq, Eq)]
struct RepoMapping {
exact: HashMap<RepoMappingKey, String>,

/// Used for `--incompatible_compact_repo_mapping_manifest`.
/// <https://github.com/bazelbuild/bazel/issues/26262>
prefixes: BTreeMap<RepoMappingKey, String>,
}

impl RepoMapping {
pub fn new() -> Self {
RepoMapping {
exact: HashMap::new(),
prefixes: BTreeMap::new(),
}
}

pub fn get(&self, key: &RepoMappingKey) -> Option<&String> {
// First try exact match with O(1) hash lookup
if let Some(value) = self.exact.get(key) {
return Some(value);
}

// Then try prefix match with O(n) iteration
// In compact mode, entries with wildcards are stored with just the prefix.
// We need to check if the lookup key's source_repo starts with any stored prefix.
let (source_repo, apparent_name) = key;
for ((stored_source, stored_apparent), value) in self.prefixes.iter() {
if source_repo.starts_with(stored_source) && apparent_name == stored_apparent {
return Some(value);
}
}

None
}
}

impl Default for RepoMapping {
fn default() -> Self {
Self::new()
}
}

/// An interface for accessing to [Bazel runfiles](https://bazel.build/extending/rules#runfiles).
#[derive(Debug)]
Expand Down Expand Up @@ -251,7 +296,8 @@ fn raw_rlocation(mode: &Mode, path: impl AsRef<Path>) -> Option<PathBuf> {
}

fn parse_repo_mapping(path: PathBuf) -> Result<RepoMapping> {
let mut repo_mapping = RepoMapping::new();
let mut exact = HashMap::new();
let mut prefixes = BTreeMap::new();

for line in std::fs::read_to_string(path)
.map_err(RunfilesError::RepoMappingIoError)?
Expand All @@ -261,10 +307,27 @@ fn parse_repo_mapping(path: PathBuf) -> Result<RepoMapping> {
if parts.len() < 3 {
return Err(RunfilesError::RepoMappingInvalidFormat);
}
repo_mapping.insert((parts[0].into(), parts[1].into()), parts[2].into());

let source_repo = parts[0];
let apparent_name = parts[1];
let target_repo = parts[2];

// Check if this is a prefix entry (ends with '*')
// The '*' character is terminal and marks a prefix match entry
if let Some(prefix) = source_repo.strip_suffix('*') {
prefixes.insert(
(prefix.to_owned(), apparent_name.to_owned()),
target_repo.to_owned(),
);
} else {
exact.insert(
(source_repo.to_owned(), apparent_name.to_owned()),
target_repo.to_owned(),
);
}
}

Ok(repo_mapping)
Ok(RepoMapping { exact, prefixes })
}

/// Returns the .runfiles directory for the currently executing binary.
Expand Down Expand Up @@ -625,43 +688,46 @@ mod test {

assert_eq!(
parse_repo_mapping(valid),
Ok(RepoMapping::from([
(
("local_config_xcode".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
),
(
("platforms".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
),
(
Ok(RepoMapping {
prefixes: BTreeMap::new(),
exact: HashMap::from([
(
"rust_darwin_aarch64__aarch64-apple-darwin__stable_tools".to_owned(),
("local_config_xcode".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
),
"rules_rust".to_owned()
),
(
("rules_rust_tinyjson".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
),
(
("local_config_sh".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
),
(
("bazel_tools".to_owned(), "__main__".to_owned()),
"rules_rust".to_owned()
),
(
("local_config_cc".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
),
(
("".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
)
]))
(
("platforms".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
),
(
(
"rust_darwin_aarch64__aarch64-apple-darwin__stable_tools".to_owned(),
"rules_rust".to_owned()
),
"rules_rust".to_owned()
),
(
("rules_rust_tinyjson".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
),
(
("local_config_sh".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
),
(
("bazel_tools".to_owned(), "__main__".to_owned()),
"rules_rust".to_owned()
),
(
("local_config_cc".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
),
(
("".to_owned(), "rules_rust".to_owned()),
"rules_rust".to_owned()
)
])
})
);
}

Expand All @@ -684,4 +750,93 @@ mod test {
Err(RunfilesError::RepoMappingInvalidFormat),
);
}

#[test]
fn test_parse_repo_mapping_with_wildcard() {
let temp_dir = PathBuf::from(std::env::var("TEST_TMPDIR").unwrap());
std::fs::create_dir_all(&temp_dir).unwrap();

let mapping_file = temp_dir.join("test_parse_repo_mapping_with_wildcard.txt");
std::fs::write(
&mapping_file,
dedent(
r#"+deps+*,aaa,_main
+deps+*,dep,+deps+dep1
+deps+*,dep1,+deps+dep1
+deps+*,dep2,+deps+dep2
+deps+*,dep3,+deps+dep3
+other+exact,foo,bar
"#,
),
)
.unwrap();

let repo_mapping = parse_repo_mapping(mapping_file).unwrap();

// Check exact match for non-wildcard entry
assert_eq!(
repo_mapping.get(&("+other+exact".to_owned(), "foo".to_owned())),
Some(&"bar".to_owned())
);

// Check prefix matches work correctly
// When looking up with +deps+dep1 as source_repo, it should match entries with +deps+ prefix
assert_eq!(
repo_mapping.get(&("+deps+dep1".to_owned(), "aaa".to_owned())),
Some(&"_main".to_owned())
);
assert_eq!(
repo_mapping.get(&("+deps+dep1".to_owned(), "dep".to_owned())),
Some(&"+deps+dep1".to_owned())
);
assert_eq!(
repo_mapping.get(&("+deps+dep2".to_owned(), "dep2".to_owned())),
Some(&"+deps+dep2".to_owned())
);
assert_eq!(
repo_mapping.get(&("+deps+dep3".to_owned(), "dep3".to_owned())),
Some(&"+deps+dep3".to_owned())
);
}

#[test]
fn test_rlocation_from_with_wildcard() {
let temp_dir = PathBuf::from(std::env::var("TEST_TMPDIR").unwrap());
std::fs::create_dir_all(&temp_dir).unwrap();

// Create a mock runfiles directory
let runfiles_dir = temp_dir.join("test_rlocation_from_with_wildcard.runfiles");
std::fs::create_dir_all(&runfiles_dir).unwrap();

let r = Runfiles {
mode: Mode::DirectoryBased(runfiles_dir.clone()),
repo_mapping: RepoMapping {
exact: HashMap::new(),
prefixes: BTreeMap::from([
(("+deps+".to_owned(), "aaa".to_owned()), "_main".to_owned()),
(
("+deps+".to_owned(), "dep".to_owned()),
"+deps+dep1".to_owned(),
),
]),
},
};

// Test prefix matching for +deps+dep1
let result = r.rlocation_from("aaa/some/path", "+deps+dep1");
assert_eq!(result, Some(runfiles_dir.join("_main/some/path")));

// Test prefix matching for +deps+dep2
let result = r.rlocation_from("aaa/other/path", "+deps+dep2");
assert_eq!(result, Some(runfiles_dir.join("_main/other/path")));

// Test prefix matching with different apparent name
let result = r.rlocation_from("dep/foo/bar", "+deps+dep3");
assert_eq!(result, Some(runfiles_dir.join("+deps+dep1/foo/bar")));

// Test non-matching source repo (doesn't start with +deps+)
let result = r.rlocation_from("aaa/path", "+other+repo");
// Should fall back to the path as-is
assert_eq!(result, Some(runfiles_dir.join("aaa/path")));
}
}