blob: 3a597cbb36a92b087bfdc51d052be63283d38e6e [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):
177 return self._runfiles.get(path)
178
179 @staticmethod
180 def _LoadRunfiles(path):
181 """Loads the runfiles manifest."""
182 result = {}
183 with open(path, "r") as f:
184 for line in f:
185 line = line.strip()
186 if line:
187 tokens = line.split(" ", 1)
188 if len(tokens) == 1:
189 result[line] = line
190 else:
191 result[tokens[0]] = tokens[1]
192 return result
193
Laszlo Csomorbb1d0852018-02-15 02:59:13 -0800194 def _GetRunfilesDir(self):
195 if self._path.endswith("/MANIFEST") or self._path.endswith("\\MANIFEST"):
196 return self._path[:-len("/MANIFEST")]
197 elif self._path.endswith(".runfiles_manifest"):
198 return self._path[:-len("_manifest")]
199 else:
200 return ""
201
Laszlo Csomor1d46d622018-02-09 02:36:35 -0800202 def EnvVars(self):
Laszlo Csomorbb1d0852018-02-15 02:59:13 -0800203 directory = self._GetRunfilesDir()
204 return {
205 "RUNFILES_MANIFEST_FILE": self._path,
206 "RUNFILES_DIR": directory,
207 # TODO(laszlocsomor): remove JAVA_RUNFILES once the Java launcher can
208 # pick up RUNFILES_DIR.
209 "JAVA_RUNFILES": directory,
210 }
Laszlo Csomora610a2b2018-02-05 05:24:34 -0800211
212
213class _DirectoryBased(object):
214 """`Runfiles` strategy that appends runfiles paths to the runfiles root."""
215
216 def __init__(self, path):
217 if not path:
218 raise ValueError()
219 if not isinstance(path, str):
220 raise TypeError()
221 self._runfiles_root = path
222
223 def RlocationChecked(self, path):
224 # Use posixpath instead of os.path, because Bazel only creates a runfiles
225 # tree on Unix platforms, so `Create()` will only create a directory-based
226 # runfiles strategy on those platforms.
227 return posixpath.join(self._runfiles_root, path)
228
Laszlo Csomor1d46d622018-02-09 02:36:35 -0800229 def EnvVars(self):
Laszlo Csomorbb1d0852018-02-15 02:59:13 -0800230 return {
231 "RUNFILES_DIR": self._runfiles_root,
232 # TODO(laszlocsomor): remove JAVA_RUNFILES once the Java launcher can
233 # pick up RUNFILES_DIR.
234 "JAVA_RUNFILES": self._runfiles_root,
235 }
Laszlo Csomorc29f34f2018-05-22 05:01:41 -0700236
237
238def _PathsFrom(argv0, runfiles_mf, runfiles_dir, is_runfiles_manifest,
239 is_runfiles_directory):
240 """Discover runfiles manifest and runfiles directory paths.
241
242 Args:
243 argv0: string; the value of sys.argv[0]
244 runfiles_mf: string; the value of the RUNFILES_MANIFEST_FILE environment
245 variable
246 runfiles_dir: string; the value of the RUNFILES_DIR environment variable
247 is_runfiles_manifest: lambda(string):bool; returns true if the argument is
248 the path of a runfiles manifest file
249 is_runfiles_directory: lambda(string):bool; returns true if the argument is
250 the path of a runfiles directory
251
252 Returns:
253 (string, string) pair, first element is the path to the runfiles manifest,
254 second element is the path to the runfiles directory. If the first element
255 is non-empty, then is_runfiles_manifest returns true for it. Same goes for
256 the second element and is_runfiles_directory respectively. If both elements
257 are empty, then this function could not find a manifest or directory for
258 which is_runfiles_manifest or is_runfiles_directory returns true.
259 """
260 mf_alid = is_runfiles_manifest(runfiles_mf)
261 dir_valid = is_runfiles_directory(runfiles_dir)
262
263 if not mf_alid and not dir_valid:
264 runfiles_mf = argv0 + ".runfiles/MANIFEST"
265 runfiles_dir = argv0 + ".runfiles"
266 mf_alid = is_runfiles_manifest(runfiles_mf)
267 dir_valid = is_runfiles_directory(runfiles_dir)
268 if not mf_alid:
269 runfiles_mf = argv0 + ".runfiles_manifest"
270 mf_alid = is_runfiles_manifest(runfiles_mf)
271
272 if not mf_alid and not dir_valid:
273 return ("", "")
274
275 if not mf_alid:
276 runfiles_mf = runfiles_dir + "/MANIFEST"
277 mf_alid = is_runfiles_manifest(runfiles_mf)
278 if not mf_alid:
279 runfiles_mf = runfiles_dir + "_manifest"
280 mf_alid = is_runfiles_manifest(runfiles_mf)
281
282 if not dir_valid:
283 runfiles_dir = runfiles_mf[:-9] # "_manifest" or "/MANIFEST"
284 dir_valid = is_runfiles_directory(runfiles_dir)
285
286 return (runfiles_mf if mf_alid else "", runfiles_dir if dir_valid else "")