1use 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#[derive(Debug)]
53pub enum RunfilesError {
54 RunfilesDirNotFound,
56
57 RunfilesDirIoError(io::Error),
60
61 RunfilesManifestIoError(io::Error),
64
65 RunfilesManifestInvalidFormat,
67
68 RepoMappingNotFound,
70
71 RepoMappingInvalidFormat,
73
74 RepoMappingIoError(io::Error),
77
78 RunfileNotFound(PathBuf),
80
81 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
129pub type Result<T> = std::result::Result<T, RunfilesError>;
131
132#[derive(Debug)]
133enum Mode {
134 DirectoryBased(PathBuf),
137
138 ManifestBased(HashMap<PathBuf, PathBuf>),
141}
142
143type RepoMappingKey = (String, String);
144type RepoMapping = HashMap<RepoMappingKey, String>;
145
146#[derive(Debug)]
148pub struct Runfiles {
149 mode: Mode,
150 repo_mapping: RepoMapping,
151}
152
153impl Runfiles {
154 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 .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 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 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
267pub 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 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 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 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 {
306 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 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 static GLOBAL_MUTEX: OnceLock<Mutex<i32>> = OnceLock::new();
354
355 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 let _guard = mtx.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
367
368 let mut old_env = HashMap::new();
370
371 for (env, val) in kvs.as_ref() {
373 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 let result = closure();
387
388 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 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]
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 #[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 #[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 #[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}