• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import os
2import sys
3import tempfile
4import operator
5import functools
6import itertools
7import re
8import contextlib
9import pickle
10import textwrap
11
12from setuptools.extern import six
13from setuptools.extern.six.moves import builtins, map
14
15import pkg_resources.py31compat
16
17if sys.platform.startswith('java'):
18    import org.python.modules.posix.PosixModule as _os
19else:
20    _os = sys.modules[os.name]
21try:
22    _file = file
23except NameError:
24    _file = None
25_open = open
26from distutils.errors import DistutilsError
27from pkg_resources import working_set
28
29
30__all__ = [
31    "AbstractSandbox", "DirectorySandbox", "SandboxViolation", "run_setup",
32]
33
34
35def _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
49def 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
60def save_path():
61    saved = sys.path[:]
62    try:
63        yield saved
64    finally:
65        sys.path[:] = saved
66
67
68@contextlib.contextmanager
69def 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
86def pushd(target):
87    saved = os.getcwd()
88    os.chdir(target)
89    try:
90        yield saved
91    finally:
92        os.chdir(saved)
93
94
95class 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
114class 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
145def 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
169def _clear_modules(module_names):
170    for mod_name in list(module_names):
171        del sys.modules[mod_name]
172
173
174@contextlib.contextmanager
175def 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
184def 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
198def _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
219def 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
230def 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
257class 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
376if hasattr(os, 'devnull'):
377    _EXCEPTIONS = [os.devnull,]
378else:
379    _EXCEPTIONS = []
380
381
382class 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
468WRITE_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
474class 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