runfiles/
runfiles.rs

1//! Runfiles lookup library for Bazel-built Rust binaries and tests.
2//!
3//! USAGE:
4//!
5//! 1. Depend on this runfiles library from your build rule:
6//! ```python
7//!   rust_binary(
8//!       name = "my_binary",
9//!       ...
10//!       data = ["//path/to/my/data.txt"],
11//!       deps = ["@rules_rust//rust/runfiles"],
12//!   )
13//! ```
14//!
15//! 2. Import the runfiles library.
16//! ```ignore
17//! use runfiles::Runfiles;
18//! ```
19//!
20//! 3. Create a Runfiles object and use `rlocation!`` to look up runfile paths:
21//! ```ignore
22//!
23//! use runfiles::{Runfiles, rlocation};
24//!
25//! let r = Runfiles::create().unwrap();
26//! let path = rlocation!(r, "my_workspace/path/to/my/data.txt").expect("Failed to locate runfile");
27//!
28//! let f = File::open(path).unwrap();
29//!
30//! // ...
31//! ```
32
33use std::collections::HashMap;
34use std::env;
35use std::fs;
36use std::io;
37use std::path::Path;
38use std::path::PathBuf;
39
40const RUNFILES_DIR_ENV_VAR: &str = "RUNFILES_DIR";
41const MANIFEST_FILE_ENV_VAR: &str = "RUNFILES_MANIFEST_FILE";
42const TEST_SRCDIR_ENV_VAR: &str = "TEST_SRCDIR";
43
44#[macro_export]
45macro_rules! rlocation {
46    ($r:expr, $path:expr) => {
47        $r.rlocation_from($path, env!("REPOSITORY_NAME"))
48    };
49}
50
51/// The error type for [Runfiles] construction.
52#[derive(Debug)]
53pub enum RunfilesError {
54    /// Directory based runfiles could not be found.
55    RunfilesDirNotFound,
56
57    /// An [I/O Error](https://doc.rust-lang.org/std/io/struct.Error.html)
58    /// which occurred during the creation of directory-based runfiles.
59    RunfilesDirIoError(io::Error),
60
61    /// An [I/O Error](https://doc.rust-lang.org/std/io/struct.Error.html)
62    /// which occurred during the creation of manifest-file-based runfiles.
63    RunfilesManifestIoError(io::Error),
64
65    /// A manifest file could not be parsed.
66    RunfilesManifestInvalidFormat,
67
68    /// The bzlmod repo-mapping file could not be found.
69    RepoMappingNotFound,
70
71    /// The bzlmod repo-mapping file could not be parsed.
72    RepoMappingInvalidFormat,
73
74    /// An [I/O Error](https://doc.rust-lang.org/std/io/struct.Error.html)
75    /// which occurred during the parsing of a repo-mapping file.
76    RepoMappingIoError(io::Error),
77
78    /// An error indicating a specific Runfile was not found.
79    RunfileNotFound(PathBuf),
80
81    /// An [I/O Error](https://doc.rust-lang.org/std/io/struct.Error.html)
82    /// which occurred when operating with a particular runfile.
83    RunfileIoError(io::Error),
84}
85
86impl std::fmt::Display for RunfilesError {
87    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
88        match self {
89            RunfilesError::RunfilesDirNotFound => write!(f, "RunfilesDirNotFound"),
90            RunfilesError::RunfilesDirIoError(err) => write!(f, "RunfilesDirIoError: {:?}", err),
91            RunfilesError::RunfilesManifestIoError(err) => {
92                write!(f, "RunfilesManifestIoError: {:?}", err)
93            }
94            RunfilesError::RunfilesManifestInvalidFormat => write!(f, "RepoMappingInvalidFormat"),
95            RunfilesError::RepoMappingNotFound => write!(f, "RepoMappingInvalidFormat"),
96            RunfilesError::RepoMappingInvalidFormat => write!(f, "RepoMappingInvalidFormat"),
97            RunfilesError::RepoMappingIoError(err) => write!(f, "RepoMappingIoError: {:?}", err),
98            RunfilesError::RunfileNotFound(path) => {
99                write!(f, "RunfileNotFound: {}", path.display())
100            }
101            RunfilesError::RunfileIoError(err) => write!(f, "RunfileIoError: {:?}", err),
102        }
103    }
104}
105
106impl std::error::Error for RunfilesError {}
107
108impl PartialEq for RunfilesError {
109    fn eq(&self, other: &Self) -> bool {
110        match (self, other) {
111            (Self::RunfilesDirIoError(l0), Self::RunfilesDirIoError(r0)) => {
112                l0.to_string() == r0.to_string()
113            }
114            (Self::RunfilesManifestIoError(l0), Self::RunfilesManifestIoError(r0)) => {
115                l0.to_string() == r0.to_string()
116            }
117            (Self::RepoMappingIoError(l0), Self::RepoMappingIoError(r0)) => {
118                l0.to_string() == r0.to_string()
119            }
120            (Self::RunfileNotFound(l0), Self::RunfileNotFound(r0)) => l0 == r0,
121            (Self::RunfileIoError(l0), Self::RunfileIoError(r0)) => {
122                l0.to_string() == r0.to_string()
123            }
124            _ => core::mem::discriminant(self) == core::mem::discriminant(other),
125        }
126    }
127}
128
129/// A specialized [`std::result::Result`] type for
130pub type Result<T> = std::result::Result<T, RunfilesError>;
131
132#[derive(Debug)]
133enum Mode {
134    /// Runfiles located in a directory indicated by the `RUNFILES_DIR` environment
135    /// variable or a neighboring `*.runfiles` directory to the executable.
136    DirectoryBased(PathBuf),
137
138    /// Runfiles represented as a mapping of `rlocationpath` to real paths indicated
139    /// by the `RUNFILES_MANIFEST_FILE` environment variable.
140    ManifestBased(HashMap<PathBuf, PathBuf>),
141}
142
143type RepoMappingKey = (String, String);
144type RepoMapping = HashMap<RepoMappingKey, String>;
145
146/// An interface for accessing to [Bazel runfiles](https://bazel.build/extending/rules#runfiles).
147#[derive(Debug)]
148pub struct Runfiles {
149    mode: Mode,
150    repo_mapping: RepoMapping,
151}
152
153impl Runfiles {
154    /// Creates a manifest based Runfiles object when
155    /// RUNFILES_MANIFEST_FILE environment variable is present,
156    /// or a directory based Runfiles object otherwise.
157    pub fn create() -> Result<Self> {
158        let mode = if let Some(manifest_file) = std::env::var_os(MANIFEST_FILE_ENV_VAR) {
159            Self::create_manifest_based(Path::new(&manifest_file))?
160        } else {
161            let dir = find_runfiles_dir()?;
162            let manifest_path = dir.join("MANIFEST");
163            match manifest_path.exists() {
164                true => Self::create_manifest_based(&manifest_path)?,
165                false => Mode::DirectoryBased(dir),
166            }
167        };
168
169        let repo_mapping = raw_rlocation(&mode, "_repo_mapping")
170            // This is the only place directory based runfiles might do file IO for a runfile. In the
171            // event that a `_repo_mapping` file does not exist, a default map should be created. Otherwise
172            // if the file is known to exist, parse it and raise errors for users should parsing fail.
173            .filter(|f| f.exists())
174            .map(parse_repo_mapping)
175            .transpose()?
176            .unwrap_or_default();
177
178        Ok(Runfiles { mode, repo_mapping })
179    }
180
181    fn create_manifest_based(manifest_path: &Path) -> Result<Mode> {
182        let manifest_content = std::fs::read_to_string(manifest_path)
183            .map_err(RunfilesError::RunfilesManifestIoError)?;
184        let path_mapping = manifest_content
185            .lines()
186            .flat_map(|line| {
187                let pair = line
188                    .split_once(' ')
189                    .ok_or(RunfilesError::RunfilesManifestInvalidFormat)?;
190                Ok::<(PathBuf, PathBuf), RunfilesError>((pair.0.into(), pair.1.into()))
191            })
192            .collect::<HashMap<_, _>>();
193        Ok(Mode::ManifestBased(path_mapping))
194    }
195
196    /// Returns the runtime path of a runfile.
197    ///
198    /// Runfiles are data-dependencies of Bazel-built binaries and tests.
199    /// The returned path may not be valid. The caller should check the path's
200    /// validity and that the path exists.
201    /// @deprecated - this is not bzlmod-aware. Prefer the `rlocation!` macro or `rlocation_from`
202    pub fn rlocation(&self, path: impl AsRef<Path>) -> Option<PathBuf> {
203        let path = path.as_ref();
204        if path.is_absolute() {
205            return Some(path.to_path_buf());
206        }
207        raw_rlocation(&self.mode, path)
208    }
209
210    /// Returns the runtime path of a runfile.
211    ///
212    /// Runfiles are data-dependencies of Bazel-built binaries and tests.
213    /// The returned path may not be valid. The caller should check the path's
214    /// validity and that the path exists.
215    ///
216    /// Typically this should be used via the `rlocation!` macro to properly set source_repo.
217    pub fn rlocation_from(&self, path: impl AsRef<Path>, source_repo: &str) -> Option<PathBuf> {
218        let path = path.as_ref();
219        if path.is_absolute() {
220            return Some(path.to_path_buf());
221        }
222
223        let path_str = path.to_str().expect("Should be valid UTF8");
224        let (repo_alias, repo_path): (&str, Option<&str>) = match path_str.split_once('/') {
225            Some((name, alias)) => (name, Some(alias)),
226            None => (path_str, None),
227        };
228        let key: (String, String) = (source_repo.into(), repo_alias.into());
229        if let Some(target_repo_directory) = self.repo_mapping.get(&key) {
230            match repo_path {
231                Some(repo_path) => {
232                    raw_rlocation(&self.mode, format!("{target_repo_directory}/{repo_path}"))
233                }
234                None => raw_rlocation(&self.mode, target_repo_directory),
235            }
236        } else {
237            raw_rlocation(&self.mode, path)
238        }
239    }
240}
241
242fn raw_rlocation(mode: &Mode, path: impl AsRef<Path>) -> Option<PathBuf> {
243    let path = path.as_ref();
244    match mode {
245        Mode::DirectoryBased(runfiles_dir) => Some(runfiles_dir.join(path)),
246        Mode::ManifestBased(path_mapping) => path_mapping.get(path).cloned(),
247    }
248}
249
250fn parse_repo_mapping(path: PathBuf) -> Result<RepoMapping> {
251    let mut repo_mapping = RepoMapping::new();
252
253    for line in std::fs::read_to_string(path)
254        .map_err(RunfilesError::RepoMappingIoError)?
255        .lines()
256    {
257        let parts: Vec<&str> = line.splitn(3, ',').collect();
258        if parts.len() < 3 {
259            return Err(RunfilesError::RepoMappingInvalidFormat);
260        }
261        repo_mapping.insert((parts[0].into(), parts[1].into()), parts[2].into());
262    }
263
264    Ok(repo_mapping)
265}
266
267/// Returns the .runfiles directory for the currently executing binary.
268pub fn find_runfiles_dir() -> Result<PathBuf> {
269    assert!(
270        std::env::var_os(MANIFEST_FILE_ENV_VAR).is_none(),
271        "Unexpected call when {} exists",
272        MANIFEST_FILE_ENV_VAR
273    );
274
275    // If Bazel told us about the runfiles dir, use that without looking further.
276    if let Some(runfiles_dir) = std::env::var_os(RUNFILES_DIR_ENV_VAR).map(PathBuf::from) {
277        if runfiles_dir.is_dir() {
278            return Ok(runfiles_dir);
279        }
280    }
281    if let Some(test_srcdir) = std::env::var_os(TEST_SRCDIR_ENV_VAR).map(PathBuf::from) {
282        if test_srcdir.is_dir() {
283            return Ok(test_srcdir);
284        }
285    }
286
287    // Consume the first argument (argv[0])
288    let exec_path = std::env::args().next().expect("arg 0 was not set");
289
290    let current_dir =
291        env::current_dir().expect("The current working directory is always expected to be set.");
292
293    let mut binary_path = PathBuf::from(&exec_path);
294    loop {
295        // Check for our neighboring `${binary}.runfiles` directory.
296        let mut runfiles_name = binary_path.file_name().unwrap().to_owned();
297        runfiles_name.push(".runfiles");
298
299        let runfiles_path = binary_path.with_file_name(&runfiles_name);
300        if runfiles_path.is_dir() {
301            return Ok(runfiles_path);
302        }
303
304        // Check if we're already under a `*.runfiles` directory.
305        {
306            // TODO: 1.28 adds Path::ancestors() which is a little simpler.
307            let mut next = binary_path.parent();
308            while let Some(ancestor) = next {
309                if ancestor
310                    .file_name()
311                    .is_some_and(|f| f.to_string_lossy().ends_with(".runfiles"))
312                {
313                    return Ok(ancestor.to_path_buf());
314                }
315                next = ancestor.parent();
316            }
317        }
318
319        if !fs::symlink_metadata(&binary_path)
320            .map_err(RunfilesError::RunfilesDirIoError)?
321            .file_type()
322            .is_symlink()
323        {
324            break;
325        }
326        // Follow symlinks and keep looking.
327        let link_target = binary_path
328            .read_link()
329            .map_err(RunfilesError::RunfilesDirIoError)?;
330        binary_path = if link_target.is_absolute() {
331            link_target
332        } else {
333            let link_dir = binary_path.parent().unwrap();
334            current_dir.join(link_dir).join(link_target)
335        }
336    }
337
338    Err(RunfilesError::RunfilesDirNotFound)
339}
340
341#[cfg(test)]
342mod test {
343    use super::*;
344
345    use std::ffi::OsStr;
346    use std::ffi::OsString;
347    use std::fs::File;
348    use std::hash::Hash;
349    use std::io::prelude::*;
350    use std::sync::{Mutex, OnceLock};
351
352    /// A mutex used to guard
353    static GLOBAL_MUTEX: OnceLock<Mutex<i32>> = OnceLock::new();
354
355    /// Mock out environment variables for a given body fo work. Very similar to
356    /// [temp-env](https://crates.io/crates/temp-env).
357    fn with_mock_env<K, V, F, R>(kvs: impl AsRef<[(K, Option<V>)]>, closure: F) -> R
358    where
359        K: AsRef<OsStr> + Clone + Eq + Hash,
360        V: AsRef<OsStr> + Clone,
361        F: FnOnce() -> R,
362    {
363        let mtx = GLOBAL_MUTEX.get_or_init(|| Mutex::new(0));
364
365        // Ignore poisoning as it's expected to be another test failing an assertion.
366        let _guard = mtx.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
367
368        // track the original state of the environment.
369        let mut old_env = HashMap::new();
370
371        // Replace or remove requested environment variables.
372        for (env, val) in kvs.as_ref() {
373            // Track the original state of the variable.
374            match std::env::var_os(env) {
375                Some(v) => old_env.insert(env, Some(v)),
376                None => old_env.insert(env, None::<OsString>),
377            };
378
379            match val {
380                Some(v) => std::env::set_var(env, v),
381                None => std::env::remove_var(env),
382            }
383        }
384
385        // Run requested work.
386        let result = closure();
387
388        // Restore original environment
389        for (env, val) in old_env {
390            match val {
391                Some(v) => std::env::set_var(env, v),
392                None => std::env::remove_var(env),
393            }
394        }
395
396        result
397    }
398
399    #[test]
400    fn test_mock_env() {
401        let original_name = std::env::var("TEST_WORKSPACE").unwrap();
402        assert!(
403            !original_name.is_empty(),
404            "In Bazel tests, `TEST_WORKSPACE` is expected to be populated."
405        );
406
407        let mocked_name = with_mock_env([("TEST_WORKSPACE", Some("foobar"))], || {
408            std::env::var("TEST_WORKSPACE").unwrap()
409        });
410
411        assert_eq!(mocked_name, "foobar");
412        assert_eq!(original_name, std::env::var("TEST_WORKSPACE").unwrap());
413    }
414
415    /// Create a temp directory to act as a runfiles directory for testing
416    /// [super::Mode::DirectoryBased] style runfiles.
417    fn make_runfiles_like_dir(name: &str) -> String {
418        with_mock_env([("FAKE", None::<&str>)], || {
419            let r = Runfiles::create().unwrap();
420
421            let path = "rules_rust/rust/runfiles/data/sample.txt";
422            let f = rlocation!(r, path).unwrap();
423
424            let temp_dir = PathBuf::from(std::env::var("TEST_TMPDIR").unwrap());
425            let runfiles_dir = temp_dir.join(name);
426            let test_path = runfiles_dir.join(path);
427            if let Some(parent) = test_path.parent() {
428                std::fs::create_dir_all(parent).expect("Failed to create test path parents.");
429            }
430
431            std::fs::copy(f, test_path).expect("Failed to copy test file");
432
433            runfiles_dir.to_str().unwrap().to_string()
434        })
435    }
436
437    /// Test the general behavior of runfiles. The behavior of runfiles will change
438    /// depending on the system but each mode is explicitly covered in other tests.
439    #[test]
440    fn test_standard_lookup() {
441        let r = Runfiles::create().unwrap();
442
443        let f = rlocation!(r, "rules_rust/rust/runfiles/data/sample.txt").unwrap();
444
445        let mut f = File::open(&f)
446            .unwrap_or_else(|e| panic!("Failed to open file: {}\n{:?}", f.display(), e));
447
448        let mut buffer = String::new();
449        f.read_to_string(&mut buffer).unwrap();
450
451        assert_eq!("Example Text!", buffer);
452    }
453
454    /// Only `RUNFILES_DIR` is set.
455    #[test]
456    fn test_env_only_runfiles_dir() {
457        let runfiles_dir = make_runfiles_like_dir("test_env_only_runfiles_dir");
458
459        with_mock_env(
460            [
461                (MANIFEST_FILE_ENV_VAR, None::<&str>),
462                (RUNFILES_DIR_ENV_VAR, Some(runfiles_dir.as_str())),
463                (TEST_SRCDIR_ENV_VAR, None::<&str>),
464            ],
465            || {
466                let r = Runfiles::create().unwrap();
467
468                let d = rlocation!(r, "rules_rust").unwrap();
469                let f = rlocation!(r, "rules_rust/rust/runfiles/data/sample.txt").unwrap();
470                assert_eq!(d.join("rust/runfiles/data/sample.txt"), f);
471
472                let mut f = File::open(&f)
473                    .unwrap_or_else(|e| panic!("Failed to open file: {}\n{:?}", f.display(), e));
474
475                let mut buffer = String::new();
476                f.read_to_string(&mut buffer).unwrap();
477
478                assert_eq!("Example Text!", buffer);
479            },
480        );
481    }
482
483    /// Only `TEST_SRCDIR` is set.
484    #[test]
485    fn test_env_only_test_srcdir() {
486        let runfiles_dir = make_runfiles_like_dir("test_env_only_test_srcdir");
487
488        with_mock_env(
489            [
490                (MANIFEST_FILE_ENV_VAR, None::<&str>),
491                (RUNFILES_DIR_ENV_VAR, None::<&str>),
492                (TEST_SRCDIR_ENV_VAR, Some(runfiles_dir.as_str())),
493            ],
494            || {
495                let r = Runfiles::create().unwrap();
496
497                let runfile = rlocation!(r, "rules_rust/rust/runfiles/data/sample.txt").unwrap();
498
499                let mut f = File::open(&runfile)
500                    .unwrap_or_else(|e| panic!("Failed to open: {}\n{:?}", runfile.display(), e));
501
502                let mut buffer = String::new();
503                f.read_to_string(&mut buffer).unwrap();
504
505                assert_eq!("Example Text!", buffer);
506            },
507        );
508    }
509
510    /// `RUNFILES_DIR`, `TEST_SRCDIR`, and `MANIFEST_FILE_ENV_VAR` are not set. This
511    /// will test the `.runfiles` directory lookup.
512    ///
513    /// This test is skipped on windows as these directories are not guaranteed
514    /// to have been created.
515    #[cfg(not(target_family = "windows"))]
516    #[test]
517    fn test_env_nothing_set() {
518        with_mock_env(
519            [
520                (RUNFILES_DIR_ENV_VAR, None::<&str>),
521                (TEST_SRCDIR_ENV_VAR, None::<&str>),
522                (MANIFEST_FILE_ENV_VAR, None::<&str>),
523            ],
524            || {
525                let r = Runfiles::create().unwrap();
526
527                let mut f =
528                    File::open(rlocation!(r, "rules_rust/rust/runfiles/data/sample.txt").unwrap())
529                        .unwrap();
530
531                let mut buffer = String::new();
532                f.read_to_string(&mut buffer).unwrap();
533
534                assert_eq!("Example Text!", buffer);
535            },
536        );
537    }
538
539    #[test]
540    fn test_manifest_based_can_read_data_from_runfiles() {
541        let mut path_mapping = HashMap::new();
542        path_mapping.insert("a/b".into(), "c/d".into());
543        let r = Runfiles {
544            mode: Mode::ManifestBased(path_mapping),
545            repo_mapping: RepoMapping::new(),
546        };
547
548        assert_eq!(r.rlocation("a/b"), Some(PathBuf::from("c/d")));
549    }
550
551    #[test]
552    fn test_manifest_based_missing_file() {
553        let mut path_mapping = HashMap::new();
554        path_mapping.insert("a/b".into(), "c/d".into());
555        let r = Runfiles {
556            mode: Mode::ManifestBased(path_mapping),
557            repo_mapping: RepoMapping::new(),
558        };
559
560        assert_eq!(r.rlocation("does/not/exist"), None);
561    }
562
563    fn dedent(text: &str) -> String {
564        text.lines()
565            .map(|l| l.trim_start())
566            .collect::<Vec<&str>>()
567            .join("\n")
568    }
569
570    #[test]
571    fn test_parse_repo_mapping() {
572        let temp_dir = PathBuf::from(std::env::var("TEST_TMPDIR").unwrap());
573        std::fs::create_dir_all(&temp_dir).unwrap();
574
575        let valid = temp_dir.join("test_parse_repo_mapping.txt");
576        std::fs::write(
577            &valid,
578            dedent(
579                r#",rules_rust,rules_rust
580            bazel_tools,__main__,rules_rust
581            local_config_cc,rules_rust,rules_rust
582            local_config_sh,rules_rust,rules_rust
583            local_config_xcode,rules_rust,rules_rust
584            platforms,rules_rust,rules_rust
585            rules_rust_tinyjson,rules_rust,rules_rust
586            rust_darwin_aarch64__aarch64-apple-darwin__stable_tools,rules_rust,rules_rust
587            "#,
588            ),
589        )
590        .unwrap();
591
592        assert_eq!(
593            parse_repo_mapping(valid),
594            Ok(RepoMapping::from([
595                (
596                    ("local_config_xcode".to_owned(), "rules_rust".to_owned()),
597                    "rules_rust".to_owned()
598                ),
599                (
600                    ("platforms".to_owned(), "rules_rust".to_owned()),
601                    "rules_rust".to_owned()
602                ),
603                (
604                    (
605                        "rust_darwin_aarch64__aarch64-apple-darwin__stable_tools".to_owned(),
606                        "rules_rust".to_owned()
607                    ),
608                    "rules_rust".to_owned()
609                ),
610                (
611                    ("rules_rust_tinyjson".to_owned(), "rules_rust".to_owned()),
612                    "rules_rust".to_owned()
613                ),
614                (
615                    ("local_config_sh".to_owned(), "rules_rust".to_owned()),
616                    "rules_rust".to_owned()
617                ),
618                (
619                    ("bazel_tools".to_owned(), "__main__".to_owned()),
620                    "rules_rust".to_owned()
621                ),
622                (
623                    ("local_config_cc".to_owned(), "rules_rust".to_owned()),
624                    "rules_rust".to_owned()
625                ),
626                (
627                    ("".to_owned(), "rules_rust".to_owned()),
628                    "rules_rust".to_owned()
629                )
630            ]))
631        );
632    }
633
634    #[test]
635    fn test_parse_repo_mapping_invalid_file() {
636        let temp_dir = PathBuf::from(std::env::var("TEST_TMPDIR").unwrap());
637        std::fs::create_dir_all(&temp_dir).unwrap();
638
639        let invalid = temp_dir.join("test_parse_repo_mapping_invalid_file.txt");
640
641        assert!(matches!(
642            parse_repo_mapping(invalid.clone()).err().unwrap(),
643            RunfilesError::RepoMappingIoError(_)
644        ));
645
646        std::fs::write(&invalid, "invalid").unwrap();
647
648        assert_eq!(
649            parse_repo_mapping(invalid),
650            Err(RunfilesError::RepoMappingInvalidFormat),
651        );
652    }
653}