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