OLD | NEW |
(Empty) | |
| 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 |
| 11 import six |
| 12 from six.moves import builtins, map |
| 13 |
| 14 import pkg_resources |
| 15 |
| 16 if sys.platform.startswith('java'): |
| 17 import org.python.modules.posix.PosixModule as _os |
| 18 else: |
| 19 _os = sys.modules[os.name] |
| 20 try: |
| 21 _file = file |
| 22 except NameError: |
| 23 _file = None |
| 24 _open = open |
| 25 from distutils.errors import DistutilsError |
| 26 from pkg_resources import working_set |
| 27 |
| 28 __all__ = [ |
| 29 "AbstractSandbox", "DirectorySandbox", "SandboxViolation", "run_setup", |
| 30 ] |
| 31 |
| 32 |
| 33 def _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.v
ersion_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 |
| 51 def 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 |
| 62 def save_path(): |
| 63 saved = sys.path[:] |
| 64 try: |
| 65 yield saved |
| 66 finally: |
| 67 sys.path[:] = saved |
| 68 |
| 69 |
| 70 @contextlib.contextmanager |
| 71 def 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 |
| 89 def pushd(target): |
| 90 saved = os.getcwd() |
| 91 os.chdir(target) |
| 92 try: |
| 93 yield saved |
| 94 finally: |
| 95 os.chdir(saved) |
| 96 |
| 97 |
| 98 class 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 |
| 117 class 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 |
| 148 def 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 |
| 172 def _clear_modules(module_names): |
| 173 for mod_name in list(module_names): |
| 174 del sys.modules[mod_name] |
| 175 |
| 176 |
| 177 @contextlib.contextmanager |
| 178 def 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 |
| 187 def 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 |
| 201 def _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 |
| 222 def 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 |
| 233 def 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 |
| 262 class 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 |
| 378 if hasattr(os, 'devnull'): |
| 379 _EXCEPTIONS = [os.devnull,] |
| 380 else: |
| 381 _EXCEPTIONS = [] |
| 382 |
| 383 |
| 384 class 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 |
| 470 WRITE_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 |
| 476 class 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 |
| 482 The package setup script has attempted to modify files on your system |
| 483 that are not within the EasyInstall build area, and has been aborted. |
| 484 |
| 485 This package cannot be safely installed by EasyInstall, and may not |
| 486 support alternate installation locations even if you run its setup |
| 487 script by hand. Please inform the package's author and the EasyInstall |
| 488 maintainers to find out if a fix or workaround is available.""" % self.args |
| 489 |
| 490 |
| 491 # |
OLD | NEW |