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