blob: f52d7202ea73c14cf70a5e469cdeb84cd232d6a0 [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
Adam Snaider1c095c92023-07-08 02:09:58 -040042const RUNFILES_DIR_ENV_VAR: &str = "RUNFILES_DIR";
43const MANIFEST_FILE_ENV_VAR: &str = "RUNFILES_MANIFEST_FILE";
44const MANIFEST_ONLY_ENV_VAR: &str = "RUNFILES_MANIFEST_ONLY";
45const TEST_SRCDIR_ENV_VAR: &str = "TEST_SRCDIR";
46
Brian Silvermancc09f182022-03-09 15:40:20 -080047#[derive(Debug)]
48enum Mode {
49 DirectoryBased(PathBuf),
50 ManifestBased(HashMap<PathBuf, PathBuf>),
51}
52
53#[derive(Debug)]
54pub struct Runfiles {
55 mode: Mode,
56}
57
58impl 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.
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
70 fn create_directory_based() -> io::Result<Self> {
71 Ok(Runfiles {
72 mode: Mode::DirectoryBased(find_runfiles_dir()?),
73 })
74 }
75
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.
98 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 }
Adam Snaider1c095c92023-07-08 02:09:58 -0400113
114 /// Returns the canonical name of the caller's Bazel repository.
115 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 }
Brian Silvermancc09f182022-03-09 15:40:20 -0800120}
121
122/// Returns the .runfiles directory for the currently executing binary.
123pub fn find_runfiles_dir() -> io::Result<PathBuf> {
124 assert_ne!(
Adam Snaider1c095c92023-07-08 02:09:58 -0400125 std::env::var_os(MANIFEST_ONLY_ENV_VAR).unwrap_or_else(|| OsString::from("0")),
Brian Silvermancc09f182022-03-09 15:40:20 -0800126 "1"
127 );
128
129 // If bazel told us about the runfiles dir, use that without looking further.
Adam Snaider1c095c92023-07-08 02:09:58 -0400130 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) {
Brian Silvermancc09f182022-03-09 15:40:20 -0800136 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
186fn make_io_error(msg: &str) -> io::Error {
187 io::Error::new(io::ErrorKind::Other, msg)
188}
189
190fn is_manifest_only() -> bool {
Adam Snaider1c095c92023-07-08 02:09:58 -0400191 match std::env::var(MANIFEST_ONLY_ENV_VAR) {
Brian Silvermancc09f182022-03-09 15:40:20 -0800192 Ok(val) => val == "1",
193 Err(_) => false,
194 }
195}
196
197fn find_manifest_path() -> io::Result<PathBuf> {
198 assert_eq!(
Adam Snaider1c095c92023-07-08 02:09:58 -0400199 std::env::var_os(MANIFEST_ONLY_ENV_VAR).expect("RUNFILES_MANIFEST_ONLY was not set"),
Brian Silvermancc09f182022-03-09 15:40:20 -0800200 OsString::from("1")
201 );
Adam Snaider1c095c92023-07-08 02:09:58 -0400202 match std::env::var_os(MANIFEST_FILE_ENV_VAR) {
Brian Silvermancc09f182022-03-09 15:40:20 -0800203 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)]
211mod test {
212 use super::*;
213
214 use std::fs::File;
215 use std::io::prelude::*;
216
217 #[test]
218 fn test_can_read_data_from_runfiles() {
Adam Snaider1c095c92023-07-08 02:09:58 -0400219 // 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.
Brian Silvermancc09f182022-03-09 15:40:20 -0800223
Adam Snaider1c095c92023-07-08 02:09:58 -0400224 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");
Brian Silvermancc09f182022-03-09 15:40:20 -0800228
Adam Snaider1c095c92023-07-08 02:09:58 -0400229 // Test case 1: Only $RUNFILES_DIR is set.
Brian Silvermancc09f182022-03-09 15:40:20 -0800230 {
Adam Snaider1c095c92023-07-08 02:09:58 -0400231 env::remove_var(TEST_SRCDIR_ENV_VAR);
Brian Silvermancc09f182022-03-09 15:40:20 -0800232 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);
Adam Snaider1c095c92023-07-08 02:09:58 -0400241 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)
Brian Silvermancc09f182022-03-09 15:40:20 -0800256 }
257
Adam Snaider1c095c92023-07-08 02:09:58 -0400258 // Test case 3: Neither are set
Brian Silvermancc09f182022-03-09 15:40:20 -0800259 {
Adam Snaider1c095c92023-07-08 02:09:58 -0400260 env::remove_var(RUNFILES_DIR_ENV_VAR);
261 env::remove_var(TEST_SRCDIR_ENV_VAR);
Brian Silvermancc09f182022-03-09 15:40:20 -0800262
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
Adam Snaider1c095c92023-07-08 02:09:58 -0400273 env::set_var(TEST_SRCDIR_ENV_VAR, &test_srcdir);
274 env::set_var(RUNFILES_DIR_ENV_VAR, &runfiles_dir);
Brian Silvermancc09f182022-03-09 15:40:20 -0800275 }
276 }
277
278 #[test]
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 }
Adam Snaider1c095c92023-07-08 02:09:58 -0400288
289 #[test]
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 }
Brian Silvermancc09f182022-03-09 15:40:20 -0800297}