blob: d58ef9a919666355d85928cd7128b6b26aba6c99 [file]
//! Runfiles lookup library for Bazel-built Rust binaries and tests.
//!
//! USAGE:
//!
//! 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"],
//! )
//! ```
//!
//! 2. Import the runfiles library.
//! ```ignore
//! use runfiles::Runfiles;
//! ```
//!
//! 3. Create a Runfiles object and use `rlocation!`` to look up runfile paths:
//! ```ignore
//!
//! use runfiles::{Runfiles, rlocation};
//!
//! let r = Runfiles::create().unwrap();
//! let path = rlocation!(r, "my_workspace/path/to/my/data.txt").expect("Failed to locate runfile");
//!
//! let f = File::open(path).unwrap();
//!
//! // ...
//! ```
use std::collections::{BTreeMap, HashMap};
#[cfg(not(target_family = "wasm"))]
use std::env;
#[cfg(not(target_family = "wasm"))]
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
const RUNFILES_DIR_ENV_VAR: &str = "RUNFILES_DIR";
const MANIFEST_FILE_ENV_VAR: &str = "RUNFILES_MANIFEST_FILE";
const TEST_SRCDIR_ENV_VAR: &str = "TEST_SRCDIR";
#[macro_export]
macro_rules! rlocation {
($r:expr, $path:expr) => {
$r.rlocation_from($path, env!("REPOSITORY_NAME"))
};
}
/// The error type for [Runfiles] construction.
#[derive(Debug)]
pub enum RunfilesError {
/// Directory based runfiles could not be found.
RunfilesDirNotFound,
/// An [I/O Error](https://doc.rust-lang.org/std/io/struct.Error.html)
/// which occurred during the creation of directory-based runfiles.
RunfilesDirIoError(io::Error),
/// An [I/O Error](https://doc.rust-lang.org/std/io/struct.Error.html)
/// which occurred during the creation of manifest-file-based runfiles.
RunfilesManifestIoError(io::Error),
/// A manifest file could not be parsed.
RunfilesManifestInvalidFormat,
/// The bzlmod repo-mapping file could not be found.
RepoMappingNotFound,
/// The bzlmod repo-mapping file could not be parsed.
RepoMappingInvalidFormat,
/// An [I/O Error](https://doc.rust-lang.org/std/io/struct.Error.html)
/// which occurred during the parsing of a repo-mapping file.
RepoMappingIoError(io::Error),
/// An error indicating a specific Runfile was not found.
RunfileNotFound(PathBuf),
/// An [I/O Error](https://doc.rust-lang.org/std/io/struct.Error.html)
/// which occurred when operating with a particular runfile.
RunfileIoError(io::Error),
}
impl std::fmt::Display for RunfilesError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
RunfilesError::RunfilesDirNotFound => write!(f, "RunfilesDirNotFound"),
RunfilesError::RunfilesDirIoError(err) => write!(f, "RunfilesDirIoError: {:?}", err),
RunfilesError::RunfilesManifestIoError(err) => {
write!(f, "RunfilesManifestIoError: {:?}", err)
}
RunfilesError::RunfilesManifestInvalidFormat => write!(f, "RepoMappingInvalidFormat"),
RunfilesError::RepoMappingNotFound => write!(f, "RepoMappingInvalidFormat"),
RunfilesError::RepoMappingInvalidFormat => write!(f, "RepoMappingInvalidFormat"),
RunfilesError::RepoMappingIoError(err) => write!(f, "RepoMappingIoError: {:?}", err),
RunfilesError::RunfileNotFound(path) => {
write!(f, "RunfileNotFound: {}", path.display())
}
RunfilesError::RunfileIoError(err) => write!(f, "RunfileIoError: {:?}", err),
}
}
}
impl std::error::Error for RunfilesError {}
impl PartialEq for RunfilesError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::RunfilesDirIoError(l0), Self::RunfilesDirIoError(r0)) => {
l0.to_string() == r0.to_string()
}
(Self::RunfilesManifestIoError(l0), Self::RunfilesManifestIoError(r0)) => {
l0.to_string() == r0.to_string()
}
(Self::RepoMappingIoError(l0), Self::RepoMappingIoError(r0)) => {
l0.to_string() == r0.to_string()
}
(Self::RunfileNotFound(l0), Self::RunfileNotFound(r0)) => l0 == r0,
(Self::RunfileIoError(l0), Self::RunfileIoError(r0)) => {
l0.to_string() == r0.to_string()
}
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}
/// A specialized [`std::result::Result`] type for
pub type Result<T> = std::result::Result<T, RunfilesError>;
#[derive(Debug)]
enum Mode {
/// Runfiles located in a directory indicated by the `RUNFILES_DIR` environment
/// variable or a neighboring `*.runfiles` directory to the executable.
DirectoryBased(PathBuf),
/// Runfiles represented as a mapping of `rlocationpath` to real paths indicated
/// by the `RUNFILES_MANIFEST_FILE` environment variable.
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);
/// 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)]
pub struct Runfiles {
mode: Mode,
repo_mapping: RepoMapping,
}
impl Runfiles {
/// Returns a new [`RunfilesBuilder`]. See its methods for the available
/// knobs; [`RunfilesBuilder::build`] with no source set is equivalent
/// to [`Runfiles::create`].
pub fn builder() -> RunfilesBuilder {
RunfilesBuilder::default()
}
/// Discovers a runfiles source from the process environment and loads
/// `_repo_mapping` if present. Equivalent to `Runfiles::builder().build()`.
///
/// Discovery order: `RUNFILES_MANIFEST_FILE`, then a `RUNFILES_DIR` /
/// `TEST_SRCDIR` / `<argv0>.runfiles` directory (with an inner `MANIFEST`
/// preferred when present), then a `<argv0>.runfiles_manifest` sibling.
pub fn create() -> Result<Self> {
Self::builder().build()
}
/// Returns the runtime path of a runfile.
///
/// Runfiles are data-dependencies of Bazel-built binaries and tests.
/// The returned path may not be valid. The caller should check the path's
/// validity and that the path exists.
/// @deprecated - this is not bzlmod-aware. Prefer the `rlocation!` macro or `rlocation_from`
pub fn rlocation(&self, path: impl AsRef<Path>) -> Option<PathBuf> {
let path = path.as_ref();
if path.is_absolute() {
return Some(path.to_path_buf());
}
raw_rlocation(&self.mode, path)
}
/// Returns the runtime path of a runfile.
///
/// Runfiles are data-dependencies of Bazel-built binaries and tests.
/// The returned path may not be valid. The caller should check the path's
/// validity and that the path exists.
///
/// Typically this should be used via the `rlocation!` macro to properly set source_repo.
pub fn rlocation_from(&self, path: impl AsRef<Path>, source_repo: &str) -> Option<PathBuf> {
let path = path.as_ref();
if path.is_absolute() {
return Some(path.to_path_buf());
}
let path_str = path.to_str().expect("Should be valid UTF8");
let (repo_alias, repo_path): (&str, Option<&str>) = match path_str.split_once('/') {
Some((name, alias)) => (name, Some(alias)),
None => (path_str, None),
};
let key: (String, String) = (source_repo.into(), repo_alias.into());
if let Some(target_repo_directory) = self.repo_mapping.get(&key) {
match repo_path {
Some(repo_path) => {
raw_rlocation(&self.mode, format!("{target_repo_directory}/{repo_path}"))
}
None => raw_rlocation(&self.mode, target_repo_directory),
}
} else {
raw_rlocation(&self.mode, path)
}
}
}
/// Builder for [`Runfiles`]. Construct via [`Runfiles::builder`].
///
/// Any unset knob falls back to the same behavior as [`Runfiles::create`]:
/// [`build`](RunfilesBuilder::build) with no source runs process-based
/// discovery, and with no `repo_mapping` reads `_repo_mapping` from the
/// resolved runfiles tree if present.
#[derive(Default)]
pub struct RunfilesBuilder {
source: Option<Source>,
repo_mapping: Option<String>,
}
enum Source {
Directory(PathBuf),
Manifest(HashMap<PathBuf, PathBuf>),
}
impl RunfilesBuilder {
/// Use this directory as the runfiles root, skipping discovery. Suitable
/// for platforms without env vars, argv, or a discoverable filesystem
/// (notably `wasm32-unknown-unknown`, where the host locates the tree).
pub fn directory(mut self, dir: impl Into<PathBuf>) -> Self {
self.source = Some(Source::Directory(dir.into()));
self
}
/// Use this in-memory manifest as the runfiles source, skipping discovery.
/// The content follows Bazel's `SourceManifestAction` format.
pub fn manifest(mut self, content: impl AsRef<str>) -> Self {
self.source = Some(Source::Manifest(parse_manifest(content.as_ref())));
self
}
/// Override the `_repo_mapping` file contents. When unset, [`build`](Self::build)
/// reads `_repo_mapping` from the resolved runfiles tree if it exists.
pub fn repo_mapping(mut self, mapping: impl Into<String>) -> Self {
self.repo_mapping = Some(mapping.into());
self
}
pub fn build(self) -> Result<Runfiles> {
let mode = match self.source {
Some(Source::Directory(dir)) => Mode::DirectoryBased(dir),
Some(Source::Manifest(map)) => Mode::ManifestBased(map),
None => discover_mode()?,
};
let repo_mapping = match self.repo_mapping {
Some(content) => parse_repo_mapping_str(&content)?,
// No override: try to load `_repo_mapping` from the resolved tree.
// `.exists()` returns false on platforms without a real fs (wasm),
// so this cleanly no-ops instead of erroring.
None => raw_rlocation(&mode, "_repo_mapping")
.filter(|f| f.exists())
.map(parse_repo_mapping)
.transpose()?
.unwrap_or_default(),
};
Ok(Runfiles { mode, repo_mapping })
}
}
fn discover_mode() -> Result<Mode> {
if let Some(manifest_file) = std::env::var_os(MANIFEST_FILE_ENV_VAR).filter(|v| !v.is_empty()) {
return read_manifest_file(Path::new(&manifest_file));
}
// Bazel itself does not publish a normative spec for how a runfiles
// library resolves runfiles when none of the documented env vars
// (`RUNFILES_MANIFEST_FILE`, `RUNFILES_DIR`, `TEST_SRCDIR`) are set —
// see <https://bazel.build/concepts/runfiles> and
// <https://bazel.build/reference/test-encyclopedia>, which describe
// the env vars and the runfiles tree but leave discovery to each
// language's library. What Bazel *does* guarantee on the producer
// side is that every binary action writes a `<binary>.runfiles_manifest`
// text file next to the output, and that `bazel run` / `bazel test`
// additionally materialize a `<binary>.runfiles/` directory.
//
// Resolution order:
// 1. Whatever `find_runfiles_dir()` returns (env vars,
// `<argv0>.runfiles/` sibling walk, or the `.runfiles` ancestor
// walk). If it returns a directory containing a `MANIFEST`,
// prefer that; otherwise use the dir.
// 2. Fall back to `<argv0>.runfiles_manifest` — the file Bazel
// writes on every build action, even when no `bazel run`
// invocation materialized the directory. This is what lets
// editor-exec'd wrappers find their runfiles without an
// out-of-band bootstrap step.
match find_runfiles_dir() {
Ok(dir) => {
let manifest_path = dir.join("MANIFEST");
if manifest_path.exists() {
read_manifest_file(&manifest_path)
} else {
Ok(Mode::DirectoryBased(dir))
}
}
Err(_) => match find_runfiles_manifest_from_argv0() {
Some(manifest_path) => read_manifest_file(&manifest_path),
None => Err(RunfilesError::RunfilesDirNotFound),
},
}
}
fn read_manifest_file(path: &Path) -> Result<Mode> {
let content = std::fs::read_to_string(path).map_err(RunfilesError::RunfilesManifestIoError)?;
Ok(Mode::ManifestBased(parse_manifest(&content)))
}
fn parse_manifest(content: &str) -> HashMap<PathBuf, PathBuf> {
content
.lines()
.flat_map(|line| {
let raw_pair = line
.strip_prefix(' ')
.unwrap_or(line)
.split_once(' ')
.ok_or(RunfilesError::RunfilesManifestInvalidFormat)?;
let pair = if line.starts_with(' ') {
// Unescape according to SourceManifestAction.java.
// https://github.com/bazelbuild/bazel/blob/3cb75e7bb181e3fb2b33707c172bf80431dc4712/src/main/java/com/google/devtools/build/lib/analysis/SourceManifestAction.java#L77-L84
(
raw_pair
.0
.replace("\\s", " ")
.replace("\\n", "\n")
.replace("\\b", "\\")
.into(),
raw_pair.1.replace("\\n", "\n").replace("\\b", "\\").into(),
)
} else {
(raw_pair.0.into(), raw_pair.1.into())
};
Ok::<(PathBuf, PathBuf), RunfilesError>(pair)
})
.collect::<HashMap<_, _>>()
}
fn raw_rlocation(mode: &Mode, path: impl AsRef<Path>) -> Option<PathBuf> {
let path = path.as_ref();
match mode {
Mode::DirectoryBased(runfiles_dir) => Some(runfiles_dir.join(path)),
Mode::ManifestBased(path_mapping) => path_mapping.get(path).cloned(),
}
}
fn parse_repo_mapping(path: PathBuf) -> Result<RepoMapping> {
let content = std::fs::read_to_string(path).map_err(RunfilesError::RepoMappingIoError)?;
parse_repo_mapping_str(&content)
}
fn parse_repo_mapping_str(content: &str) -> Result<RepoMapping> {
let mut exact = HashMap::new();
let mut prefixes = BTreeMap::new();
for line in content.lines() {
let parts: Vec<&str> = line.splitn(3, ',').collect();
if parts.len() < 3 {
return Err(RunfilesError::RepoMappingInvalidFormat);
}
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(RepoMapping { exact, prefixes })
}
/// Locate a `<binary>.runfiles_manifest` file sitting next to `argv[0]`.
///
/// Bazel writes this manifest on every binary build action but only
/// materializes the `<binary>.runfiles/` directory on `bazel run` /
/// `bazel test`. When a binary is exec'd by path outside a `bazel run`
/// invocation — e.g. an editor launching a wrapper directly — the
/// manifest file is the only runfiles source of truth.
///
/// `argv[0]` is preferred over [`std::env::current_exe`] because Bazel
/// materializes the runfiles next to the launcher symlink the user
/// invoked, not next to the resolved executable target. The fallback to
/// [`std::env::current_exe`] only applies when `argv[0]` is a bare
/// basename (i.e. the binary was found via `$PATH` and has no directory
/// component to use as the manifest's parent).
///
/// Returns `None` when no manifest file exists (in which case callers
/// fall through to `find_runfiles_dir`).
#[cfg(not(target_family = "wasm"))]
fn find_runfiles_manifest_from_argv0() -> Option<PathBuf> {
let argv0 = std::env::args_os().next()?;
let argv0_path = PathBuf::from(&argv0);
if argv0_path
.parent()
.is_some_and(|p| !p.as_os_str().is_empty())
{
if let Some(manifest) = find_runfiles_manifest_for(&argv0_path) {
return Some(manifest);
}
}
// `argv[0]` was a bare basename (PATH lookup) or its sibling check
// came up empty; try the resolved executable path next.
let exe = std::env::current_exe().ok()?;
find_runfiles_manifest_for(&exe)
}
#[cfg(target_family = "wasm")]
fn find_runfiles_manifest_from_argv0() -> Option<PathBuf> {
None
}
/// Inner helper for [`find_runfiles_manifest_from_argv0`] that takes an
/// explicit binary path so it's testable without controlling argv[0].
#[cfg(not(target_family = "wasm"))]
fn find_runfiles_manifest_for(exe_path: &Path) -> Option<PathBuf> {
let dir = exe_path.parent()?;
let file_name = exe_path.file_name()?;
let mut manifest_name = file_name.to_owned();
manifest_name.push(".runfiles_manifest");
let manifest_path = dir.join(&manifest_name);
manifest_path.is_file().then_some(manifest_path)
}
/// Returns the .runfiles directory for the currently executing binary.
pub fn find_runfiles_dir() -> Result<PathBuf> {
if let Some(value) = std::env::var_os(MANIFEST_FILE_ENV_VAR) {
assert!(
value.is_empty(),
"Unexpected call when {} exists",
MANIFEST_FILE_ENV_VAR
);
}
// If Bazel told us about the runfiles dir, use that without looking further.
if let Some(runfiles_dir) = std::env::var_os(RUNFILES_DIR_ENV_VAR).map(PathBuf::from) {
if runfiles_dir.is_dir() {
return Ok(runfiles_dir);
}
}
if let Some(test_srcdir) = std::env::var_os(TEST_SRCDIR_ENV_VAR).map(PathBuf::from) {
if test_srcdir.is_dir() {
return Ok(test_srcdir);
}
}
find_runfiles_dir_from_argv0()
}
#[cfg(not(target_family = "wasm"))]
fn find_runfiles_dir_from_argv0() -> Result<PathBuf> {
// Consume the first argument (argv[0])
let exec_path = std::env::args().next().expect("arg 0 was not set");
let current_dir =
env::current_dir().expect("The current working directory is always expected to be set.");
let mut binary_path = PathBuf::from(&exec_path);
loop {
// Check for our neighboring `${binary}.runfiles` directory.
let mut runfiles_name = binary_path.file_name().unwrap().to_owned();
runfiles_name.push(".runfiles");
let runfiles_path = binary_path.with_file_name(&runfiles_name);
if runfiles_path.is_dir() {
return Ok(runfiles_path);
}
// Check if we're already under a `*.runfiles` directory.
{
// TODO: 1.28 adds Path::ancestors() which is a little simpler.
let mut next = binary_path.parent();
while let Some(ancestor) = next {
if ancestor
.file_name()
.is_some_and(|f| f.to_string_lossy().ends_with(".runfiles"))
{
return Ok(ancestor.to_path_buf());
}
next = ancestor.parent();
}
}
if !fs::symlink_metadata(&binary_path)
.map_err(RunfilesError::RunfilesDirIoError)?
.file_type()
.is_symlink()
{
break;
}
// Follow symlinks and keep looking.
let link_target = binary_path
.read_link()
.map_err(RunfilesError::RunfilesDirIoError)?;
binary_path = if link_target.is_absolute() {
link_target
} else {
let link_dir = binary_path.parent().unwrap();
current_dir.join(link_dir).join(link_target)
}
}
Err(RunfilesError::RunfilesDirNotFound)
}
/// wasm has no `argv[0]` or symlink resolution to work with.
#[cfg(target_family = "wasm")]
fn find_runfiles_dir_from_argv0() -> Result<PathBuf> {
Err(RunfilesError::RunfilesDirNotFound)
}
#[cfg(test)]
mod test {
use super::*;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::fs::File;
use std::hash::Hash;
use std::io::prelude::*;
use std::sync::{Mutex, OnceLock};
/// A mutex used to guard
static GLOBAL_MUTEX: OnceLock<Mutex<i32>> = OnceLock::new();
/// Mock out environment variables for a given body to work. Very similar to
/// [temp-env](https://crates.io/crates/temp-env).
fn with_mock_env<K, V, F, R>(kvs: impl AsRef<[(K, Option<V>)]>, closure: F) -> R
where
K: AsRef<OsStr> + Clone + Eq + Hash,
V: AsRef<OsStr> + Clone,
F: FnOnce() -> R,
{
let mtx = GLOBAL_MUTEX.get_or_init(|| Mutex::new(0));
// Ignore poisoning as it's expected to be another test failing an assertion.
let _guard = mtx.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
// track the original state of the environment.
let mut old_env = HashMap::new();
// Replace or remove requested environment variables.
for (env, val) in kvs.as_ref() {
// Track the original state of the variable.
match std::env::var_os(env) {
Some(v) => old_env.insert(env, Some(v)),
None => old_env.insert(env, None::<OsString>),
};
match val {
Some(v) => std::env::set_var(env, v),
None => std::env::remove_var(env),
}
}
// Run requested work.
let result = closure();
// Restore original environment
for (env, val) in old_env {
match val {
Some(v) => std::env::set_var(env, v),
None => std::env::remove_var(env),
}
}
result
}
#[test]
fn test_mock_env() {
let original_name = std::env::var("TEST_WORKSPACE").unwrap();
assert!(
!original_name.is_empty(),
"In Bazel tests, `TEST_WORKSPACE` is expected to be populated."
);
let mocked_name = with_mock_env([("TEST_WORKSPACE", Some("foobar"))], || {
std::env::var("TEST_WORKSPACE").unwrap()
});
assert_eq!(mocked_name, "foobar");
assert_eq!(original_name, std::env::var("TEST_WORKSPACE").unwrap());
}
/// Create a temp directory to act as a runfiles directory for testing
/// [super::Mode::DirectoryBased] style runfiles.
fn make_runfiles_like_dir(name: &str) -> String {
with_mock_env([("FAKE", None::<&str>)], || {
let r = Runfiles::create().unwrap();
let path = "rules_rust/rust/runfiles/data/sample.txt";
let f = rlocation!(r, path).unwrap();
let temp_dir = PathBuf::from(std::env::var("TEST_TMPDIR").unwrap());
let runfiles_dir = temp_dir.join(name);
let test_path = runfiles_dir.join(path);
if let Some(parent) = test_path.parent() {
std::fs::create_dir_all(parent).expect("Failed to create test path parents.");
}
std::fs::copy(f, test_path).expect("Failed to copy test file");
runfiles_dir.to_str().unwrap().to_string()
})
}
/// Test the general behavior of runfiles. The behavior of runfiles will change
/// depending on the system but each mode is explicitly covered in other tests.
#[test]
fn test_standard_lookup() {
let r = Runfiles::create().unwrap();
let f = rlocation!(r, "rules_rust/rust/runfiles/data/sample.txt").unwrap();
let mut f = File::open(&f)
.unwrap_or_else(|e| panic!("Failed to open file: {}\n{:?}", f.display(), e));
let mut buffer = String::new();
f.read_to_string(&mut buffer).unwrap();
assert_eq!("Example Text!", buffer);
}
/// Only `RUNFILES_DIR` is set.
#[test]
fn test_env_only_runfiles_dir() {
let runfiles_dir = make_runfiles_like_dir("test_env_only_runfiles_dir");
with_mock_env(
[
(MANIFEST_FILE_ENV_VAR, None::<&str>),
(RUNFILES_DIR_ENV_VAR, Some(runfiles_dir.as_str())),
(TEST_SRCDIR_ENV_VAR, None::<&str>),
],
|| {
let r = Runfiles::create().unwrap();
let d = rlocation!(r, "rules_rust").unwrap();
let f = rlocation!(r, "rules_rust/rust/runfiles/data/sample.txt").unwrap();
assert_eq!(d.join("rust/runfiles/data/sample.txt"), f);
let mut f = File::open(&f)
.unwrap_or_else(|e| panic!("Failed to open file: {}\n{:?}", f.display(), e));
let mut buffer = String::new();
f.read_to_string(&mut buffer).unwrap();
assert_eq!("Example Text!", buffer);
},
);
}
/// Tests when MANIFEST_FILE is set to an empty string, RUNFILES_DIR is preferred.
#[test]
fn test_runfiles_manifest_file_empty() {
let runfiles_dir = make_runfiles_like_dir("test_runfiles_manifest_file_empty");
with_mock_env(
[
(MANIFEST_FILE_ENV_VAR, Some("")),
(RUNFILES_DIR_ENV_VAR, Some(runfiles_dir.as_str())),
(TEST_SRCDIR_ENV_VAR, None::<&str>),
],
|| {
let r = Runfiles::create().unwrap();
let d = rlocation!(r, "rules_rust").unwrap();
let f = rlocation!(r, "rules_rust/rust/runfiles/data/sample.txt").unwrap();
assert_eq!(d.join("rust/runfiles/data/sample.txt"), f);
let mut f = File::open(&f)
.unwrap_or_else(|e| panic!("Failed to open file: {}\n{:?}", f.display(), e));
let mut buffer = String::new();
f.read_to_string(&mut buffer).unwrap();
assert_eq!("Example Text!", buffer);
},
);
}
/// Only `TEST_SRCDIR` is set.
#[test]
fn test_env_only_test_srcdir() {
let runfiles_dir = make_runfiles_like_dir("test_env_only_test_srcdir");
with_mock_env(
[
(MANIFEST_FILE_ENV_VAR, None::<&str>),
(RUNFILES_DIR_ENV_VAR, None::<&str>),
(TEST_SRCDIR_ENV_VAR, Some(runfiles_dir.as_str())),
],
|| {
let r = Runfiles::create().unwrap();
let runfile = rlocation!(r, "rules_rust/rust/runfiles/data/sample.txt").unwrap();
let mut f = File::open(&runfile)
.unwrap_or_else(|e| panic!("Failed to open: {}\n{:?}", runfile.display(), e));
let mut buffer = String::new();
f.read_to_string(&mut buffer).unwrap();
assert_eq!("Example Text!", buffer);
},
);
}
/// `RUNFILES_DIR`, `TEST_SRCDIR`, and `MANIFEST_FILE_ENV_VAR` are not set. This
/// will test the `.runfiles` directory lookup.
///
/// This test is skipped on windows as these directories are not guaranteed
/// to have been created.
#[cfg(not(target_family = "windows"))]
#[test]
fn test_env_nothing_set() {
with_mock_env(
[
(RUNFILES_DIR_ENV_VAR, None::<&str>),
(TEST_SRCDIR_ENV_VAR, None::<&str>),
(MANIFEST_FILE_ENV_VAR, None::<&str>),
],
|| {
let r = Runfiles::create().unwrap();
let mut f =
File::open(rlocation!(r, "rules_rust/rust/runfiles/data/sample.txt").unwrap())
.unwrap();
let mut buffer = String::new();
f.read_to_string(&mut buffer).unwrap();
assert_eq!("Example Text!", buffer);
},
);
}
#[test]
fn test_manifest_parsing() {
let temp_dir = PathBuf::from(std::env::var("TEST_TMPDIR").unwrap());
std::fs::create_dir_all(&temp_dir).unwrap();
let manifest_file = temp_dir.join("test_manifest_parsing.manifest");
std::fs::write(
&manifest_file,
[
"a/b c/d\n",
" a\\sb/c\\nd\\be f g/h\\ni\\bj\n",
"empty-file \n",
]
.join("\n"),
)
.unwrap();
with_mock_env(
[
(MANIFEST_FILE_ENV_VAR, Some(manifest_file.to_str().unwrap())),
(RUNFILES_DIR_ENV_VAR, None::<&str>),
(TEST_SRCDIR_ENV_VAR, None::<&str>),
],
|| {
let r = Runfiles::create().unwrap();
assert_eq!(r.rlocation("a/b"), Some(PathBuf::from("c/d")));
// Note that the `\s` is not escaped in the link, so not testing for it.
assert_eq!(
r.rlocation("a b/c\nd\\e"),
Some(PathBuf::from("f g/h\ni\\j"))
);
assert_eq!(r.rlocation("empty-file"), Some(PathBuf::from("")));
assert_eq!(r.rlocation("does/not/exist"), None);
},
);
}
#[test]
fn test_manifest_based_can_read_data_from_runfiles() {
let mut path_mapping = HashMap::new();
path_mapping.insert("a/b".into(), "c/d".into());
let r = Runfiles {
mode: Mode::ManifestBased(path_mapping),
repo_mapping: RepoMapping::new(),
};
assert_eq!(r.rlocation("a/b"), Some(PathBuf::from("c/d")));
}
/// Direct unit test for the `<binary>.runfiles_manifest` discovery
/// step that backs the build-action-friendly path in
/// `Runfiles::create()`. Built as a pure helper so it doesn't depend
/// on controlling argv[0] (which Bazel's test runner sets in
/// environment-specific ways that vary across platforms and
/// invocations).
#[test]
fn find_runfiles_manifest_for_returns_sibling_when_present() {
let tmp =
PathBuf::from(std::env::var("TEST_TMPDIR").unwrap()).join("manifest_sibling_test");
std::fs::create_dir_all(&tmp).unwrap();
let bin = tmp.join("my_wrapper");
let manifest = tmp.join("my_wrapper.runfiles_manifest");
std::fs::write(&bin, "").unwrap();
std::fs::write(&manifest, "_repo_mapping /irrelevant\n").unwrap();
assert_eq!(find_runfiles_manifest_for(&bin), Some(manifest));
// Without the manifest file, returns None — caller falls through
// to the runfiles-directory walk.
let bin_no_manifest = tmp.join("other_wrapper");
std::fs::write(&bin_no_manifest, "").unwrap();
assert_eq!(find_runfiles_manifest_for(&bin_no_manifest), None);
}
/// Bare-basename argv[0] (i.e. binary found via `$PATH`) has no
/// directory component, so the sibling-manifest lookup has nothing
/// to look next to. `find_runfiles_manifest_from_argv0` falls back
/// to `std::env::current_exe` in that case; this test pins the
/// inner helper's behavior on bare paths.
#[test]
fn find_runfiles_manifest_for_returns_none_for_bare_basename() {
let bare = PathBuf::from("my_wrapper");
assert_eq!(find_runfiles_manifest_for(&bare), None);
}
#[test]
fn test_manifest_based_missing_file() {
let mut path_mapping = HashMap::new();
path_mapping.insert("a/b".into(), "c/d".into());
let r = Runfiles {
mode: Mode::ManifestBased(path_mapping),
repo_mapping: RepoMapping::new(),
};
assert_eq!(r.rlocation("does/not/exist"), None);
}
fn dedent(text: &str) -> String {
text.lines()
.map(|l| l.trim_start())
.collect::<Vec<&str>>()
.join("\n")
}
#[test]
fn test_parse_repo_mapping() {
let temp_dir = PathBuf::from(std::env::var("TEST_TMPDIR").unwrap());
std::fs::create_dir_all(&temp_dir).unwrap();
let valid = temp_dir.join("test_parse_repo_mapping.txt");
std::fs::write(
&valid,
dedent(
r#",rules_rust,rules_rust
bazel_tools,__main__,rules_rust
local_config_cc,rules_rust,rules_rust
local_config_sh,rules_rust,rules_rust
local_config_xcode,rules_rust,rules_rust
platforms,rules_rust,rules_rust
rules_rust_tinyjson,rules_rust,rules_rust
rust_darwin_aarch64__aarch64-apple-darwin__stable_tools,rules_rust,rules_rust
"#,
),
)
.unwrap();
assert_eq!(
parse_repo_mapping(valid),
Ok(RepoMapping {
prefixes: BTreeMap::new(),
exact: HashMap::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()
),
(
(
"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()
)
])
})
);
}
#[test]
fn test_parse_repo_mapping_invalid_file() {
let temp_dir = PathBuf::from(std::env::var("TEST_TMPDIR").unwrap());
std::fs::create_dir_all(&temp_dir).unwrap();
let invalid = temp_dir.join("test_parse_repo_mapping_invalid_file.txt");
assert!(matches!(
parse_repo_mapping(invalid.clone()).err().unwrap(),
RunfilesError::RepoMappingIoError(_)
));
std::fs::write(&invalid, "invalid").unwrap();
assert_eq!(
parse_repo_mapping(invalid),
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")));
}
#[test]
fn test_builder_directory() {
let runfiles_dir = make_runfiles_like_dir("test_builder_directory");
let r = Runfiles::builder()
.directory(&runfiles_dir)
.build()
.unwrap();
let f = rlocation!(r, "rules_rust/rust/runfiles/data/sample.txt").unwrap();
let mut file = File::open(&f)
.unwrap_or_else(|e| panic!("Failed to open file: {}\n{:?}", f.display(), e));
let mut buffer = String::new();
file.read_to_string(&mut buffer).unwrap();
assert_eq!("Example Text!", buffer);
}
/// Point the builder at a nonexistent root and confirm construction
/// still succeeds — the builder must not touch the fs when a source is set.
#[test]
fn test_builder_directory_does_not_touch_fs() {
let r = Runfiles::builder()
.directory("/definitely/does/not/exist/for/runfiles")
.build()
.unwrap();
let p = r.rlocation("some/workspace/data.txt").unwrap();
assert_eq!(
p,
PathBuf::from("/definitely/does/not/exist/for/runfiles/some/workspace/data.txt")
);
}
#[test]
fn test_builder_manifest() {
let r = Runfiles::builder()
.manifest("a/b c/d\n empty-file \n")
.build()
.unwrap();
assert_eq!(r.rlocation("a/b"), Some(PathBuf::from("c/d")));
assert_eq!(r.rlocation("empty-file"), Some(PathBuf::from("")));
assert_eq!(r.rlocation("does/not/exist"), None);
}
#[test]
fn test_builder_manifest_with_repo_mapping() {
let r = Runfiles::builder()
.manifest("canonical/x/y actual/x/y\n")
.repo_mapping("source_repo,apparent_name,canonical\n")
.build()
.unwrap();
assert_eq!(
r.rlocation_from("apparent_name/x/y", "source_repo"),
Some(PathBuf::from("actual/x/y"))
);
assert_eq!(r.rlocation_from("apparent_name/x/y", "other_repo"), None);
}
#[test]
fn test_builder_rejects_invalid_repo_mapping() {
let err = Runfiles::builder()
.directory("/tmp")
.repo_mapping("not,enough")
.build()
.expect_err("Should have failed with an invalid repo mapping");
assert_eq!(err, RunfilesError::RepoMappingInvalidFormat);
}
}