jpayne@69
|
1 from __future__ import annotations
|
jpayne@69
|
2
|
jpayne@69
|
3 import builtins
|
jpayne@69
|
4 import contextlib
|
jpayne@69
|
5 import functools
|
jpayne@69
|
6 import itertools
|
jpayne@69
|
7 import operator
|
jpayne@69
|
8 import os
|
jpayne@69
|
9 import pickle
|
jpayne@69
|
10 import re
|
jpayne@69
|
11 import sys
|
jpayne@69
|
12 import tempfile
|
jpayne@69
|
13 import textwrap
|
jpayne@69
|
14 from types import TracebackType
|
jpayne@69
|
15 from typing import TYPE_CHECKING
|
jpayne@69
|
16
|
jpayne@69
|
17 import pkg_resources
|
jpayne@69
|
18 from pkg_resources import working_set
|
jpayne@69
|
19
|
jpayne@69
|
20 from distutils.errors import DistutilsError
|
jpayne@69
|
21
|
jpayne@69
|
22 if sys.platform.startswith('java'):
|
jpayne@69
|
23 import org.python.modules.posix.PosixModule as _os # pyright: ignore[reportMissingImports]
|
jpayne@69
|
24 else:
|
jpayne@69
|
25 _os = sys.modules[os.name]
|
jpayne@69
|
26 _open = open
|
jpayne@69
|
27
|
jpayne@69
|
28
|
jpayne@69
|
29 if TYPE_CHECKING:
|
jpayne@69
|
30 from typing_extensions import Self
|
jpayne@69
|
31
|
jpayne@69
|
32 __all__ = [
|
jpayne@69
|
33 "AbstractSandbox",
|
jpayne@69
|
34 "DirectorySandbox",
|
jpayne@69
|
35 "SandboxViolation",
|
jpayne@69
|
36 "run_setup",
|
jpayne@69
|
37 ]
|
jpayne@69
|
38
|
jpayne@69
|
39
|
jpayne@69
|
40 def _execfile(filename, globals, locals=None):
|
jpayne@69
|
41 """
|
jpayne@69
|
42 Python 3 implementation of execfile.
|
jpayne@69
|
43 """
|
jpayne@69
|
44 mode = 'rb'
|
jpayne@69
|
45 with open(filename, mode) as stream:
|
jpayne@69
|
46 script = stream.read()
|
jpayne@69
|
47 if locals is None:
|
jpayne@69
|
48 locals = globals
|
jpayne@69
|
49 code = compile(script, filename, 'exec')
|
jpayne@69
|
50 exec(code, globals, locals)
|
jpayne@69
|
51
|
jpayne@69
|
52
|
jpayne@69
|
53 @contextlib.contextmanager
|
jpayne@69
|
54 def save_argv(repl=None):
|
jpayne@69
|
55 saved = sys.argv[:]
|
jpayne@69
|
56 if repl is not None:
|
jpayne@69
|
57 sys.argv[:] = repl
|
jpayne@69
|
58 try:
|
jpayne@69
|
59 yield saved
|
jpayne@69
|
60 finally:
|
jpayne@69
|
61 sys.argv[:] = saved
|
jpayne@69
|
62
|
jpayne@69
|
63
|
jpayne@69
|
64 @contextlib.contextmanager
|
jpayne@69
|
65 def save_path():
|
jpayne@69
|
66 saved = sys.path[:]
|
jpayne@69
|
67 try:
|
jpayne@69
|
68 yield saved
|
jpayne@69
|
69 finally:
|
jpayne@69
|
70 sys.path[:] = saved
|
jpayne@69
|
71
|
jpayne@69
|
72
|
jpayne@69
|
73 @contextlib.contextmanager
|
jpayne@69
|
74 def override_temp(replacement):
|
jpayne@69
|
75 """
|
jpayne@69
|
76 Monkey-patch tempfile.tempdir with replacement, ensuring it exists
|
jpayne@69
|
77 """
|
jpayne@69
|
78 os.makedirs(replacement, exist_ok=True)
|
jpayne@69
|
79
|
jpayne@69
|
80 saved = tempfile.tempdir
|
jpayne@69
|
81
|
jpayne@69
|
82 tempfile.tempdir = replacement
|
jpayne@69
|
83
|
jpayne@69
|
84 try:
|
jpayne@69
|
85 yield
|
jpayne@69
|
86 finally:
|
jpayne@69
|
87 tempfile.tempdir = saved
|
jpayne@69
|
88
|
jpayne@69
|
89
|
jpayne@69
|
90 @contextlib.contextmanager
|
jpayne@69
|
91 def pushd(target):
|
jpayne@69
|
92 saved = os.getcwd()
|
jpayne@69
|
93 os.chdir(target)
|
jpayne@69
|
94 try:
|
jpayne@69
|
95 yield saved
|
jpayne@69
|
96 finally:
|
jpayne@69
|
97 os.chdir(saved)
|
jpayne@69
|
98
|
jpayne@69
|
99
|
jpayne@69
|
100 class UnpickleableException(Exception):
|
jpayne@69
|
101 """
|
jpayne@69
|
102 An exception representing another Exception that could not be pickled.
|
jpayne@69
|
103 """
|
jpayne@69
|
104
|
jpayne@69
|
105 @staticmethod
|
jpayne@69
|
106 def dump(type, exc):
|
jpayne@69
|
107 """
|
jpayne@69
|
108 Always return a dumped (pickled) type and exc. If exc can't be pickled,
|
jpayne@69
|
109 wrap it in UnpickleableException first.
|
jpayne@69
|
110 """
|
jpayne@69
|
111 try:
|
jpayne@69
|
112 return pickle.dumps(type), pickle.dumps(exc)
|
jpayne@69
|
113 except Exception:
|
jpayne@69
|
114 # get UnpickleableException inside the sandbox
|
jpayne@69
|
115 from setuptools.sandbox import UnpickleableException as cls
|
jpayne@69
|
116
|
jpayne@69
|
117 return cls.dump(cls, cls(repr(exc)))
|
jpayne@69
|
118
|
jpayne@69
|
119
|
jpayne@69
|
120 class ExceptionSaver:
|
jpayne@69
|
121 """
|
jpayne@69
|
122 A Context Manager that will save an exception, serialize, and restore it
|
jpayne@69
|
123 later.
|
jpayne@69
|
124 """
|
jpayne@69
|
125
|
jpayne@69
|
126 def __enter__(self) -> Self:
|
jpayne@69
|
127 return self
|
jpayne@69
|
128
|
jpayne@69
|
129 def __exit__(
|
jpayne@69
|
130 self,
|
jpayne@69
|
131 type: type[BaseException] | None,
|
jpayne@69
|
132 exc: BaseException | None,
|
jpayne@69
|
133 tb: TracebackType | None,
|
jpayne@69
|
134 ) -> bool:
|
jpayne@69
|
135 if not exc:
|
jpayne@69
|
136 return False
|
jpayne@69
|
137
|
jpayne@69
|
138 # dump the exception
|
jpayne@69
|
139 self._saved = UnpickleableException.dump(type, exc)
|
jpayne@69
|
140 self._tb = tb
|
jpayne@69
|
141
|
jpayne@69
|
142 # suppress the exception
|
jpayne@69
|
143 return True
|
jpayne@69
|
144
|
jpayne@69
|
145 def resume(self):
|
jpayne@69
|
146 "restore and re-raise any exception"
|
jpayne@69
|
147
|
jpayne@69
|
148 if '_saved' not in vars(self):
|
jpayne@69
|
149 return
|
jpayne@69
|
150
|
jpayne@69
|
151 type, exc = map(pickle.loads, self._saved)
|
jpayne@69
|
152 raise exc.with_traceback(self._tb)
|
jpayne@69
|
153
|
jpayne@69
|
154
|
jpayne@69
|
155 @contextlib.contextmanager
|
jpayne@69
|
156 def save_modules():
|
jpayne@69
|
157 """
|
jpayne@69
|
158 Context in which imported modules are saved.
|
jpayne@69
|
159
|
jpayne@69
|
160 Translates exceptions internal to the context into the equivalent exception
|
jpayne@69
|
161 outside the context.
|
jpayne@69
|
162 """
|
jpayne@69
|
163 saved = sys.modules.copy()
|
jpayne@69
|
164 with ExceptionSaver() as saved_exc:
|
jpayne@69
|
165 yield saved
|
jpayne@69
|
166
|
jpayne@69
|
167 sys.modules.update(saved)
|
jpayne@69
|
168 # remove any modules imported since
|
jpayne@69
|
169 del_modules = (
|
jpayne@69
|
170 mod_name
|
jpayne@69
|
171 for mod_name in sys.modules
|
jpayne@69
|
172 if mod_name not in saved
|
jpayne@69
|
173 # exclude any encodings modules. See #285
|
jpayne@69
|
174 and not mod_name.startswith('encodings.')
|
jpayne@69
|
175 )
|
jpayne@69
|
176 _clear_modules(del_modules)
|
jpayne@69
|
177
|
jpayne@69
|
178 saved_exc.resume()
|
jpayne@69
|
179
|
jpayne@69
|
180
|
jpayne@69
|
181 def _clear_modules(module_names):
|
jpayne@69
|
182 for mod_name in list(module_names):
|
jpayne@69
|
183 del sys.modules[mod_name]
|
jpayne@69
|
184
|
jpayne@69
|
185
|
jpayne@69
|
186 @contextlib.contextmanager
|
jpayne@69
|
187 def save_pkg_resources_state():
|
jpayne@69
|
188 saved = pkg_resources.__getstate__()
|
jpayne@69
|
189 try:
|
jpayne@69
|
190 yield saved
|
jpayne@69
|
191 finally:
|
jpayne@69
|
192 pkg_resources.__setstate__(saved)
|
jpayne@69
|
193
|
jpayne@69
|
194
|
jpayne@69
|
195 @contextlib.contextmanager
|
jpayne@69
|
196 def setup_context(setup_dir):
|
jpayne@69
|
197 temp_dir = os.path.join(setup_dir, 'temp')
|
jpayne@69
|
198 with save_pkg_resources_state():
|
jpayne@69
|
199 with save_modules():
|
jpayne@69
|
200 with save_path():
|
jpayne@69
|
201 hide_setuptools()
|
jpayne@69
|
202 with save_argv():
|
jpayne@69
|
203 with override_temp(temp_dir):
|
jpayne@69
|
204 with pushd(setup_dir):
|
jpayne@69
|
205 # ensure setuptools commands are available
|
jpayne@69
|
206 __import__('setuptools')
|
jpayne@69
|
207 yield
|
jpayne@69
|
208
|
jpayne@69
|
209
|
jpayne@69
|
210 _MODULES_TO_HIDE = {
|
jpayne@69
|
211 'setuptools',
|
jpayne@69
|
212 'distutils',
|
jpayne@69
|
213 'pkg_resources',
|
jpayne@69
|
214 'Cython',
|
jpayne@69
|
215 '_distutils_hack',
|
jpayne@69
|
216 }
|
jpayne@69
|
217
|
jpayne@69
|
218
|
jpayne@69
|
219 def _needs_hiding(mod_name):
|
jpayne@69
|
220 """
|
jpayne@69
|
221 >>> _needs_hiding('setuptools')
|
jpayne@69
|
222 True
|
jpayne@69
|
223 >>> _needs_hiding('pkg_resources')
|
jpayne@69
|
224 True
|
jpayne@69
|
225 >>> _needs_hiding('setuptools_plugin')
|
jpayne@69
|
226 False
|
jpayne@69
|
227 >>> _needs_hiding('setuptools.__init__')
|
jpayne@69
|
228 True
|
jpayne@69
|
229 >>> _needs_hiding('distutils')
|
jpayne@69
|
230 True
|
jpayne@69
|
231 >>> _needs_hiding('os')
|
jpayne@69
|
232 False
|
jpayne@69
|
233 >>> _needs_hiding('Cython')
|
jpayne@69
|
234 True
|
jpayne@69
|
235 """
|
jpayne@69
|
236 base_module = mod_name.split('.', 1)[0]
|
jpayne@69
|
237 return base_module in _MODULES_TO_HIDE
|
jpayne@69
|
238
|
jpayne@69
|
239
|
jpayne@69
|
240 def hide_setuptools():
|
jpayne@69
|
241 """
|
jpayne@69
|
242 Remove references to setuptools' modules from sys.modules to allow the
|
jpayne@69
|
243 invocation to import the most appropriate setuptools. This technique is
|
jpayne@69
|
244 necessary to avoid issues such as #315 where setuptools upgrading itself
|
jpayne@69
|
245 would fail to find a function declared in the metadata.
|
jpayne@69
|
246 """
|
jpayne@69
|
247 _distutils_hack = sys.modules.get('_distutils_hack', None)
|
jpayne@69
|
248 if _distutils_hack is not None:
|
jpayne@69
|
249 _distutils_hack._remove_shim()
|
jpayne@69
|
250
|
jpayne@69
|
251 modules = filter(_needs_hiding, sys.modules)
|
jpayne@69
|
252 _clear_modules(modules)
|
jpayne@69
|
253
|
jpayne@69
|
254
|
jpayne@69
|
255 def run_setup(setup_script, args):
|
jpayne@69
|
256 """Run a distutils setup script, sandboxed in its directory"""
|
jpayne@69
|
257 setup_dir = os.path.abspath(os.path.dirname(setup_script))
|
jpayne@69
|
258 with setup_context(setup_dir):
|
jpayne@69
|
259 try:
|
jpayne@69
|
260 sys.argv[:] = [setup_script] + list(args)
|
jpayne@69
|
261 sys.path.insert(0, setup_dir)
|
jpayne@69
|
262 # reset to include setup dir, w/clean callback list
|
jpayne@69
|
263 working_set.__init__()
|
jpayne@69
|
264 working_set.callbacks.append(lambda dist: dist.activate())
|
jpayne@69
|
265
|
jpayne@69
|
266 with DirectorySandbox(setup_dir):
|
jpayne@69
|
267 ns = dict(__file__=setup_script, __name__='__main__')
|
jpayne@69
|
268 _execfile(setup_script, ns)
|
jpayne@69
|
269 except SystemExit as v:
|
jpayne@69
|
270 if v.args and v.args[0]:
|
jpayne@69
|
271 raise
|
jpayne@69
|
272 # Normal exit, just return
|
jpayne@69
|
273
|
jpayne@69
|
274
|
jpayne@69
|
275 class AbstractSandbox:
|
jpayne@69
|
276 """Wrap 'os' module and 'open()' builtin for virtualizing setup scripts"""
|
jpayne@69
|
277
|
jpayne@69
|
278 _active = False
|
jpayne@69
|
279
|
jpayne@69
|
280 def __init__(self):
|
jpayne@69
|
281 self._attrs = [
|
jpayne@69
|
282 name
|
jpayne@69
|
283 for name in dir(_os)
|
jpayne@69
|
284 if not name.startswith('_') and hasattr(self, name)
|
jpayne@69
|
285 ]
|
jpayne@69
|
286
|
jpayne@69
|
287 def _copy(self, source):
|
jpayne@69
|
288 for name in self._attrs:
|
jpayne@69
|
289 setattr(os, name, getattr(source, name))
|
jpayne@69
|
290
|
jpayne@69
|
291 def __enter__(self) -> None:
|
jpayne@69
|
292 self._copy(self)
|
jpayne@69
|
293 builtins.open = self._open
|
jpayne@69
|
294 self._active = True
|
jpayne@69
|
295
|
jpayne@69
|
296 def __exit__(
|
jpayne@69
|
297 self,
|
jpayne@69
|
298 exc_type: type[BaseException] | None,
|
jpayne@69
|
299 exc_value: BaseException | None,
|
jpayne@69
|
300 traceback: TracebackType | None,
|
jpayne@69
|
301 ):
|
jpayne@69
|
302 self._active = False
|
jpayne@69
|
303 builtins.open = _open
|
jpayne@69
|
304 self._copy(_os)
|
jpayne@69
|
305
|
jpayne@69
|
306 def run(self, func):
|
jpayne@69
|
307 """Run 'func' under os sandboxing"""
|
jpayne@69
|
308 with self:
|
jpayne@69
|
309 return func()
|
jpayne@69
|
310
|
jpayne@69
|
311 def _mk_dual_path_wrapper(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
|
jpayne@69
|
312 original = getattr(_os, name)
|
jpayne@69
|
313
|
jpayne@69
|
314 def wrap(self, src, dst, *args, **kw):
|
jpayne@69
|
315 if self._active:
|
jpayne@69
|
316 src, dst = self._remap_pair(name, src, dst, *args, **kw)
|
jpayne@69
|
317 return original(src, dst, *args, **kw)
|
jpayne@69
|
318
|
jpayne@69
|
319 return wrap
|
jpayne@69
|
320
|
jpayne@69
|
321 for __name in ["rename", "link", "symlink"]:
|
jpayne@69
|
322 if hasattr(_os, __name):
|
jpayne@69
|
323 locals()[__name] = _mk_dual_path_wrapper(__name)
|
jpayne@69
|
324
|
jpayne@69
|
325 def _mk_single_path_wrapper(name: str, original=None): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
|
jpayne@69
|
326 original = original or getattr(_os, name)
|
jpayne@69
|
327
|
jpayne@69
|
328 def wrap(self, path, *args, **kw):
|
jpayne@69
|
329 if self._active:
|
jpayne@69
|
330 path = self._remap_input(name, path, *args, **kw)
|
jpayne@69
|
331 return original(path, *args, **kw)
|
jpayne@69
|
332
|
jpayne@69
|
333 return wrap
|
jpayne@69
|
334
|
jpayne@69
|
335 _open = _mk_single_path_wrapper('open', _open)
|
jpayne@69
|
336 for __name in [
|
jpayne@69
|
337 "stat",
|
jpayne@69
|
338 "listdir",
|
jpayne@69
|
339 "chdir",
|
jpayne@69
|
340 "open",
|
jpayne@69
|
341 "chmod",
|
jpayne@69
|
342 "chown",
|
jpayne@69
|
343 "mkdir",
|
jpayne@69
|
344 "remove",
|
jpayne@69
|
345 "unlink",
|
jpayne@69
|
346 "rmdir",
|
jpayne@69
|
347 "utime",
|
jpayne@69
|
348 "lchown",
|
jpayne@69
|
349 "chroot",
|
jpayne@69
|
350 "lstat",
|
jpayne@69
|
351 "startfile",
|
jpayne@69
|
352 "mkfifo",
|
jpayne@69
|
353 "mknod",
|
jpayne@69
|
354 "pathconf",
|
jpayne@69
|
355 "access",
|
jpayne@69
|
356 ]:
|
jpayne@69
|
357 if hasattr(_os, __name):
|
jpayne@69
|
358 locals()[__name] = _mk_single_path_wrapper(__name)
|
jpayne@69
|
359
|
jpayne@69
|
360 def _mk_single_with_return(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
|
jpayne@69
|
361 original = getattr(_os, name)
|
jpayne@69
|
362
|
jpayne@69
|
363 def wrap(self, path, *args, **kw):
|
jpayne@69
|
364 if self._active:
|
jpayne@69
|
365 path = self._remap_input(name, path, *args, **kw)
|
jpayne@69
|
366 return self._remap_output(name, original(path, *args, **kw))
|
jpayne@69
|
367 return original(path, *args, **kw)
|
jpayne@69
|
368
|
jpayne@69
|
369 return wrap
|
jpayne@69
|
370
|
jpayne@69
|
371 for __name in ['readlink', 'tempnam']:
|
jpayne@69
|
372 if hasattr(_os, __name):
|
jpayne@69
|
373 locals()[__name] = _mk_single_with_return(__name)
|
jpayne@69
|
374
|
jpayne@69
|
375 def _mk_query(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099
|
jpayne@69
|
376 original = getattr(_os, name)
|
jpayne@69
|
377
|
jpayne@69
|
378 def wrap(self, *args, **kw):
|
jpayne@69
|
379 retval = original(*args, **kw)
|
jpayne@69
|
380 if self._active:
|
jpayne@69
|
381 return self._remap_output(name, retval)
|
jpayne@69
|
382 return retval
|
jpayne@69
|
383
|
jpayne@69
|
384 return wrap
|
jpayne@69
|
385
|
jpayne@69
|
386 for __name in ['getcwd', 'tmpnam']:
|
jpayne@69
|
387 if hasattr(_os, __name):
|
jpayne@69
|
388 locals()[__name] = _mk_query(__name)
|
jpayne@69
|
389
|
jpayne@69
|
390 def _validate_path(self, path):
|
jpayne@69
|
391 """Called to remap or validate any path, whether input or output"""
|
jpayne@69
|
392 return path
|
jpayne@69
|
393
|
jpayne@69
|
394 def _remap_input(self, operation, path, *args, **kw):
|
jpayne@69
|
395 """Called for path inputs"""
|
jpayne@69
|
396 return self._validate_path(path)
|
jpayne@69
|
397
|
jpayne@69
|
398 def _remap_output(self, operation, path):
|
jpayne@69
|
399 """Called for path outputs"""
|
jpayne@69
|
400 return self._validate_path(path)
|
jpayne@69
|
401
|
jpayne@69
|
402 def _remap_pair(self, operation, src, dst, *args, **kw):
|
jpayne@69
|
403 """Called for path pairs like rename, link, and symlink operations"""
|
jpayne@69
|
404 return (
|
jpayne@69
|
405 self._remap_input(operation + '-from', src, *args, **kw),
|
jpayne@69
|
406 self._remap_input(operation + '-to', dst, *args, **kw),
|
jpayne@69
|
407 )
|
jpayne@69
|
408
|
jpayne@69
|
409
|
jpayne@69
|
410 if hasattr(os, 'devnull'):
|
jpayne@69
|
411 _EXCEPTIONS = [os.devnull]
|
jpayne@69
|
412 else:
|
jpayne@69
|
413 _EXCEPTIONS = []
|
jpayne@69
|
414
|
jpayne@69
|
415
|
jpayne@69
|
416 class DirectorySandbox(AbstractSandbox):
|
jpayne@69
|
417 """Restrict operations to a single subdirectory - pseudo-chroot"""
|
jpayne@69
|
418
|
jpayne@69
|
419 write_ops: dict[str, None] = dict.fromkeys([
|
jpayne@69
|
420 "open",
|
jpayne@69
|
421 "chmod",
|
jpayne@69
|
422 "chown",
|
jpayne@69
|
423 "mkdir",
|
jpayne@69
|
424 "remove",
|
jpayne@69
|
425 "unlink",
|
jpayne@69
|
426 "rmdir",
|
jpayne@69
|
427 "utime",
|
jpayne@69
|
428 "lchown",
|
jpayne@69
|
429 "chroot",
|
jpayne@69
|
430 "mkfifo",
|
jpayne@69
|
431 "mknod",
|
jpayne@69
|
432 "tempnam",
|
jpayne@69
|
433 ])
|
jpayne@69
|
434
|
jpayne@69
|
435 _exception_patterns: list[str | re.Pattern] = []
|
jpayne@69
|
436 "exempt writing to paths that match the pattern"
|
jpayne@69
|
437
|
jpayne@69
|
438 def __init__(self, sandbox, exceptions=_EXCEPTIONS):
|
jpayne@69
|
439 self._sandbox = os.path.normcase(os.path.realpath(sandbox))
|
jpayne@69
|
440 self._prefix = os.path.join(self._sandbox, '')
|
jpayne@69
|
441 self._exceptions = [
|
jpayne@69
|
442 os.path.normcase(os.path.realpath(path)) for path in exceptions
|
jpayne@69
|
443 ]
|
jpayne@69
|
444 AbstractSandbox.__init__(self)
|
jpayne@69
|
445
|
jpayne@69
|
446 def _violation(self, operation, *args, **kw):
|
jpayne@69
|
447 from setuptools.sandbox import SandboxViolation
|
jpayne@69
|
448
|
jpayne@69
|
449 raise SandboxViolation(operation, args, kw)
|
jpayne@69
|
450
|
jpayne@69
|
451 def _open(self, path, mode='r', *args, **kw):
|
jpayne@69
|
452 if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path):
|
jpayne@69
|
453 self._violation("open", path, mode, *args, **kw)
|
jpayne@69
|
454 return _open(path, mode, *args, **kw)
|
jpayne@69
|
455
|
jpayne@69
|
456 def tmpnam(self):
|
jpayne@69
|
457 self._violation("tmpnam")
|
jpayne@69
|
458
|
jpayne@69
|
459 def _ok(self, path):
|
jpayne@69
|
460 active = self._active
|
jpayne@69
|
461 try:
|
jpayne@69
|
462 self._active = False
|
jpayne@69
|
463 realpath = os.path.normcase(os.path.realpath(path))
|
jpayne@69
|
464 return (
|
jpayne@69
|
465 self._exempted(realpath)
|
jpayne@69
|
466 or realpath == self._sandbox
|
jpayne@69
|
467 or realpath.startswith(self._prefix)
|
jpayne@69
|
468 )
|
jpayne@69
|
469 finally:
|
jpayne@69
|
470 self._active = active
|
jpayne@69
|
471
|
jpayne@69
|
472 def _exempted(self, filepath):
|
jpayne@69
|
473 start_matches = (
|
jpayne@69
|
474 filepath.startswith(exception) for exception in self._exceptions
|
jpayne@69
|
475 )
|
jpayne@69
|
476 pattern_matches = (
|
jpayne@69
|
477 re.match(pattern, filepath) for pattern in self._exception_patterns
|
jpayne@69
|
478 )
|
jpayne@69
|
479 candidates = itertools.chain(start_matches, pattern_matches)
|
jpayne@69
|
480 return any(candidates)
|
jpayne@69
|
481
|
jpayne@69
|
482 def _remap_input(self, operation, path, *args, **kw):
|
jpayne@69
|
483 """Called for path inputs"""
|
jpayne@69
|
484 if operation in self.write_ops and not self._ok(path):
|
jpayne@69
|
485 self._violation(operation, os.path.realpath(path), *args, **kw)
|
jpayne@69
|
486 return path
|
jpayne@69
|
487
|
jpayne@69
|
488 def _remap_pair(self, operation, src, dst, *args, **kw):
|
jpayne@69
|
489 """Called for path pairs like rename, link, and symlink operations"""
|
jpayne@69
|
490 if not self._ok(src) or not self._ok(dst):
|
jpayne@69
|
491 self._violation(operation, src, dst, *args, **kw)
|
jpayne@69
|
492 return (src, dst)
|
jpayne@69
|
493
|
jpayne@69
|
494 def open(self, file, flags, mode: int = 0o777, *args, **kw):
|
jpayne@69
|
495 """Called for low-level os.open()"""
|
jpayne@69
|
496 if flags & WRITE_FLAGS and not self._ok(file):
|
jpayne@69
|
497 self._violation("os.open", file, flags, mode, *args, **kw)
|
jpayne@69
|
498 return _os.open(file, flags, mode, *args, **kw)
|
jpayne@69
|
499
|
jpayne@69
|
500
|
jpayne@69
|
501 WRITE_FLAGS = functools.reduce(
|
jpayne@69
|
502 operator.or_,
|
jpayne@69
|
503 [
|
jpayne@69
|
504 getattr(_os, a, 0)
|
jpayne@69
|
505 for a in "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split()
|
jpayne@69
|
506 ],
|
jpayne@69
|
507 )
|
jpayne@69
|
508
|
jpayne@69
|
509
|
jpayne@69
|
510 class SandboxViolation(DistutilsError):
|
jpayne@69
|
511 """A setup script attempted to modify the filesystem outside the sandbox"""
|
jpayne@69
|
512
|
jpayne@69
|
513 tmpl = textwrap.dedent(
|
jpayne@69
|
514 """
|
jpayne@69
|
515 SandboxViolation: {cmd}{args!r} {kwargs}
|
jpayne@69
|
516
|
jpayne@69
|
517 The package setup script has attempted to modify files on your system
|
jpayne@69
|
518 that are not within the EasyInstall build area, and has been aborted.
|
jpayne@69
|
519
|
jpayne@69
|
520 This package cannot be safely installed by EasyInstall, and may not
|
jpayne@69
|
521 support alternate installation locations even if you run its setup
|
jpayne@69
|
522 script by hand. Please inform the package's author and the EasyInstall
|
jpayne@69
|
523 maintainers to find out if a fix or workaround is available.
|
jpayne@69
|
524 """
|
jpayne@69
|
525 ).lstrip()
|
jpayne@69
|
526
|
jpayne@69
|
527 def __str__(self) -> str:
|
jpayne@69
|
528 cmd, args, kwargs = self.args
|
jpayne@69
|
529 return self.tmpl.format(**locals())
|