1 import os 2 import sys 3 import tempfile 4 import operator 5 import functools 6 import itertools 7 import re 8 import contextlib 9 import pickle 10 import textwrap 11 12 from setuptools.extern import six 13 from setuptools.extern.six.moves import builtins, map 14 15 import pkg_resources.py31compat 16 17 if sys.platform.startswith('java'): 18 import org.python.modules.posix.PosixModule as _os 19 else: 20 _os = sys.modules[os.name] 21 try: 22 _file = file 23 except NameError: 24 _file = None 25 _open = open 26 from distutils.errors import DistutilsError 27 from pkg_resources import working_set 28 29 30 __all__ = [ 31 "AbstractSandbox", "DirectorySandbox", "SandboxViolation", "run_setup", 32 ] 33 34 35 def _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 49 def 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 60 def save_path(): 61 saved = sys.path[:] 62 try: 63 yield saved 64 finally: 65 sys.path[:] = saved 66 67 68 @contextlib.contextmanager 69 def override_temp(replacement): 70 """ 71 Monkey-patch tempfile.tempdir with replacement, ensuring it exists 72 """ 73 pkg_resources.py31compat.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 86 def pushd(target): 87 saved = os.getcwd() 88 os.chdir(target) 89 try: 90 yield saved 91 finally: 92 os.chdir(saved) 93 94 95 class 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 114 class 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 145 def 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 169 def _clear_modules(module_names): 170 for mod_name in list(module_names): 171 del sys.modules[mod_name] 172 173 174 @contextlib.contextmanager 175 def 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 184 def setup_context(setup_dir): 185 temp_dir = os.path.join(setup_dir, 'temp') 186 with save_pkg_resources_state(): 187 with save_modules(): 188 hide_setuptools() 189 with save_path(): 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 def _needs_hiding(mod_name): 199 """ 200 >>> _needs_hiding('setuptools') 201 True 202 >>> _needs_hiding('pkg_resources') 203 True 204 >>> _needs_hiding('setuptools_plugin') 205 False 206 >>> _needs_hiding('setuptools.__init__') 207 True 208 >>> _needs_hiding('distutils') 209 True 210 >>> _needs_hiding('os') 211 False 212 >>> _needs_hiding('Cython') 213 True 214 """ 215 pattern = re.compile(r'(setuptools|pkg_resources|distutils|Cython)(\.|$)') 216 return bool(pattern.match(mod_name)) 217 218 219 def hide_setuptools(): 220 """ 221 Remove references to setuptools' modules from sys.modules to allow the 222 invocation to import the most appropriate setuptools. This technique is 223 necessary to avoid issues such as #315 where setuptools upgrading itself 224 would fail to find a function declared in the metadata. 225 """ 226 modules = filter(_needs_hiding, sys.modules) 227 _clear_modules(modules) 228 229 230 def run_setup(setup_script, args): 231 """Run a distutils setup script, sandboxed in its directory""" 232 setup_dir = os.path.abspath(os.path.dirname(setup_script)) 233 with setup_context(setup_dir): 234 try: 235 sys.argv[:] = [setup_script] + list(args) 236 sys.path.insert(0, setup_dir) 237 # reset to include setup dir, w/clean callback list 238 working_set.__init__() 239 working_set.callbacks.append(lambda dist: dist.activate()) 240 241 # __file__ should be a byte string on Python 2 (#712) 242 dunder_file = ( 243 setup_script 244 if isinstance(setup_script, str) else 245 setup_script.encode(sys.getfilesystemencoding()) 246 ) 247 248 with DirectorySandbox(setup_dir): 249 ns = dict(__file__=dunder_file, __name__='__main__') 250 _execfile(setup_script, ns) 251 except SystemExit as v: 252 if v.args and v.args[0]: 253 raise 254 # Normal exit, just return 255 256 257 class AbstractSandbox: 258 """Wrap 'os' module and 'open()' builtin for virtualizing setup scripts""" 259 260 _active = False 261 262 def __init__(self): 263 self._attrs = [ 264 name for name in dir(_os) 265 if not name.startswith('_') and hasattr(self, name) 266 ] 267 268 def _copy(self, source): 269 for name in self._attrs: 270 setattr(os, name, getattr(source, name)) 271 272 def __enter__(self): 273 self._copy(self) 274 if _file: 275 builtins.file = self._file 276 builtins.open = self._open 277 self._active = True 278 279 def __exit__(self, exc_type, exc_value, traceback): 280 self._active = False 281 if _file: 282 builtins.file = _file 283 builtins.open = _open 284 self._copy(_os) 285 286 def run(self, func): 287 """Run 'func' under os sandboxing""" 288 with self: 289 return func() 290 291 def _mk_dual_path_wrapper(name): 292 original = getattr(_os, name) 293 294 def wrap(self, src, dst, *args, **kw): 295 if self._active: 296 src, dst = self._remap_pair(name, src, dst, *args, **kw) 297 return original(src, dst, *args, **kw) 298 299 return wrap 300 301 for name in ["rename", "link", "symlink"]: 302 if hasattr(_os, name): 303 locals()[name] = _mk_dual_path_wrapper(name) 304 305 def _mk_single_path_wrapper(name, original=None): 306 original = original or getattr(_os, name) 307 308 def wrap(self, path, *args, **kw): 309 if self._active: 310 path = self._remap_input(name, path, *args, **kw) 311 return original(path, *args, **kw) 312 313 return wrap 314 315 if _file: 316 _file = _mk_single_path_wrapper('file', _file) 317 _open = _mk_single_path_wrapper('open', _open) 318 for name in [ 319 "stat", "listdir", "chdir", "open", "chmod", "chown", "mkdir", 320 "remove", "unlink", "rmdir", "utime", "lchown", "chroot", "lstat", 321 "startfile", "mkfifo", "mknod", "pathconf", "access" 322 ]: 323 if hasattr(_os, name): 324 locals()[name] = _mk_single_path_wrapper(name) 325 326 def _mk_single_with_return(name): 327 original = getattr(_os, name) 328 329 def wrap(self, path, *args, **kw): 330 if self._active: 331 path = self._remap_input(name, path, *args, **kw) 332 return self._remap_output(name, original(path, *args, **kw)) 333 return original(path, *args, **kw) 334 335 return wrap 336 337 for name in ['readlink', 'tempnam']: 338 if hasattr(_os, name): 339 locals()[name] = _mk_single_with_return(name) 340 341 def _mk_query(name): 342 original = getattr(_os, name) 343 344 def wrap(self, *args, **kw): 345 retval = original(*args, **kw) 346 if self._active: 347 return self._remap_output(name, retval) 348 return retval 349 350 return wrap 351 352 for name in ['getcwd', 'tmpnam']: 353 if hasattr(_os, name): 354 locals()[name] = _mk_query(name) 355 356 def _validate_path(self, path): 357 """Called to remap or validate any path, whether input or output""" 358 return path 359 360 def _remap_input(self, operation, path, *args, **kw): 361 """Called for path inputs""" 362 return self._validate_path(path) 363 364 def _remap_output(self, operation, path): 365 """Called for path outputs""" 366 return self._validate_path(path) 367 368 def _remap_pair(self, operation, src, dst, *args, **kw): 369 """Called for path pairs like rename, link, and symlink operations""" 370 return ( 371 self._remap_input(operation + '-from', src, *args, **kw), 372 self._remap_input(operation + '-to', dst, *args, **kw) 373 ) 374 375 376 if hasattr(os, 'devnull'): 377 _EXCEPTIONS = [os.devnull,] 378 else: 379 _EXCEPTIONS = [] 380 381 382 class DirectorySandbox(AbstractSandbox): 383 """Restrict operations to a single subdirectory - pseudo-chroot""" 384 385 write_ops = dict.fromkeys([ 386 "open", "chmod", "chown", "mkdir", "remove", "unlink", "rmdir", 387 "utime", "lchown", "chroot", "mkfifo", "mknod", "tempnam", 388 ]) 389 390 _exception_patterns = [ 391 # Allow lib2to3 to attempt to save a pickled grammar object (#121) 392 r'.*lib2to3.*\.pickle$', 393 ] 394 "exempt writing to paths that match the pattern" 395 396 def __init__(self, sandbox, exceptions=_EXCEPTIONS): 397 self._sandbox = os.path.normcase(os.path.realpath(sandbox)) 398 self._prefix = os.path.join(self._sandbox, '') 399 self._exceptions = [ 400 os.path.normcase(os.path.realpath(path)) 401 for path in exceptions 402 ] 403 AbstractSandbox.__init__(self) 404 405 def _violation(self, operation, *args, **kw): 406 from setuptools.sandbox import SandboxViolation 407 raise SandboxViolation(operation, args, kw) 408 409 if _file: 410 411 def _file(self, path, mode='r', *args, **kw): 412 if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path): 413 self._violation("file", path, mode, *args, **kw) 414 return _file(path, mode, *args, **kw) 415 416 def _open(self, path, mode='r', *args, **kw): 417 if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path): 418 self._violation("open", path, mode, *args, **kw) 419 return _open(path, mode, *args, **kw) 420 421 def tmpnam(self): 422 self._violation("tmpnam") 423 424 def _ok(self, path): 425 active = self._active 426 try: 427 self._active = False 428 realpath = os.path.normcase(os.path.realpath(path)) 429 return ( 430 self._exempted(realpath) 431 or realpath == self._sandbox 432 or realpath.startswith(self._prefix) 433 ) 434 finally: 435 self._active = active 436 437 def _exempted(self, filepath): 438 start_matches = ( 439 filepath.startswith(exception) 440 for exception in self._exceptions 441 ) 442 pattern_matches = ( 443 re.match(pattern, filepath) 444 for pattern in self._exception_patterns 445 ) 446 candidates = itertools.chain(start_matches, pattern_matches) 447 return any(candidates) 448 449 def _remap_input(self, operation, path, *args, **kw): 450 """Called for path inputs""" 451 if operation in self.write_ops and not self._ok(path): 452 self._violation(operation, os.path.realpath(path), *args, **kw) 453 return path 454 455 def _remap_pair(self, operation, src, dst, *args, **kw): 456 """Called for path pairs like rename, link, and symlink operations""" 457 if not self._ok(src) or not self._ok(dst): 458 self._violation(operation, src, dst, *args, **kw) 459 return (src, dst) 460 461 def open(self, file, flags, mode=0o777, *args, **kw): 462 """Called for low-level os.open()""" 463 if flags & WRITE_FLAGS and not self._ok(file): 464 self._violation("os.open", file, flags, mode, *args, **kw) 465 return _os.open(file, flags, mode, *args, **kw) 466 467 468 WRITE_FLAGS = functools.reduce( 469 operator.or_, [getattr(_os, a, 0) for a in 470 "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split()] 471 ) 472 473 474 class SandboxViolation(DistutilsError): 475 """A setup script attempted to modify the filesystem outside the sandbox""" 476 477 tmpl = textwrap.dedent(""" 478 SandboxViolation: {cmd}{args!r} {kwargs} 479 480 The package setup script has attempted to modify files on your system 481 that are not within the EasyInstall build area, and has been aborted. 482 483 This package cannot be safely installed by EasyInstall, and may not 484 support alternate installation locations even if you run its setup 485 script by hand. Please inform the package's author and the EasyInstall 486 maintainers to find out if a fix or workaround is available. 487 """).lstrip() 488 489 def __str__(self): 490 cmd, args, kwargs = self.args 491 return self.tmpl.format(**locals()) 492