• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
26 //!
27 //!     let r = Runfiles::create().unwrap();
28 //!     let path = r.rlocation("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::ffi::OsString;
37 use std::fs;
38 use std::io;
39 use std::path::Path;
40 use std::path::PathBuf;
41 
42 const RUNFILES_DIR_ENV_VAR: &str = "RUNFILES_DIR";
43 const MANIFEST_FILE_ENV_VAR: &str = "RUNFILES_MANIFEST_FILE";
44 const MANIFEST_ONLY_ENV_VAR: &str = "RUNFILES_MANIFEST_ONLY";
45 const TEST_SRCDIR_ENV_VAR: &str = "TEST_SRCDIR";
46 
47 #[derive(Debug)]
48 enum Mode {
49     DirectoryBased(PathBuf),
50     ManifestBased(HashMap<PathBuf, PathBuf>),
51 }
52 
53 #[derive(Debug)]
54 pub struct Runfiles {
55     mode: Mode,
56 }
57 
58 impl Runfiles {
59     /// Creates a manifest based Runfiles object when
60     /// RUNFILES_MANIFEST_ONLY environment variable is present,
61     /// or a directory based Runfiles object otherwise.
create() -> io::Result<Self>62     pub fn create() -> io::Result<Self> {
63         if is_manifest_only() {
64             Self::create_manifest_based()
65         } else {
66             Self::create_directory_based()
67         }
68     }
69 
create_directory_based() -> io::Result<Self>70     fn create_directory_based() -> io::Result<Self> {
71         Ok(Runfiles {
72             mode: Mode::DirectoryBased(find_runfiles_dir()?),
73         })
74     }
75 
create_manifest_based() -> io::Result<Self>76     fn create_manifest_based() -> io::Result<Self> {
77         let manifest_path = find_manifest_path()?;
78         let manifest_content = std::fs::read_to_string(manifest_path)?;
79         let path_mapping = manifest_content
80             .lines()
81             .map(|line| {
82                 let pair = line
83                     .split_once(' ')
84                     .expect("manifest file contained unexpected content");
85                 (pair.0.into(), pair.1.into())
86             })
87             .collect::<HashMap<_, _>>();
88         Ok(Runfiles {
89             mode: Mode::ManifestBased(path_mapping),
90         })
91     }
92 
93     /// Returns the runtime path of a runfile.
94     ///
95     /// Runfiles are data-dependencies of Bazel-built binaries and tests.
96     /// The returned path may not be valid. The caller should check the path's
97     /// validity and that the path exists.
rlocation(&self, path: impl AsRef<Path>) -> PathBuf98     pub fn rlocation(&self, path: impl AsRef<Path>) -> PathBuf {
99         let path = path.as_ref();
100         if path.is_absolute() {
101             return path.to_path_buf();
102         }
103         match &self.mode {
104             Mode::DirectoryBased(runfiles_dir) => runfiles_dir.join(path),
105             Mode::ManifestBased(path_mapping) => path_mapping
106                 .get(path)
107                 .unwrap_or_else(|| {
108                     panic!("Path {} not found among runfiles.", path.to_string_lossy())
109                 })
110                 .clone(),
111         }
112     }
113 
114     /// Returns the canonical name of the caller's Bazel repository.
current_repository(&self) -> &str115     pub fn current_repository(&self) -> &str {
116         // This value must match the value of `_RULES_RUST_RUNFILES_WORKSPACE_NAME`
117         // which can be found in `@rules_rust//tools/runfiles/private:workspace_name.bzl`
118         env!("RULES_RUST_RUNFILES_WORKSPACE_NAME")
119     }
120 }
121 
122 /// Returns the .runfiles directory for the currently executing binary.
find_runfiles_dir() -> io::Result<PathBuf>123 pub fn find_runfiles_dir() -> io::Result<PathBuf> {
124     assert_ne!(
125         std::env::var_os(MANIFEST_ONLY_ENV_VAR).unwrap_or_else(|| OsString::from("0")),
126         "1"
127     );
128 
129     // If bazel told us about the runfiles dir, use that without looking further.
130     if let Some(runfiles_dir) = std::env::var_os(RUNFILES_DIR_ENV_VAR).map(PathBuf::from) {
131         if runfiles_dir.is_dir() {
132             return Ok(runfiles_dir);
133         }
134     }
135     if let Some(test_srcdir) = std::env::var_os(TEST_SRCDIR_ENV_VAR).map(PathBuf::from) {
136         if test_srcdir.is_dir() {
137             return Ok(test_srcdir);
138         }
139     }
140 
141     // Consume the first argument (argv[0])
142     let exec_path = std::env::args().next().expect("arg 0 was not set");
143 
144     let mut binary_path = PathBuf::from(&exec_path);
145     loop {
146         // Check for our neighboring $binary.runfiles directory.
147         let mut runfiles_name = binary_path.file_name().unwrap().to_owned();
148         runfiles_name.push(".runfiles");
149 
150         let runfiles_path = binary_path.with_file_name(&runfiles_name);
151         if runfiles_path.is_dir() {
152             return Ok(runfiles_path);
153         }
154 
155         // Check if we're already under a *.runfiles directory.
156         {
157             // TODO: 1.28 adds Path::ancestors() which is a little simpler.
158             let mut next = binary_path.parent();
159             while let Some(ancestor) = next {
160                 if ancestor
161                     .file_name()
162                     .map_or(false, |f| f.to_string_lossy().ends_with(".runfiles"))
163                 {
164                     return Ok(ancestor.to_path_buf());
165                 }
166                 next = ancestor.parent();
167             }
168         }
169 
170         if !fs::symlink_metadata(&binary_path)?.file_type().is_symlink() {
171             break;
172         }
173         // Follow symlinks and keep looking.
174         let link_target = binary_path.read_link()?;
175         binary_path = if link_target.is_absolute() {
176             link_target
177         } else {
178             let link_dir = binary_path.parent().unwrap();
179             env::current_dir()?.join(link_dir).join(link_target)
180         }
181     }
182 
183     Err(make_io_error("failed to find .runfiles directory"))
184 }
185 
make_io_error(msg: &str) -> io::Error186 fn make_io_error(msg: &str) -> io::Error {
187     io::Error::new(io::ErrorKind::Other, msg)
188 }
189 
is_manifest_only() -> bool190 fn is_manifest_only() -> bool {
191     match std::env::var(MANIFEST_ONLY_ENV_VAR) {
192         Ok(val) => val == "1",
193         Err(_) => false,
194     }
195 }
196 
find_manifest_path() -> io::Result<PathBuf>197 fn find_manifest_path() -> io::Result<PathBuf> {
198     assert_eq!(
199         std::env::var_os(MANIFEST_ONLY_ENV_VAR).expect("RUNFILES_MANIFEST_ONLY was not set"),
200         OsString::from("1")
201     );
202     match std::env::var_os(MANIFEST_FILE_ENV_VAR) {
203         Some(path) => Ok(path.into()),
204         None => Err(
205             make_io_error(
206                 "RUNFILES_MANIFEST_ONLY was set to '1', but RUNFILES_MANIFEST_FILE was not set. Did Bazel change?"))
207     }
208 }
209 
210 #[cfg(test)]
211 mod test {
212     use super::*;
213 
214     use std::fs::File;
215     use std::io::prelude::*;
216 
217     #[test]
test_can_read_data_from_runfiles()218     fn test_can_read_data_from_runfiles() {
219         // We want to run multiple test cases with different environment variables set. Since
220         // environment variables are global state, we need to ensure the two test cases do not run
221         // concurrently. Rust runs tests in parallel and does not provide an easy way to synchronise
222         // them, so we run all test cases in the same #[test] function.
223 
224         let test_srcdir =
225             env::var_os(TEST_SRCDIR_ENV_VAR).expect("bazel did not provide TEST_SRCDIR");
226         let runfiles_dir =
227             env::var_os(RUNFILES_DIR_ENV_VAR).expect("bazel did not provide RUNFILES_DIR");
228 
229         // Test case 1: Only $RUNFILES_DIR is set.
230         {
231             env::remove_var(TEST_SRCDIR_ENV_VAR);
232             let r = Runfiles::create().unwrap();
233 
234             let mut f =
235                 File::open(r.rlocation("rules_rust/tools/runfiles/data/sample.txt")).unwrap();
236 
237             let mut buffer = String::new();
238             f.read_to_string(&mut buffer).unwrap();
239 
240             assert_eq!("Example Text!", buffer);
241             env::set_var(TEST_SRCDIR_ENV_VAR, &test_srcdir)
242         }
243         // Test case 2: Only $TEST_SRCDIR is set.
244         {
245             env::remove_var(RUNFILES_DIR_ENV_VAR);
246             let r = Runfiles::create().unwrap();
247 
248             let mut f =
249                 File::open(r.rlocation("rules_rust/tools/runfiles/data/sample.txt")).unwrap();
250 
251             let mut buffer = String::new();
252             f.read_to_string(&mut buffer).unwrap();
253 
254             assert_eq!("Example Text!", buffer);
255             env::set_var(RUNFILES_DIR_ENV_VAR, &runfiles_dir)
256         }
257 
258         // Test case 3: Neither are set
259         {
260             env::remove_var(RUNFILES_DIR_ENV_VAR);
261             env::remove_var(TEST_SRCDIR_ENV_VAR);
262 
263             let r = Runfiles::create().unwrap();
264 
265             let mut f =
266                 File::open(r.rlocation("rules_rust/tools/runfiles/data/sample.txt")).unwrap();
267 
268             let mut buffer = String::new();
269             f.read_to_string(&mut buffer).unwrap();
270 
271             assert_eq!("Example Text!", buffer);
272 
273             env::set_var(TEST_SRCDIR_ENV_VAR, &test_srcdir);
274             env::set_var(RUNFILES_DIR_ENV_VAR, &runfiles_dir);
275         }
276     }
277 
278     #[test]
test_manifest_based_can_read_data_from_runfiles()279     fn test_manifest_based_can_read_data_from_runfiles() {
280         let mut path_mapping = HashMap::new();
281         path_mapping.insert("a/b".into(), "c/d".into());
282         let r = Runfiles {
283             mode: Mode::ManifestBased(path_mapping),
284         };
285 
286         assert_eq!(r.rlocation("a/b"), PathBuf::from("c/d"));
287     }
288 
289     #[test]
test_current_repository()290     fn test_current_repository() {
291         let r = Runfiles::create().unwrap();
292 
293         // This check is unique to the rules_rust repository. The name
294         // here is expected to be different in consumers of this library
295         assert_eq!(r.current_repository(), "rules_rust")
296     }
297 }
298