blob: 606944bd2918f711c248e304fecd9c36b35b7f1f [file] [log] [blame]
Thomas Wouters49fd7fa2006-04-21 10:40:58 +00001import os, sys, __builtin__, tempfile
2_os = sys.modules[os.name]
3_open = open
4from distutils.errors import DistutilsError
5__all__ = [
6 "AbstractSandbox", "DirectorySandbox", "SandboxViolation", "run_setup",
7]
8
9def run_setup(setup_script, args):
10 """Run a distutils setup script, sandboxed in its directory"""
11
12 old_dir = os.getcwd()
13 save_argv = sys.argv[:]
14 save_path = sys.path[:]
15 setup_dir = os.path.abspath(os.path.dirname(setup_script))
16 temp_dir = os.path.join(setup_dir,'temp')
17 if not os.path.isdir(temp_dir): os.makedirs(temp_dir)
18 save_tmp = tempfile.tempdir
19
20 try:
21 tempfile.tempdir = temp_dir
22 os.chdir(setup_dir)
23 try:
24 sys.argv[:] = [setup_script]+list(args)
25 sys.path.insert(0, setup_dir)
26 DirectorySandbox(setup_dir).run(
27 lambda: execfile(
28 "setup.py",
29 {'__file__':setup_script, '__name__':'__main__'}
30 )
31 )
32 except SystemExit, v:
33 if v.args and v.args[0]:
34 raise
35 # Normal exit, just return
36 finally:
37 os.chdir(old_dir)
38 sys.path[:] = save_path
39 sys.argv[:] = save_argv
40 tempfile.tempdir = save_tmp
41
42class AbstractSandbox:
43 """Wrap 'os' module and 'open()' builtin for virtualizing setup scripts"""
44
45 _active = False
46
47 def __init__(self):
48 self._attrs = [
49 name for name in dir(_os)
50 if not name.startswith('_') and hasattr(self,name)
51 ]
52
53 def _copy(self, source):
54 for name in self._attrs:
55 setattr(os, name, getattr(source,name))
56
57 def run(self, func):
58 """Run 'func' under os sandboxing"""
59 try:
60 self._copy(self)
61 __builtin__.open = __builtin__.file = self._open
62 self._active = True
63 return func()
64 finally:
65 self._active = False
66 __builtin__.open = __builtin__.file = _open
67 self._copy(_os)
68
69
70 def _mk_dual_path_wrapper(name):
71 original = getattr(_os,name)
72 def wrap(self,src,dst,*args,**kw):
73 if self._active:
74 src,dst = self._remap_pair(name,src,dst,*args,**kw)
75 return original(src,dst,*args,**kw)
76 return wrap
77
78
79 for name in ["rename", "link", "symlink"]:
80 if hasattr(_os,name): locals()[name] = _mk_dual_path_wrapper(name)
81
82
83 def _mk_single_path_wrapper(name, original=None):
84 original = original or getattr(_os,name)
85 def wrap(self,path,*args,**kw):
86 if self._active:
87 path = self._remap_input(name,path,*args,**kw)
88 return original(path,*args,**kw)
89 return wrap
90
91 _open = _mk_single_path_wrapper('file', _open)
92 for name in [
93 "stat", "listdir", "chdir", "open", "chmod", "chown", "mkdir",
94 "remove", "unlink", "rmdir", "utime", "lchown", "chroot", "lstat",
95 "startfile", "mkfifo", "mknod", "pathconf", "access"
96 ]:
97 if hasattr(_os,name): locals()[name] = _mk_single_path_wrapper(name)
98
99
100 def _mk_single_with_return(name):
101 original = getattr(_os,name)
102 def wrap(self,path,*args,**kw):
103 if self._active:
104 path = self._remap_input(name,path,*args,**kw)
105 return self._remap_output(name, original(path,*args,**kw))
106 return original(path,*args,**kw)
107 return wrap
108
109 for name in ['readlink', 'tempnam']:
110 if hasattr(_os,name): locals()[name] = _mk_single_with_return(name)
111
112 def _mk_query(name):
113 original = getattr(_os,name)
114 def wrap(self,*args,**kw):
115 retval = original(*args,**kw)
116 if self._active:
117 return self._remap_output(name, retval)
118 return retval
119 return wrap
120
121 for name in ['getcwd', 'tmpnam']:
122 if hasattr(_os,name): locals()[name] = _mk_query(name)
123
124 def _validate_path(self,path):
125 """Called to remap or validate any path, whether input or output"""
126 return path
127
128 def _remap_input(self,operation,path,*args,**kw):
129 """Called for path inputs"""
130 return self._validate_path(path)
131
132 def _remap_output(self,operation,path):
133 """Called for path outputs"""
134 return self._validate_path(path)
135
136 def _remap_pair(self,operation,src,dst,*args,**kw):
137 """Called for path pairs like rename, link, and symlink operations"""
138 return (
139 self._remap_input(operation+'-from',src,*args,**kw),
140 self._remap_input(operation+'-to',dst,*args,**kw)
141 )
142
143
144class DirectorySandbox(AbstractSandbox):
145 """Restrict operations to a single subdirectory - pseudo-chroot"""
146
147 write_ops = dict.fromkeys([
148 "open", "chmod", "chown", "mkdir", "remove", "unlink", "rmdir",
149 "utime", "lchown", "chroot", "mkfifo", "mknod", "tempnam",
150 ])
151
152 def __init__(self,sandbox):
153 self._sandbox = os.path.normcase(os.path.realpath(sandbox))
154 self._prefix = os.path.join(self._sandbox,'')
155 AbstractSandbox.__init__(self)
156
157 def _violation(self, operation, *args, **kw):
158 raise SandboxViolation(operation, args, kw)
159
160 def _open(self, path, mode='r', *args, **kw):
161 if mode not in ('r', 'rt', 'rb', 'rU') and not self._ok(path):
162 self._violation("open", path, mode, *args, **kw)
163 return _open(path,mode,*args,**kw)
164
165 def tmpnam(self):
166 self._violation("tmpnam")
167
168 def _ok(self,path):
169 active = self._active
170 try:
171 self._active = False
172 realpath = os.path.normcase(os.path.realpath(path))
173 if realpath==self._sandbox or realpath.startswith(self._prefix):
174 return True
175 finally:
176 self._active = active
177
178 def _remap_input(self,operation,path,*args,**kw):
179 """Called for path inputs"""
180 if operation in self.write_ops and not self._ok(path):
181 self._violation(operation, os.path.realpath(path), *args, **kw)
182 return path
183
184 def _remap_pair(self,operation,src,dst,*args,**kw):
185 """Called for path pairs like rename, link, and symlink operations"""
186 if not self._ok(src) or not self._ok(dst):
187 self._violation(operation, src, dst, *args, **kw)
188 return (src,dst)
189
190
191class SandboxViolation(DistutilsError):
192 """A setup script attempted to modify the filesystem outside the sandbox"""
193
194 def __str__(self):
195 return """SandboxViolation: %s%r %s
196
197The package setup script has attempted to modify files on your system
198that are not within the EasyInstall build area, and has been aborted.
199
200This package cannot be safely installed by EasyInstall, and may not
201support alternate installation locations even if you run its setup
202script by hand. Please inform the package's author and the EasyInstall
203maintainers to find out if a fix or workaround is available.""" % self.args