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