| //! 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); |
| } |
| } |