| import os |
| import sys |
| import tempfile |
| import operator |
| import functools |
| import itertools |
| import re |
| import contextlib |
| import pickle |
| import textwrap |
| |
| from setuptools.extern import six |
| from setuptools.extern.six.moves import builtins, map |
| |
| import pkg_resources.py31compat |
| |
| if sys.platform.startswith('java'): |
| import org.python.modules.posix.PosixModule as _os |
| else: |
| _os = sys.modules[os.name] |
| try: |
| _file = file |
| except NameError: |
| _file = None |
| _open = open |
| from distutils.errors import DistutilsError |
| from pkg_resources import working_set |
| |
| |
| __all__ = [ |
| "AbstractSandbox", "DirectorySandbox", "SandboxViolation", "run_setup", |
| ] |
| |
| |
| def _execfile(filename, globals, locals=None): |
| """ |
| Python 3 implementation of execfile. |
| """ |
| mode = 'rb' |
| with open(filename, mode) as stream: |
| script = stream.read() |
| if locals is None: |
| locals = globals |
| code = compile(script, filename, 'exec') |
| exec(code, globals, locals) |
| |
| |
| @contextlib.contextmanager |
| def save_argv(repl=None): |
| saved = sys.argv[:] |
| if repl is not None: |
| sys.argv[:] = repl |
| try: |
| yield saved |
| finally: |
| sys.argv[:] = saved |
| |
| |
| @contextlib.contextmanager |
| def save_path(): |
| saved = sys.path[:] |
| try: |
| yield saved |
| finally: |
| sys.path[:] = saved |
| |
| |
| @contextlib.contextmanager |
| def override_temp(replacement): |
| """ |
| Monkey-patch tempfile.tempdir with replacement, ensuring it exists |
| """ |
| pkg_resources.py31compat.makedirs(replacement, exist_ok=True) |
| |
| saved = tempfile.tempdir |
| |
| tempfile.tempdir = replacement |
| |
| try: |
| yield |
| finally: |
| tempfile.tempdir = saved |
| |
| |
| @contextlib.contextmanager |
| def pushd(target): |
| saved = os.getcwd() |
| os.chdir(target) |
| try: |
| yield saved |
| finally: |
| os.chdir(saved) |
| |
| |
| class UnpickleableException(Exception): |
| """ |
| An exception representing another Exception that could not be pickled. |
| """ |
| |
| @staticmethod |
| def dump(type, exc): |
| """ |
| Always return a dumped (pickled) type and exc. If exc can't be pickled, |
| wrap it in UnpickleableException first. |
| """ |
| try: |
| return pickle.dumps(type), pickle.dumps(exc) |
| except Exception: |
| # get UnpickleableException inside the sandbox |
| from setuptools.sandbox import UnpickleableException as cls |
| return cls.dump(cls, cls(repr(exc))) |
| |
| |
| class ExceptionSaver: |
| """ |
| A Context Manager that will save an exception, serialized, and restore it |
| later. |
| """ |
| |
| def __enter__(self): |
| return self |
| |
| def __exit__(self, type, exc, tb): |
| if not exc: |
| return |
| |
| # dump the exception |
| self._saved = UnpickleableException.dump(type, exc) |
| self._tb = tb |
| |
| # suppress the exception |
| return True |
| |
| def resume(self): |
| "restore and re-raise any exception" |
| |
| if '_saved' not in vars(self): |
| return |
| |
| type, exc = map(pickle.loads, self._saved) |
| six.reraise(type, exc, self._tb) |
| |
| |
| @contextlib.contextmanager |
| def save_modules(): |
| """ |
| Context in which imported modules are saved. |
| |
| Translates exceptions internal to the context into the equivalent exception |
| outside the context. |
| """ |
| saved = sys.modules.copy() |
| with ExceptionSaver() as saved_exc: |
| yield saved |
| |
| sys.modules.update(saved) |
| # remove any modules imported since |
| del_modules = ( |
| mod_name for mod_name in sys.modules |
| if mod_name not in saved |
| # exclude any encodings modules. See #285 |
| and not mod_name.startswith('encodings.') |
| ) |
| _clear_modules(del_modules) |
| |
| saved_exc.resume() |
| |
| |
| def _clear_modules(module_names): |
| for mod_name in list(module_names): |
| del sys.modules[mod_name] |
| |
| |
| @contextlib.contextmanager |
| def save_pkg_resources_state(): |
| saved = pkg_resources.__getstate__() |
| try: |
| yield saved |
| finally: |
| pkg_resources.__setstate__(saved) |
| |
| |
| @contextlib.contextmanager |
| def setup_context(setup_dir): |
| temp_dir = os.path.join(setup_dir, 'temp') |
| with save_pkg_resources_state(): |
| with save_modules(): |
| hide_setuptools() |
| with save_path(): |
| with save_argv(): |
| with override_temp(temp_dir): |
| with pushd(setup_dir): |
| # ensure setuptools commands are available |
| __import__('setuptools') |
| yield |
| |
| |
| def _needs_hiding(mod_name): |
| """ |
| >>> _needs_hiding('setuptools') |
| True |
| >>> _needs_hiding('pkg_resources') |
| True |
| >>> _needs_hiding('setuptools_plugin') |
| False |
| >>> _needs_hiding('setuptools.__init__') |
| True |
| >>> _needs_hiding('distutils') |
| True |
| >>> _needs_hiding('os') |
| False |
| >>> _needs_hiding('Cython') |
| True |
| """ |
| pattern = re.compile(r'(setuptools|pkg_resources|distutils|Cython)(\.|$)') |
| return bool(pattern.match(mod_name)) |
| |
| |
| def hide_setuptools(): |
| """ |
| Remove references to setuptools' modules from sys.modules to allow the |
| invocation to import the most appropriate setuptools. This technique is |
| necessary to avoid issues such as #315 where setuptools upgrading itself |
| would fail to find a function declared in the metadata. |
| """ |
| modules = filter(_needs_hiding, sys.modules) |
| _clear_modules(modules) |
| |
| |
| def run_setup(setup_script, args): |
| """Run a distutils setup script, sandboxed in its directory""" |
| setup_dir = os.path.abspath(os.path.dirname(setup_script)) |
| with setup_context(setup_dir): |
| try: |
| sys.argv[:] = [setup_script] + list(args) |
| sys.path.insert(0, setup_dir) |
| # reset to include setup dir, w/clean callback list |
| working_set.__init__() |
| working_set.callbacks.append(lambda dist: dist.activate()) |
| |
| # __file__ should be a byte string on Python 2 (#712) |
| dunder_file = ( |
| setup_script |
| if isinstance(setup_script, str) else |
| setup_script.encode(sys.getfilesystemencoding()) |
| ) |
| |
| with DirectorySandbox(setup_dir): |
| ns = dict(__file__=dunder_file, __name__='__main__') |
| _execfile(setup_script, ns) |
| except SystemExit as v: |
| if v.args and v.args[0]: |
| raise |
| # Normal exit, just return |
| |
| |
| class AbstractSandbox: |
| """Wrap 'os' module and 'open()' builtin for virtualizing setup scripts""" |
| |
| _active = False |
| |
| def __init__(self): |
| self._attrs = [ |
| name for name in dir(_os) |
| if not name.startswith('_') and hasattr(self, name) |
| ] |
| |
| def _copy(self, source): |
| for name in self._attrs: |
| setattr(os, name, getattr(source, name)) |
| |
| def __enter__(self): |
| self._copy(self) |
| if _file: |
| builtins.file = self._file |
| builtins.open = self._open |
| self._active = True |
| |
| def __exit__(self, exc_type, exc_value, traceback): |
| self._active = False |
| if _file: |
| builtins.file = _file |
| builtins.open = _open |
| self._copy(_os) |
| |
| def run(self, func): |
| """Run 'func' under os sandboxing""" |
| with self: |
| return func() |
| |
| def _mk_dual_path_wrapper(name): |
| original = getattr(_os, name) |
| |
| def wrap(self, src, dst, *args, **kw): |
| if self._active: |
| src, dst = self._remap_pair(name, src, dst, *args, **kw) |
| return original(src, dst, *args, **kw) |
| |
| return wrap |
| |
| for name in ["rename", "link", "symlink"]: |
| if hasattr(_os, name): |
| locals()[name] = _mk_dual_path_wrapper(name) |
| |
| def _mk_single_path_wrapper(name, original=None): |
| original = original or getattr(_os, name) |
| |
| def wrap(self, path, *args, **kw): |
| if self._active: |
| path = self._remap_input(name, path, *args, **kw) |
| return original(path, *args, **kw) |
| |
| return wrap |
| |
| if _file: |
| _file = _mk_single_path_wrapper('file', _file) |
| _open = _mk_single_path_wrapper('open', _open) |
| for name in [ |
| "stat", "listdir", "chdir", "open", "chmod", "chown", "mkdir", |
| "remove", "unlink", "rmdir", "utime", "lchown", "chroot", "lstat", |
| "startfile", "mkfifo", "mknod", "pathconf", "access" |
| ]: |
| if hasattr(_os, name): |
| locals()[name] = _mk_single_path_wrapper(name) |
| |
| def _mk_single_with_return(name): |
| original = getattr(_os, name) |
| |
| def wrap(self, path, *args, **kw): |
| if self._active: |
| path = self._remap_input(name, path, *args, **kw) |
| return self._remap_output(name, original(path, *args, **kw)) |
| return original(path, *args, **kw) |
| |
| return wrap |
| |
| for name in ['readlink', 'tempnam']: |
| if hasattr(_os, name): |
| locals()[name] = _mk_single_with_return(name) |
| |
| def _mk_query(name): |
| original = getattr(_os, name) |
| |
| def wrap(self, *args, **kw): |
| retval = original(*args, **kw) |
| if self._active: |
| return self._remap_output(name, retval) |
| return retval |
| |
| return wrap |
| |
| for name in ['getcwd', 'tmpnam']: |
| if hasattr(_os, name): |
| locals()[name] = _mk_query(name) |
| |
| def _validate_path(self, path): |
| """Called to remap or validate any path, whether input or output""" |
| return path |
| |
| def _remap_input(self, operation, path, *args, **kw): |
| """Called for path inputs""" |
| return self._validate_path(path) |
| |
| def _remap_output(self, operation, path): |
| """Called for path outputs""" |
| return self._validate_path(path) |
| |
| def _remap_pair(self, operation, src, dst, *args, **kw): |
| """Called for path pairs like rename, link, and symlink operations""" |
| return ( |
| self._remap_input(operation + '-from', src, *args, **kw), |
| self._remap_input(operation + '-to', dst, *args, **kw) |
| ) |
| |
| |
| if hasattr(os, 'devnull'): |
| _EXCEPTIONS = [os.devnull,] |
| else: |
| _EXCEPTIONS = [] |
| |
| |
| class DirectorySandbox(AbstractSandbox): |
| """Restrict operations to a single subdirectory - pseudo-chroot""" |
| |
| write_ops = dict.fromkeys([ |
| "open", "chmod", "chown", "mkdir", "remove", "unlink", "rmdir", |
| "utime", "lchown", "chroot", "mkfifo", "mknod", "tempnam", |
| ]) |
| |
| _exception_patterns = [ |
| # Allow lib2to3 to attempt to save a pickled grammar object (#121) |
| r'.*lib2to3.*\.pickle$', |
| ] |
| "exempt writing to paths that match the pattern" |
| |
| def __init__(self, sandbox, exceptions=_EXCEPTIONS): |
| self._sandbox = os.path.normcase(os.path.realpath(sandbox)) |
| self._prefix = os.path.join(self._sandbox, '') |
| self._exceptions = [ |
| os.path.normcase(os.path.realpath(path)) |
| for path in exceptions |
| ] |
| AbstractSandbox.__init__(self) |
| |
| def _violation(self, operation, *args, **kw): |
| from setuptools.sandbox import SandboxViolation |
| raise SandboxViolation(operation, args, kw) |
| |
| if _file: |
| |
| def _file(self, path, mode='r', *args, **kw): |
| if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path): |
| self._violation("file", path, mode, *args, **kw) |
| return _file(path, mode, *args, **kw) |
| |
| def _open(self, path, mode='r', *args, **kw): |
| if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path): |
| self._violation("open", path, mode, *args, **kw) |
| return _open(path, mode, *args, **kw) |
| |
| def tmpnam(self): |
| self._violation("tmpnam") |
| |
| def _ok(self, path): |
| active = self._active |
| try: |
| self._active = False |
| realpath = os.path.normcase(os.path.realpath(path)) |
| return ( |
| self._exempted(realpath) |
| or realpath == self._sandbox |
| or realpath.startswith(self._prefix) |
| ) |
| finally: |
| self._active = active |
| |
| def _exempted(self, filepath): |
| start_matches = ( |
| filepath.startswith(exception) |
| for exception in self._exceptions |
| ) |
| pattern_matches = ( |
| re.match(pattern, filepath) |
| for pattern in self._exception_patterns |
| ) |
| candidates = itertools.chain(start_matches, pattern_matches) |
| return any(candidates) |
| |
| def _remap_input(self, operation, path, *args, **kw): |
| """Called for path inputs""" |
| if operation in self.write_ops and not self._ok(path): |
| self._violation(operation, os.path.realpath(path), *args, **kw) |
| return path |
| |
| def _remap_pair(self, operation, src, dst, *args, **kw): |
| """Called for path pairs like rename, link, and symlink operations""" |
| if not self._ok(src) or not self._ok(dst): |
| self._violation(operation, src, dst, *args, **kw) |
| return (src, dst) |
| |
| def open(self, file, flags, mode=0o777, *args, **kw): |
| """Called for low-level os.open()""" |
| if flags & WRITE_FLAGS and not self._ok(file): |
| self._violation("os.open", file, flags, mode, *args, **kw) |
| return _os.open(file, flags, mode, *args, **kw) |
| |
| |
| WRITE_FLAGS = functools.reduce( |
| operator.or_, [getattr(_os, a, 0) for a in |
| "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split()] |
| ) |
| |
| |
| class SandboxViolation(DistutilsError): |
| """A setup script attempted to modify the filesystem outside the sandbox""" |
| |
| tmpl = textwrap.dedent(""" |
| SandboxViolation: {cmd}{args!r} {kwargs} |
| |
| The package setup script has attempted to modify files on your system |
| that are not within the EasyInstall build area, and has been aborted. |
| |
| This package cannot be safely installed by EasyInstall, and may not |
| support alternate installation locations even if you run its setup |
| script by hand. Please inform the package's author and the EasyInstall |
| maintainers to find out if a fix or workaround is available. |
| """).lstrip() |
| |
| def __str__(self): |
| cmd, args, kwargs = self.args |
| return self.tmpl.format(**locals()) |