blob: 03cff1c27ee386c4320840560bd833de26d678a1 [file] [log] [blame]
Laszlo Csomora610a2b2018-02-05 05:24:34 -08001# Copyright 2018 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Runfiles lookup library for Bazel-built Python binaries and tests.
15
Laszlo Csomor44646c22018-06-27 05:09:38 -070016USAGE:
Laszlo Csomora610a2b2018-02-05 05:24:34 -080017
Laszlo Csomor44646c22018-06-27 05:09:38 -0700181. Depend on this runfiles library from your build rule:
Laszlo Csomora610a2b2018-02-05 05:24:34 -080019
Laszlo Csomor44646c22018-06-27 05:09:38 -070020 py_binary(
21 name = "my_binary",
22 ...
brandjone4ccba42019-08-01 14:27:50 -070023 deps = ["@rules_python//python/runfiles"],
Laszlo Csomor44646c22018-06-27 05:09:38 -070024 )
Laszlo Csomora610a2b2018-02-05 05:24:34 -080025
Laszlo Csomor44646c22018-06-27 05:09:38 -0700262. Import the runfiles library.
Laszlo Csomora610a2b2018-02-05 05:24:34 -080027
brandjone4ccba42019-08-01 14:27:50 -070028 from rules_python.python.runfiles import runfiles
Laszlo Csomora610a2b2018-02-05 05:24:34 -080029
Laszlo Csomor44646c22018-06-27 05:09:38 -0700303. Create a Runfiles object and use rlocation to look up runfile paths:
Laszlo Csomora610a2b2018-02-05 05:24:34 -080031
Laszlo Csomor44646c22018-06-27 05:09:38 -070032 r = runfiles.Create()
33 ...
34 with open(r.Rlocation("my_workspace/path/to/my/data.txt"), "r") as f:
35 contents = f.readlines()
36 ...
Laszlo Csomora610a2b2018-02-05 05:24:34 -080037
Laszlo Csomor44646c22018-06-27 05:09:38 -070038 The code above creates a manifest- or directory-based implementations based
39 on the environment variables in os.environ. See `Create()` for more info.
Laszlo Csomora610a2b2018-02-05 05:24:34 -080040
Laszlo Csomor44646c22018-06-27 05:09:38 -070041 If you want to explicitly create a manifest- or directory-based
42 implementations, you can do so as follows:
Laszlo Csomora610a2b2018-02-05 05:24:34 -080043
Laszlo Csomor44646c22018-06-27 05:09:38 -070044 r1 = runfiles.CreateManifestBased("path/to/foo.runfiles_manifest")
45
46 r2 = runfiles.CreateDirectoryBased("path/to/foo.runfiles/")
47
48 If you want to start subprocesses that also need runfiles, you need to set
49 the right environment variables for them:
50
51 import subprocess
brandjone4ccba42019-08-01 14:27:50 -070052 from rules_python.python.runfiles import runfiles
Laszlo Csomor44646c22018-06-27 05:09:38 -070053
54 r = runfiles.Create()
55 env = {}
56 ...
57 env.update(r.EnvVars())
58 p = subprocess.Popen([r.Rlocation("path/to/binary")], env, ...)
Laszlo Csomora610a2b2018-02-05 05:24:34 -080059"""
60
61import os
62import posixpath
63
64
65def CreateManifestBased(manifest_path):
66 return _Runfiles(_ManifestBased(manifest_path))
67
68
69def CreateDirectoryBased(runfiles_dir_path):
70 return _Runfiles(_DirectoryBased(runfiles_dir_path))
71
72
73def Create(env=None):
74 """Returns a new `Runfiles` instance.
75
76 The returned object is either:
77 - manifest-based, meaning it looks up runfile paths from a manifest file, or
78 - directory-based, meaning it looks up runfile paths under a given directory
79 path
80
81 If `env` contains "RUNFILES_MANIFEST_FILE" with non-empty value, this method
82 returns a manifest-based implementation. The object eagerly reads and caches
83 the whole manifest file upon instantiation; this may be relevant for
84 performance consideration.
85
Laszlo Csomor13c33732018-02-08 09:56:04 -080086 Otherwise, if `env` contains "RUNFILES_DIR" with non-empty value (checked in
87 this priority order), this method returns a directory-based implementation.
Laszlo Csomora610a2b2018-02-05 05:24:34 -080088
89 If neither cases apply, this method returns null.
90
91 Args:
92 env: {string: string}; optional; the map of environment variables. If None,
93 this function uses the environment variable map of this process.
94 Raises:
95 IOError: if some IO error occurs.
96 """
97 env_map = os.environ if env is None else env
98 manifest = env_map.get("RUNFILES_MANIFEST_FILE")
99 if manifest:
100 return CreateManifestBased(manifest)
101
102 directory = env_map.get("RUNFILES_DIR")
Laszlo Csomora610a2b2018-02-05 05:24:34 -0800103 if directory:
104 return CreateDirectoryBased(directory)
105
106 return None
107
108
109class _Runfiles(object):
110 """Returns the runtime location of runfiles.
111
112 Runfiles are data-dependencies of Bazel-built binaries and tests.
113 """
114
115 def __init__(self, strategy):
116 self._strategy = strategy
117
118 def Rlocation(self, path):
119 """Returns the runtime path of a runfile.
120
121 Runfiles are data-dependencies of Bazel-built binaries and tests.
122
123 The returned path may not be valid. The caller should check the path's
124 validity and that the path exists.
125
126 The function may return None. In that case the caller can be sure that the
127 rule does not know about this data-dependency.
128
129 Args:
130 path: string; runfiles-root-relative path of the runfile
131 Returns:
132 the path to the runfile, which the caller should check for existence, or
133 None if the method doesn't know about this runfile
134 Raises:
135 TypeError: if `path` is not a string
Laszlo Csomorf9cb8592018-04-24 05:52:37 -0700136 ValueError: if `path` is None or empty, or it's absolute or not normalized
Laszlo Csomora610a2b2018-02-05 05:24:34 -0800137 """
138 if not path:
139 raise ValueError()
140 if not isinstance(path, str):
141 raise TypeError()
Laszlo Csomorf9cb8592018-04-24 05:52:37 -0700142 if (path.startswith("../") or "/.." in path or path.startswith("./") or
143 "/./" in path or path.endswith("/.") or "//" in path):
144 raise ValueError("path is not normalized: \"%s\"" % path)
Laszlo Csomorb961b0a2018-03-09 01:02:45 -0800145 if path[0] == "\\":
146 raise ValueError("path is absolute without a drive letter: \"%s\"" % path)
147 if os.path.isabs(path):
148 return path
Laszlo Csomora610a2b2018-02-05 05:24:34 -0800149 return self._strategy.RlocationChecked(path)
150
Laszlo Csomor1d46d622018-02-09 02:36:35 -0800151 def EnvVars(self):
152 """Returns environment variables for subprocesses.
Laszlo Csomora610a2b2018-02-05 05:24:34 -0800153
Laszlo Csomor1d46d622018-02-09 02:36:35 -0800154 The caller should set the returned key-value pairs in the environment of
Laszlo Csomora610a2b2018-02-05 05:24:34 -0800155 subprocesses in case those subprocesses are also Bazel-built binaries that
156 need to use runfiles.
157
158 Returns:
Laszlo Csomor1d46d622018-02-09 02:36:35 -0800159 {string: string}; a dict; keys are environment variable names, values are
160 the values for these environment variables
Laszlo Csomora610a2b2018-02-05 05:24:34 -0800161 """
Laszlo Csomor1d46d622018-02-09 02:36:35 -0800162 return self._strategy.EnvVars()
Laszlo Csomora610a2b2018-02-05 05:24:34 -0800163
164
165class _ManifestBased(object):
166 """`Runfiles` strategy that parses a runfiles-manifest to look up runfiles."""
167
168 def __init__(self, path):
169 if not path:
170 raise ValueError()
171 if not isinstance(path, str):
172 raise TypeError()
173 self._path = path
174 self._runfiles = _ManifestBased._LoadRunfiles(path)
175
176 def RlocationChecked(self, path):
Fabian Meumertzheim486d1532022-02-07 05:49:47 -0800177 """Returns the runtime path of a runfile."""
178 exact_match = self._runfiles.get(path)
179 if exact_match:
180 return exact_match
181 # If path references a runfile that lies under a directory that itself is a
182 # runfile, then only the directory is listed in the manifest. Look up all
183 # prefixes of path in the manifest and append the relative path from the
184 # prefix to the looked up path.
185 prefix_end = len(path)
186 while True:
187 prefix_end = path.rfind("/", 0, prefix_end - 1)
188 if prefix_end == -1:
189 return None
190 prefix_match = self._runfiles.get(path[0:prefix_end])
191 if prefix_match:
192 return prefix_match + "/" + path[prefix_end + 1:]
Laszlo Csomora610a2b2018-02-05 05:24:34 -0800193
194 @staticmethod
195 def _LoadRunfiles(path):
196 """Loads the runfiles manifest."""
197 result = {}
198 with open(path, "r") as f:
199 for line in f:
200 line = line.strip()
201 if line:
202 tokens = line.split(" ", 1)
203 if len(tokens) == 1:
204 result[line] = line
205 else:
206 result[tokens[0]] = tokens[1]
207 return result
208
Laszlo Csomorbb1d0852018-02-15 02:59:13 -0800209 def _GetRunfilesDir(self):
210 if self._path.endswith("/MANIFEST") or self._path.endswith("\\MANIFEST"):
211 return self._path[:-len("/MANIFEST")]
212 elif self._path.endswith(".runfiles_manifest"):
213 return self._path[:-len("_manifest")]
214 else:
215 return ""
216
Laszlo Csomor1d46d622018-02-09 02:36:35 -0800217 def EnvVars(self):
Laszlo Csomorbb1d0852018-02-15 02:59:13 -0800218 directory = self._GetRunfilesDir()
219 return {
220 "RUNFILES_MANIFEST_FILE": self._path,
221 "RUNFILES_DIR": directory,
222 # TODO(laszlocsomor): remove JAVA_RUNFILES once the Java launcher can
223 # pick up RUNFILES_DIR.
224 "JAVA_RUNFILES": directory,
225 }
Laszlo Csomora610a2b2018-02-05 05:24:34 -0800226
227
228class _DirectoryBased(object):
229 """`Runfiles` strategy that appends runfiles paths to the runfiles root."""
230
231 def __init__(self, path):
232 if not path:
233 raise ValueError()
234 if not isinstance(path, str):
235 raise TypeError()
236 self._runfiles_root = path
237
238 def RlocationChecked(self, path):
239 # Use posixpath instead of os.path, because Bazel only creates a runfiles
240 # tree on Unix platforms, so `Create()` will only create a directory-based
241 # runfiles strategy on those platforms.
242 return posixpath.join(self._runfiles_root, path)
243
Laszlo Csomor1d46d622018-02-09 02:36:35 -0800244 def EnvVars(self):
Laszlo Csomorbb1d0852018-02-15 02:59:13 -0800245 return {
246 "RUNFILES_DIR": self._runfiles_root,
247 # TODO(laszlocsomor): remove JAVA_RUNFILES once the Java launcher can
248 # pick up RUNFILES_DIR.
249 "JAVA_RUNFILES": self._runfiles_root,
250 }
Laszlo Csomorc29f34f2018-05-22 05:01:41 -0700251
252
253def _PathsFrom(argv0, runfiles_mf, runfiles_dir, is_runfiles_manifest,
254 is_runfiles_directory):
255 """Discover runfiles manifest and runfiles directory paths.
256
257 Args:
258 argv0: string; the value of sys.argv[0]
259 runfiles_mf: string; the value of the RUNFILES_MANIFEST_FILE environment
260 variable
261 runfiles_dir: string; the value of the RUNFILES_DIR environment variable
262 is_runfiles_manifest: lambda(string):bool; returns true if the argument is
263 the path of a runfiles manifest file
264 is_runfiles_directory: lambda(string):bool; returns true if the argument is
265 the path of a runfiles directory
266
267 Returns:
268 (string, string) pair, first element is the path to the runfiles manifest,
269 second element is the path to the runfiles directory. If the first element
270 is non-empty, then is_runfiles_manifest returns true for it. Same goes for
271 the second element and is_runfiles_directory respectively. If both elements
272 are empty, then this function could not find a manifest or directory for
273 which is_runfiles_manifest or is_runfiles_directory returns true.
274 """
275 mf_alid = is_runfiles_manifest(runfiles_mf)
276 dir_valid = is_runfiles_directory(runfiles_dir)
277
278 if not mf_alid and not dir_valid:
279 runfiles_mf = argv0 + ".runfiles/MANIFEST"
280 runfiles_dir = argv0 + ".runfiles"
281 mf_alid = is_runfiles_manifest(runfiles_mf)
282 dir_valid = is_runfiles_directory(runfiles_dir)
283 if not mf_alid:
284 runfiles_mf = argv0 + ".runfiles_manifest"
285 mf_alid = is_runfiles_manifest(runfiles_mf)
286
287 if not mf_alid and not dir_valid:
288 return ("", "")
289
290 if not mf_alid:
291 runfiles_mf = runfiles_dir + "/MANIFEST"
292 mf_alid = is_runfiles_manifest(runfiles_mf)
293 if not mf_alid:
294 runfiles_mf = runfiles_dir + "_manifest"
295 mf_alid = is_runfiles_manifest(runfiles_mf)
296
297 if not dir_valid:
298 runfiles_dir = runfiles_mf[:-9] # "_manifest" or "/MANIFEST"
299 dir_valid = is_runfiles_directory(runfiles_dir)
300
301 return (runfiles_mf if mf_alid else "", runfiles_dir if dir_valid else "")