xref: /aosp_15_r20/external/bazelbuild-rules_rust/tools/runfiles/runfiles.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
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//tools/runfiles"],
12 //!       )
13 //!     ```
14 //!
15 //! 2.  Import the runfiles library.
16 //!     ```ignore
17 //!     extern crate runfiles;
18 //!
19 //!     use runfiles::Runfiles;
20 //!     ```
21 //!
22 //! 3.  Create a Runfiles object and use `rlocation!`` to look up runfile paths:
23 //!     ```ignore -- This doesn't work under rust_doc_test because argv[0] is not what we expect.
24 //!
25 //!     use runfiles::{Runfiles, rlocation};
26 //!
27 //!     let r = Runfiles::create().unwrap();
28 //!     let path = rlocation!(r, "my_workspace/path/to/my/data.txt");
29 //!
30 //!     let f = File::open(path).unwrap();
31 //!     // ...
32 //!     ```
33 
34 use std::collections::HashMap;
35 use std::env;
36 use std::fs;
37 use std::io;
38 use std::path::Path;
39 use std::path::PathBuf;
40 
41 const RUNFILES_DIR_ENV_VAR: &str = "RUNFILES_DIR";
42 const MANIFEST_FILE_ENV_VAR: &str = "RUNFILES_MANIFEST_FILE";
43 const TEST_SRCDIR_ENV_VAR: &str = "TEST_SRCDIR";
44 
45 #[macro_export]
46 macro_rules! rlocation {
47     ($r:expr, $path:expr) => {
48         $r.rlocation_from($path, env!("REPOSITORY_NAME"))
49     };
50 }
51 
52 #[derive(Debug)]
53 enum Mode {
54     DirectoryBased(PathBuf),
55     ManifestBased(HashMap<PathBuf, PathBuf>),
56 }
57 
58 type RepoMappingKey = (String, String);
59 type RepoMapping = HashMap<RepoMappingKey, String>;
60 
61 #[derive(Debug)]
62 pub struct Runfiles {
63     mode: Mode,
64     repo_mapping: RepoMapping,
65 }
66 
67 impl Runfiles {
68     /// Creates a manifest based Runfiles object when
69     /// RUNFILES_MANIFEST_FILE environment variable is present,
70     /// or a directory based Runfiles object otherwise.
create() -> io::Result<Self>71     pub fn create() -> io::Result<Self> {
72         let mode = if let Some(manifest_file) = std::env::var_os(MANIFEST_FILE_ENV_VAR) {
73             Self::create_manifest_based(Path::new(&manifest_file))?
74         } else {
75             Mode::DirectoryBased(find_runfiles_dir()?)
76         };
77 
78         let repo_mapping = parse_repo_mapping(raw_rlocation(&mode, "_repo_mapping"))
79             .unwrap_or_else(|_| {
80                 println!("No repo mapping found!");
81                 RepoMapping::new()
82             });
83 
84         Ok(Runfiles { mode, repo_mapping })
85     }
86 
create_manifest_based(manifest_path: &Path) -> io::Result<Mode>87     fn create_manifest_based(manifest_path: &Path) -> io::Result<Mode> {
88         let manifest_content = std::fs::read_to_string(manifest_path)?;
89         let path_mapping = manifest_content
90             .lines()
91             .map(|line| {
92                 let pair = line
93                     .split_once(' ')
94                     .expect("manifest file contained unexpected content");
95                 (pair.0.into(), pair.1.into())
96             })
97             .collect::<HashMap<_, _>>();
98         Ok(Mode::ManifestBased(path_mapping))
99     }
100 
101     /// Returns the runtime path of a runfile.
102     ///
103     /// Runfiles are data-dependencies of Bazel-built binaries and tests.
104     /// The returned path may not be valid. The caller should check the path's
105     /// validity and that the path exists.
106     /// @deprecated - this is not bzlmod-aware. Prefer the `rlocation!` macro or `rlocation_from`
rlocation(&self, path: impl AsRef<Path>) -> PathBuf107     pub fn rlocation(&self, path: impl AsRef<Path>) -> PathBuf {
108         let path = path.as_ref();
109         if path.is_absolute() {
110             return path.to_path_buf();
111         }
112         raw_rlocation(&self.mode, path)
113     }
114 
115     /// Returns the runtime path of a runfile.
116     ///
117     /// Runfiles are data-dependencies of Bazel-built binaries and tests.
118     /// The returned path may not be valid. The caller should check the path's
119     /// validity and that the path exists.
120     ///
121     /// Typically this should be used via the `rlocation!` macro to properly set source_repo.
rlocation_from(&self, path: impl AsRef<Path>, source_repo: &str) -> PathBuf122     pub fn rlocation_from(&self, path: impl AsRef<Path>, source_repo: &str) -> PathBuf {
123         let path = path.as_ref();
124         if path.is_absolute() {
125             return path.to_path_buf();
126         }
127 
128         let path_str = path.to_str().expect("Should be valid UTF8");
129         let (repo_alias, repo_path): (&str, Option<&str>) = match path_str.split_once('/') {
130             Some((name, alias)) => (name, Some(alias)),
131             None => (path_str, None),
132         };
133         let key: (String, String) = (source_repo.into(), repo_alias.into());
134         if let Some(target_repo_directory) = self.repo_mapping.get(&key) {
135             match repo_path {
136                 Some(repo_path) => {
137                     raw_rlocation(&self.mode, format!("{target_repo_directory}/{repo_path}"))
138                 }
139                 None => raw_rlocation(&self.mode, target_repo_directory),
140             }
141         } else {
142             raw_rlocation(&self.mode, path)
143         }
144     }
145 }
146 
raw_rlocation(mode: &Mode, path: impl AsRef<Path>) -> PathBuf147 fn raw_rlocation(mode: &Mode, path: impl AsRef<Path>) -> PathBuf {
148     let path = path.as_ref();
149     match mode {
150         Mode::DirectoryBased(runfiles_dir) => runfiles_dir.join(path),
151         Mode::ManifestBased(path_mapping) => path_mapping
152             .get(path)
153             .unwrap_or_else(|| panic!("Path {} not found among runfiles.", path.to_string_lossy()))
154             .clone(),
155     }
156 }
157 
parse_repo_mapping(path: PathBuf) -> io::Result<RepoMapping>158 fn parse_repo_mapping(path: PathBuf) -> io::Result<RepoMapping> {
159     let mut repo_mapping = RepoMapping::new();
160 
161     for line in std::fs::read_to_string(path)?.lines() {
162         let parts: Vec<&str> = line.splitn(3, ',').collect();
163         if parts.len() < 3 {
164             return Err(make_io_error("Malformed repo_mapping file"));
165         }
166         repo_mapping.insert((parts[0].into(), parts[1].into()), parts[2].into());
167     }
168 
169     Ok(repo_mapping)
170 }
171 
172 /// Returns the .runfiles directory for the currently executing binary.
find_runfiles_dir() -> io::Result<PathBuf>173 pub fn find_runfiles_dir() -> io::Result<PathBuf> {
174     assert!(std::env::var_os(MANIFEST_FILE_ENV_VAR).is_none());
175 
176     // If bazel told us about the runfiles dir, use that without looking further.
177     if let Some(runfiles_dir) = std::env::var_os(RUNFILES_DIR_ENV_VAR).map(PathBuf::from) {
178         if runfiles_dir.is_dir() {
179             return Ok(runfiles_dir);
180         }
181     }
182     if let Some(test_srcdir) = std::env::var_os(TEST_SRCDIR_ENV_VAR).map(PathBuf::from) {
183         if test_srcdir.is_dir() {
184             return Ok(test_srcdir);
185         }
186     }
187 
188     // Consume the first argument (argv[0])
189     let exec_path = std::env::args().next().expect("arg 0 was not set");
190 
191     let mut binary_path = PathBuf::from(&exec_path);
192     loop {
193         // Check for our neighboring $binary.runfiles directory.
194         let mut runfiles_name = binary_path.file_name().unwrap().to_owned();
195         runfiles_name.push(".runfiles");
196 
197         let runfiles_path = binary_path.with_file_name(&runfiles_name);
198         if runfiles_path.is_dir() {
199             return Ok(runfiles_path);
200         }
201 
202         // Check if we're already under a *.runfiles directory.
203         {
204             // TODO: 1.28 adds Path::ancestors() which is a little simpler.
205             let mut next = binary_path.parent();
206             while let Some(ancestor) = next {
207                 if ancestor
208                     .file_name()
209                     .map_or(false, |f| f.to_string_lossy().ends_with(".runfiles"))
210                 {
211                     return Ok(ancestor.to_path_buf());
212                 }
213                 next = ancestor.parent();
214             }
215         }
216 
217         if !fs::symlink_metadata(&binary_path)?.file_type().is_symlink() {
218             break;
219         }
220         // Follow symlinks and keep looking.
221         let link_target = binary_path.read_link()?;
222         binary_path = if link_target.is_absolute() {
223             link_target
224         } else {
225             let link_dir = binary_path.parent().unwrap();
226             env::current_dir()?.join(link_dir).join(link_target)
227         }
228     }
229 
230     Err(make_io_error("failed to find .runfiles directory"))
231 }
232 
make_io_error(msg: &str) -> io::Error233 fn make_io_error(msg: &str) -> io::Error {
234     io::Error::new(io::ErrorKind::Other, msg)
235 }
236 
237 #[cfg(test)]
238 mod test {
239     use super::*;
240 
241     use std::fs::File;
242     use std::io::prelude::*;
243 
244     #[test]
test_can_read_data_from_runfiles()245     fn test_can_read_data_from_runfiles() {
246         // We want to run multiple test cases with different environment variables set. Since
247         // environment variables are global state, we need to ensure the test cases do not run
248         // concurrently. Rust runs tests in parallel and does not provide an easy way to synchronise
249         // them, so we run all test cases in the same #[test] function.
250 
251         let test_srcdir =
252             env::var_os(TEST_SRCDIR_ENV_VAR).expect("bazel did not provide TEST_SRCDIR");
253         let runfiles_dir =
254             env::var_os(RUNFILES_DIR_ENV_VAR).expect("bazel did not provide RUNFILES_DIR");
255         let runfiles_manifest_file = env::var_os(MANIFEST_FILE_ENV_VAR).unwrap_or("".into());
256 
257         // Test case 1: Only $RUNFILES_DIR is set.
258         {
259             env::remove_var(TEST_SRCDIR_ENV_VAR);
260             env::remove_var(MANIFEST_FILE_ENV_VAR);
261             let r = Runfiles::create().unwrap();
262 
263             let d = rlocation!(r, "rules_rust");
264             let f = rlocation!(r, "rules_rust/tools/runfiles/data/sample.txt");
265             assert_eq!(d.join("tools/runfiles/data/sample.txt"), f);
266 
267             let mut f = File::open(f).unwrap();
268 
269             let mut buffer = String::new();
270             f.read_to_string(&mut buffer).unwrap();
271 
272             assert_eq!("Example Text!", buffer);
273             env::set_var(TEST_SRCDIR_ENV_VAR, &test_srcdir);
274             env::set_var(MANIFEST_FILE_ENV_VAR, &runfiles_manifest_file);
275         }
276         // Test case 2: Only $TEST_SRCDIR is set.
277         {
278             env::remove_var(RUNFILES_DIR_ENV_VAR);
279             env::remove_var(MANIFEST_FILE_ENV_VAR);
280             let r = Runfiles::create().unwrap();
281 
282             let mut f =
283                 File::open(rlocation!(r, "rules_rust/tools/runfiles/data/sample.txt")).unwrap();
284 
285             let mut buffer = String::new();
286             f.read_to_string(&mut buffer).unwrap();
287 
288             assert_eq!("Example Text!", buffer);
289             env::set_var(RUNFILES_DIR_ENV_VAR, &runfiles_dir);
290             env::set_var(MANIFEST_FILE_ENV_VAR, &runfiles_manifest_file);
291         }
292 
293         // Test case 3: Neither are set
294         {
295             env::remove_var(RUNFILES_DIR_ENV_VAR);
296             env::remove_var(TEST_SRCDIR_ENV_VAR);
297             env::remove_var(MANIFEST_FILE_ENV_VAR);
298 
299             let r = Runfiles::create().unwrap();
300 
301             let mut f =
302                 File::open(rlocation!(r, "rules_rust/tools/runfiles/data/sample.txt")).unwrap();
303 
304             let mut buffer = String::new();
305             f.read_to_string(&mut buffer).unwrap();
306 
307             assert_eq!("Example Text!", buffer);
308 
309             env::set_var(TEST_SRCDIR_ENV_VAR, &test_srcdir);
310             env::set_var(RUNFILES_DIR_ENV_VAR, &runfiles_dir);
311             env::set_var(MANIFEST_FILE_ENV_VAR, &runfiles_manifest_file);
312         }
313     }
314 
315     #[test]
test_manifest_based_can_read_data_from_runfiles()316     fn test_manifest_based_can_read_data_from_runfiles() {
317         let mut path_mapping = HashMap::new();
318         path_mapping.insert("a/b".into(), "c/d".into());
319         let r = Runfiles {
320             mode: Mode::ManifestBased(path_mapping),
321             repo_mapping: RepoMapping::new(),
322         };
323 
324         assert_eq!(r.rlocation("a/b"), PathBuf::from("c/d"));
325     }
326 }
327