]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
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 | import textwrap | |
11 | import builtins | |
12 | ||
13 | import pkg_resources | |
14 | from distutils.errors import DistutilsError | |
15 | from pkg_resources import working_set | |
16 | ||
17 | if sys.platform.startswith('java'): | |
18 | import org.python.modules.posix.PosixModule as _os | |
19 | else: | |
20 | _os = sys.modules[os.name] | |
21 | try: | |
22 | _file = file | |
23 | except NameError: | |
24 | _file = None | |
25 | _open = open | |
26 | ||
27 | ||
28 | __all__ = [ | |
29 | "AbstractSandbox", | |
30 | "DirectorySandbox", | |
31 | "SandboxViolation", | |
32 | "run_setup", | |
33 | ] | |
34 | ||
35 | ||
36 | def _execfile(filename, globals, locals=None): | |
37 | """ | |
38 | Python 3 implementation of execfile. | |
39 | """ | |
40 | mode = 'rb' | |
41 | with open(filename, mode) as stream: | |
42 | script = stream.read() | |
43 | if locals is None: | |
44 | locals = globals | |
45 | code = compile(script, filename, 'exec') | |
46 | exec(code, globals, locals) | |
47 | ||
48 | ||
49 | @contextlib.contextmanager | |
50 | def save_argv(repl=None): | |
51 | saved = sys.argv[:] | |
52 | if repl is not None: | |
53 | sys.argv[:] = repl | |
54 | try: | |
55 | yield saved | |
56 | finally: | |
57 | sys.argv[:] = saved | |
58 | ||
59 | ||
60 | @contextlib.contextmanager | |
61 | def save_path(): | |
62 | saved = sys.path[:] | |
63 | try: | |
64 | yield saved | |
65 | finally: | |
66 | sys.path[:] = saved | |
67 | ||
68 | ||
69 | @contextlib.contextmanager | |
70 | def override_temp(replacement): | |
71 | """ | |
72 | Monkey-patch tempfile.tempdir with replacement, ensuring it exists | |
73 | """ | |
74 | os.makedirs(replacement, exist_ok=True) | |
75 | ||
76 | saved = tempfile.tempdir | |
77 | ||
78 | tempfile.tempdir = replacement | |
79 | ||
80 | try: | |
81 | yield | |
82 | finally: | |
83 | tempfile.tempdir = saved | |
84 | ||
85 | ||
86 | @contextlib.contextmanager | |
87 | def pushd(target): | |
88 | saved = os.getcwd() | |
89 | os.chdir(target) | |
90 | try: | |
91 | yield saved | |
92 | finally: | |
93 | os.chdir(saved) | |
94 | ||
95 | ||
96 | class UnpickleableException(Exception): | |
97 | """ | |
98 | An exception representing another Exception that could not be pickled. | |
99 | """ | |
100 | ||
101 | @staticmethod | |
102 | def dump(type, exc): | |
103 | """ | |
104 | Always return a dumped (pickled) type and exc. If exc can't be pickled, | |
105 | wrap it in UnpickleableException first. | |
106 | """ | |
107 | try: | |
108 | return pickle.dumps(type), pickle.dumps(exc) | |
109 | except Exception: | |
110 | # get UnpickleableException inside the sandbox | |
111 | from setuptools.sandbox import UnpickleableException as cls | |
112 | ||
113 | return cls.dump(cls, cls(repr(exc))) | |
114 | ||
115 | ||
116 | class ExceptionSaver: | |
117 | """ | |
118 | A Context Manager that will save an exception, serialized, and restore it | |
119 | later. | |
120 | """ | |
121 | ||
122 | def __enter__(self): | |
123 | return self | |
124 | ||
125 | def __exit__(self, type, exc, tb): | |
126 | if not exc: | |
127 | return | |
128 | ||
129 | # dump the exception | |
130 | self._saved = UnpickleableException.dump(type, exc) | |
131 | self._tb = tb | |
132 | ||
133 | # suppress the exception | |
134 | return True | |
135 | ||
136 | def resume(self): | |
137 | "restore and re-raise any exception" | |
138 | ||
139 | if '_saved' not in vars(self): | |
140 | return | |
141 | ||
142 | type, exc = map(pickle.loads, self._saved) | |
143 | raise exc.with_traceback(self._tb) | |
144 | ||
145 | ||
146 | @contextlib.contextmanager | |
147 | def save_modules(): | |
148 | """ | |
149 | Context in which imported modules are saved. | |
150 | ||
151 | Translates exceptions internal to the context into the equivalent exception | |
152 | outside the context. | |
153 | """ | |
154 | saved = sys.modules.copy() | |
155 | with ExceptionSaver() as saved_exc: | |
156 | yield saved | |
157 | ||
158 | sys.modules.update(saved) | |
159 | # remove any modules imported since | |
160 | del_modules = ( | |
161 | mod_name | |
162 | 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 | with save_path(): | |
192 | hide_setuptools() | |
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 | _MODULES_TO_HIDE = { | |
202 | 'setuptools', | |
203 | 'distutils', | |
204 | 'pkg_resources', | |
205 | 'Cython', | |
206 | '_distutils_hack', | |
207 | } | |
208 | ||
209 | ||
210 | def _needs_hiding(mod_name): | |
211 | """ | |
212 | >>> _needs_hiding('setuptools') | |
213 | True | |
214 | >>> _needs_hiding('pkg_resources') | |
215 | True | |
216 | >>> _needs_hiding('setuptools_plugin') | |
217 | False | |
218 | >>> _needs_hiding('setuptools.__init__') | |
219 | True | |
220 | >>> _needs_hiding('distutils') | |
221 | True | |
222 | >>> _needs_hiding('os') | |
223 | False | |
224 | >>> _needs_hiding('Cython') | |
225 | True | |
226 | """ | |
227 | base_module = mod_name.split('.', 1)[0] | |
228 | return base_module in _MODULES_TO_HIDE | |
229 | ||
230 | ||
231 | def hide_setuptools(): | |
232 | """ | |
233 | Remove references to setuptools' modules from sys.modules to allow the | |
234 | invocation to import the most appropriate setuptools. This technique is | |
235 | necessary to avoid issues such as #315 where setuptools upgrading itself | |
236 | would fail to find a function declared in the metadata. | |
237 | """ | |
238 | _distutils_hack = sys.modules.get('_distutils_hack', None) | |
239 | if _distutils_hack is not None: | |
240 | _distutils_hack.remove_shim() | |
241 | ||
242 | modules = filter(_needs_hiding, sys.modules) | |
243 | _clear_modules(modules) | |
244 | ||
245 | ||
246 | def run_setup(setup_script, args): | |
247 | """Run a distutils setup script, sandboxed in its directory""" | |
248 | setup_dir = os.path.abspath(os.path.dirname(setup_script)) | |
249 | with setup_context(setup_dir): | |
250 | try: | |
251 | sys.argv[:] = [setup_script] + list(args) | |
252 | sys.path.insert(0, setup_dir) | |
253 | # reset to include setup dir, w/clean callback list | |
254 | working_set.__init__() | |
255 | working_set.callbacks.append(lambda dist: dist.activate()) | |
256 | ||
257 | with DirectorySandbox(setup_dir): | |
258 | ns = dict(__file__=setup_script, __name__='__main__') | |
259 | _execfile(setup_script, ns) | |
260 | except SystemExit as v: | |
261 | if v.args and v.args[0]: | |
262 | raise | |
263 | # Normal exit, just return | |
264 | ||
265 | ||
266 | class AbstractSandbox: | |
267 | """Wrap 'os' module and 'open()' builtin for virtualizing setup scripts""" | |
268 | ||
269 | _active = False | |
270 | ||
271 | def __init__(self): | |
272 | self._attrs = [ | |
273 | name | |
274 | for name in dir(_os) | |
275 | if not name.startswith('_') and hasattr(self, name) | |
276 | ] | |
277 | ||
278 | def _copy(self, source): | |
279 | for name in self._attrs: | |
280 | setattr(os, name, getattr(source, name)) | |
281 | ||
282 | def __enter__(self): | |
283 | self._copy(self) | |
284 | if _file: | |
285 | builtins.file = self._file | |
286 | builtins.open = self._open | |
287 | self._active = True | |
288 | ||
289 | def __exit__(self, exc_type, exc_value, traceback): | |
290 | self._active = False | |
291 | if _file: | |
292 | builtins.file = _file | |
293 | builtins.open = _open | |
294 | self._copy(_os) | |
295 | ||
296 | def run(self, func): | |
297 | """Run 'func' under os sandboxing""" | |
298 | with self: | |
299 | return func() | |
300 | ||
301 | def _mk_dual_path_wrapper(name): | |
302 | original = getattr(_os, name) | |
303 | ||
304 | def wrap(self, src, dst, *args, **kw): | |
305 | if self._active: | |
306 | src, dst = self._remap_pair(name, src, dst, *args, **kw) | |
307 | return original(src, dst, *args, **kw) | |
308 | ||
309 | return wrap | |
310 | ||
311 | for name in ["rename", "link", "symlink"]: | |
312 | if hasattr(_os, name): | |
313 | locals()[name] = _mk_dual_path_wrapper(name) | |
314 | ||
315 | def _mk_single_path_wrapper(name, original=None): | |
316 | original = original or getattr(_os, name) | |
317 | ||
318 | def wrap(self, path, *args, **kw): | |
319 | if self._active: | |
320 | path = self._remap_input(name, path, *args, **kw) | |
321 | return original(path, *args, **kw) | |
322 | ||
323 | return wrap | |
324 | ||
325 | if _file: | |
326 | _file = _mk_single_path_wrapper('file', _file) | |
327 | _open = _mk_single_path_wrapper('open', _open) | |
328 | for name in [ | |
329 | "stat", | |
330 | "listdir", | |
331 | "chdir", | |
332 | "open", | |
333 | "chmod", | |
334 | "chown", | |
335 | "mkdir", | |
336 | "remove", | |
337 | "unlink", | |
338 | "rmdir", | |
339 | "utime", | |
340 | "lchown", | |
341 | "chroot", | |
342 | "lstat", | |
343 | "startfile", | |
344 | "mkfifo", | |
345 | "mknod", | |
346 | "pathconf", | |
347 | "access", | |
348 | ]: | |
349 | if hasattr(_os, name): | |
350 | locals()[name] = _mk_single_path_wrapper(name) | |
351 | ||
352 | def _mk_single_with_return(name): | |
353 | original = getattr(_os, name) | |
354 | ||
355 | def wrap(self, path, *args, **kw): | |
356 | if self._active: | |
357 | path = self._remap_input(name, path, *args, **kw) | |
358 | return self._remap_output(name, original(path, *args, **kw)) | |
359 | return original(path, *args, **kw) | |
360 | ||
361 | return wrap | |
362 | ||
363 | for name in ['readlink', 'tempnam']: | |
364 | if hasattr(_os, name): | |
365 | locals()[name] = _mk_single_with_return(name) | |
366 | ||
367 | def _mk_query(name): | |
368 | original = getattr(_os, name) | |
369 | ||
370 | def wrap(self, *args, **kw): | |
371 | retval = original(*args, **kw) | |
372 | if self._active: | |
373 | return self._remap_output(name, retval) | |
374 | return retval | |
375 | ||
376 | return wrap | |
377 | ||
378 | for name in ['getcwd', 'tmpnam']: | |
379 | if hasattr(_os, name): | |
380 | locals()[name] = _mk_query(name) | |
381 | ||
382 | def _validate_path(self, path): | |
383 | """Called to remap or validate any path, whether input or output""" | |
384 | return path | |
385 | ||
386 | def _remap_input(self, operation, path, *args, **kw): | |
387 | """Called for path inputs""" | |
388 | return self._validate_path(path) | |
389 | ||
390 | def _remap_output(self, operation, path): | |
391 | """Called for path outputs""" | |
392 | return self._validate_path(path) | |
393 | ||
394 | def _remap_pair(self, operation, src, dst, *args, **kw): | |
395 | """Called for path pairs like rename, link, and symlink operations""" | |
396 | return ( | |
397 | self._remap_input(operation + '-from', src, *args, **kw), | |
398 | self._remap_input(operation + '-to', dst, *args, **kw), | |
399 | ) | |
400 | ||
401 | ||
402 | if hasattr(os, 'devnull'): | |
403 | _EXCEPTIONS = [os.devnull] | |
404 | else: | |
405 | _EXCEPTIONS = [] | |
406 | ||
407 | ||
408 | class DirectorySandbox(AbstractSandbox): | |
409 | """Restrict operations to a single subdirectory - pseudo-chroot""" | |
410 | ||
411 | write_ops = dict.fromkeys( | |
412 | [ | |
413 | "open", | |
414 | "chmod", | |
415 | "chown", | |
416 | "mkdir", | |
417 | "remove", | |
418 | "unlink", | |
419 | "rmdir", | |
420 | "utime", | |
421 | "lchown", | |
422 | "chroot", | |
423 | "mkfifo", | |
424 | "mknod", | |
425 | "tempnam", | |
426 | ] | |
427 | ) | |
428 | ||
429 | _exception_patterns = [] | |
430 | "exempt writing to paths that match the pattern" | |
431 | ||
432 | def __init__(self, sandbox, exceptions=_EXCEPTIONS): | |
433 | self._sandbox = os.path.normcase(os.path.realpath(sandbox)) | |
434 | self._prefix = os.path.join(self._sandbox, '') | |
435 | self._exceptions = [ | |
436 | os.path.normcase(os.path.realpath(path)) for path in exceptions | |
437 | ] | |
438 | AbstractSandbox.__init__(self) | |
439 | ||
440 | def _violation(self, operation, *args, **kw): | |
441 | from setuptools.sandbox import SandboxViolation | |
442 | ||
443 | raise SandboxViolation(operation, args, kw) | |
444 | ||
445 | if _file: | |
446 | ||
447 | def _file(self, path, mode='r', *args, **kw): | |
448 | if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path): | |
449 | self._violation("file", path, mode, *args, **kw) | |
450 | return _file(path, mode, *args, **kw) | |
451 | ||
452 | def _open(self, path, mode='r', *args, **kw): | |
453 | if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path): | |
454 | self._violation("open", path, mode, *args, **kw) | |
455 | return _open(path, mode, *args, **kw) | |
456 | ||
457 | def tmpnam(self): | |
458 | self._violation("tmpnam") | |
459 | ||
460 | def _ok(self, path): | |
461 | active = self._active | |
462 | try: | |
463 | self._active = False | |
464 | realpath = os.path.normcase(os.path.realpath(path)) | |
465 | return ( | |
466 | self._exempted(realpath) | |
467 | or realpath == self._sandbox | |
468 | or realpath.startswith(self._prefix) | |
469 | ) | |
470 | finally: | |
471 | self._active = active | |
472 | ||
473 | def _exempted(self, filepath): | |
474 | start_matches = ( | |
475 | filepath.startswith(exception) for exception in self._exceptions | |
476 | ) | |
477 | pattern_matches = ( | |
478 | re.match(pattern, filepath) for pattern in self._exception_patterns | |
479 | ) | |
480 | candidates = itertools.chain(start_matches, pattern_matches) | |
481 | return any(candidates) | |
482 | ||
483 | def _remap_input(self, operation, path, *args, **kw): | |
484 | """Called for path inputs""" | |
485 | if operation in self.write_ops and not self._ok(path): | |
486 | self._violation(operation, os.path.realpath(path), *args, **kw) | |
487 | return path | |
488 | ||
489 | def _remap_pair(self, operation, src, dst, *args, **kw): | |
490 | """Called for path pairs like rename, link, and symlink operations""" | |
491 | if not self._ok(src) or not self._ok(dst): | |
492 | self._violation(operation, src, dst, *args, **kw) | |
493 | return (src, dst) | |
494 | ||
495 | def open(self, file, flags, mode=0o777, *args, **kw): | |
496 | """Called for low-level os.open()""" | |
497 | if flags & WRITE_FLAGS and not self._ok(file): | |
498 | self._violation("os.open", file, flags, mode, *args, **kw) | |
499 | return _os.open(file, flags, mode, *args, **kw) | |
500 | ||
501 | ||
502 | WRITE_FLAGS = functools.reduce( | |
503 | operator.or_, | |
504 | [ | |
505 | getattr(_os, a, 0) | |
506 | for a in "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split() | |
507 | ], | |
508 | ) | |
509 | ||
510 | ||
511 | class SandboxViolation(DistutilsError): | |
512 | """A setup script attempted to modify the filesystem outside the sandbox""" | |
513 | ||
514 | tmpl = textwrap.dedent( | |
515 | """ | |
516 | SandboxViolation: {cmd}{args!r} {kwargs} | |
517 | ||
518 | The package setup script has attempted to modify files on your system | |
519 | that are not within the EasyInstall build area, and has been aborted. | |
520 | ||
521 | This package cannot be safely installed by EasyInstall, and may not | |
522 | support alternate installation locations even if you run its setup | |
523 | script by hand. Please inform the package's author and the EasyInstall | |
524 | maintainers to find out if a fix or workaround is available. | |
525 | """ | |
526 | ).lstrip() | |
527 | ||
528 | def __str__(self): | |
529 | cmd, args, kwargs = self.args | |
530 | return self.tmpl.format(**locals()) |