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