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