annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/site-packages/setuptools/sandbox.py @ 69:33d812a61356

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 17:55:14 -0400
parents
children
rev   line source
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())