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