annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/site-packages/setuptools/sandbox.py @ 68:5028fdace37b

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