|
|
- 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())
|