Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 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 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 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 | |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 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. |
| 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 Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 113 | |
| 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 120 | } |
| 121 | |
| 122 | /// Returns the .runfiles directory for the currently executing binary. |
| 123 | pub fn find_runfiles_dir() -> io::Result<PathBuf> { |
| 124 | assert_ne!( |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 125 | std::env::var_os(MANIFEST_ONLY_ENV_VAR).unwrap_or_else(|| OsString::from("0")), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 126 | "1" |
| 127 | ); |
| 128 | |
| 129 | // If bazel told us about the runfiles dir, use that without looking further. |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 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) { |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 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 | |
| 186 | fn make_io_error(msg: &str) -> io::Error { |
| 187 | io::Error::new(io::ErrorKind::Other, msg) |
| 188 | } |
| 189 | |
| 190 | fn is_manifest_only() -> bool { |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 191 | match std::env::var(MANIFEST_ONLY_ENV_VAR) { |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 192 | Ok(val) => val == "1", |
| 193 | Err(_) => false, |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | fn find_manifest_path() -> io::Result<PathBuf> { |
| 198 | assert_eq!( |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 199 | std::env::var_os(MANIFEST_ONLY_ENV_VAR).expect("RUNFILES_MANIFEST_ONLY was not set"), |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 200 | OsString::from("1") |
| 201 | ); |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 202 | match std::env::var_os(MANIFEST_FILE_ENV_VAR) { |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 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] |
| 218 | fn test_can_read_data_from_runfiles() { |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 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. |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 223 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 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"); |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 228 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 229 | // Test case 1: Only $RUNFILES_DIR is set. |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 230 | { |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 231 | env::remove_var(TEST_SRCDIR_ENV_VAR); |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 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); |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 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) |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 256 | } |
| 257 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 258 | // Test case 3: Neither are set |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 259 | { |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 260 | env::remove_var(RUNFILES_DIR_ENV_VAR); |
| 261 | env::remove_var(TEST_SRCDIR_ENV_VAR); |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 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 | |
Adam Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 273 | env::set_var(TEST_SRCDIR_ENV_VAR, &test_srcdir); |
| 274 | env::set_var(RUNFILES_DIR_ENV_VAR, &runfiles_dir); |
Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 275 | } |
| 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 Snaider | 1c095c9 | 2023-07-08 02:09:58 -0400 | [diff] [blame^] | 288 | |
| 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 Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame] | 297 | } |