Removed unused imports (pyflakes) and fixed some missing symbol bugs
[0compile.git] / support.py
blob32fef88cd9ad5a07b90dc5f5184431b7f4c4b10d
1 # Copyright (C) 2006, Thomas Leonard
2 # See http://0install.net/0compile.html
4 import os, sys, shutil, traceback
5 import subprocess
6 from os.path import join
7 from logging import info
8 import ConfigParser
10 from zeroinstall.injector import model, selections, qdom
12 from zeroinstall.injector.iface_cache import iface_cache
13 from zeroinstall import SafeException
14 from zeroinstall.zerostore import Store, NotStored
16 def _(x): return x
18 ENV_FILE = '0compile.properties'
19 XMLNS_0COMPILE = 'http://zero-install.sourceforge.net/2006/namespaces/0compile'
21 zeroinstall_dir = os.environ.get('0COMPILE_ZEROINSTALL', None)
22 if zeroinstall_dir:
23 launch_prog = [os.path.join(zeroinstall_dir, '0launch')]
24 else:
25 launch_prog = ['0launch']
27 if os.path.isdir('dependencies'):
28 dep_dir = os.path.realpath('dependencies')
29 iface_cache.stores.stores.append(Store(dep_dir))
30 launch_prog += ['--with-store', dep_dir]
32 class NoImpl:
33 id = "none"
34 version = "none"
35 no_impl = NoImpl()
37 def is_package_impl(impl):
38 return impl.id.startswith("package:")
40 def lookup(impl_or_sel):
41 id = impl_or_sel.id
42 if id.startswith('/'):
43 if os.path.isdir(id):
44 return id
45 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % id)
46 try:
47 return iface_cache.stores.lookup_any(impl_or_sel.digests)
48 except NotStored, ex:
49 raise NotStored(str(ex) + "\nHint: try '0compile setup'")
51 def ensure_dir(d, clean = False):
52 if os.path.isdir(d):
53 if clean:
54 print "Removing", d
55 shutil.rmtree(d)
56 else:
57 return
58 if os.path.exists(d):
59 raise SafeException("'%s' exists, but is not a directory!" % d)
60 os.mkdir(d)
62 def find_in_path(prog):
63 for d in os.environ['PATH'].split(':'):
64 path = os.path.join(d, prog)
65 if os.path.isfile(path):
66 return path
67 return None
69 def spawn_and_check(prog, args):
70 status = os.spawnv(os.P_WAIT, prog, [prog] + args)
71 if status > 0:
72 raise SafeException("Program '%s' failed with exit code %d" % (prog, status))
73 elif status < 0:
74 raise SafeException("Program '%s' failed with signal %d" % (prog, -status))
76 def wait_for_child(child):
77 """Wait for child to exit and reap it. Throw an exception if it doesn't return success."""
78 pid, status = os.waitpid(child, 0)
79 assert pid == child
80 if os.WIFEXITED(status):
81 exit_code = os.WEXITSTATUS(status)
82 if exit_code == 0:
83 return
84 else:
85 raise SafeException('Command failed with exit status %d' % exit_code)
86 else:
87 raise SafeException('Command failed with signal %d' % os.WTERMSIG(status))
89 def spawn_maybe_sandboxed(readable, writable, tmpdir, prog, args):
90 child = os.fork()
91 if child == 0:
92 try:
93 try:
94 exec_maybe_sandboxed(readable, writable, tmpdir, prog, args)
95 except:
96 traceback.print_exc()
97 finally:
98 print >>sys.stderr, "Exec failed"
99 os._exit(1)
100 wait_for_child(child)
102 def exec_maybe_sandboxed(readable, writable, tmpdir, prog, args):
103 """execl prog, with (only) the 'writable' directories writable if sandboxing is available.
104 The readable directories will be readable, as well as various standard locations.
105 If no sandbox is available, run without a sandbox."""
107 USE_PLASH = 'USE_PLASH_0COMPILE'
109 assert prog.startswith('/')
110 _pola_run = find_in_path('pola-run')
112 if _pola_run is None:
113 print "Not using sandbox (plash not installed)"
114 use_plash = False
115 else:
116 use_plash = os.environ.get(USE_PLASH, '').lower() or 'not set'
117 if use_plash in ('not set', 'false'):
118 print "Not using plash: $%s is %s" % (USE_PLASH, use_plash)
119 use_plash = False
120 elif use_plash == 'true':
121 use_plash = True
122 else:
123 raise Exception('$%s must be "true" or "false", not "%s"' % (USE_PLASH, use_plash))
125 if not use_plash:
126 os.execlp(prog, prog, *args)
128 print "Using plash to sandbox the build..."
130 # We have pola-shell :-)
131 pola_args = ['--prog', prog, '-B']
132 for a in args:
133 pola_args += ['-a', a]
134 for r in readable:
135 pola_args += ['-f', r]
136 for w in writable:
137 pola_args += ['-fw', w]
138 pola_args += ['-tw', '/tmp', tmpdir]
139 os.environ['TMPDIR'] = '/tmp'
140 os.execl(_pola_run, _pola_run, *pola_args)
142 def get_arch_name():
143 uname = os.uname()
144 target_os, target_machine = uname[0], uname[-1]
145 if target_machine in ('i585', 'i686'):
146 target_machine = 'i486' # (sensible default)
147 return target_os + '-' + target_machine
149 class BuildEnv:
150 def __init__(self, need_config = True):
151 if need_config and not os.path.isfile(ENV_FILE):
152 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE)
154 self.config = ConfigParser.RawConfigParser()
155 self.config.add_section('compile')
156 self.config.set('compile', 'download-base-url', '')
157 self.config.set('compile', 'version-modifier', '')
158 self.config.set('compile', 'interface', '')
159 self.config.set('compile', 'selections', '')
160 self.config.set('compile', 'metadir', '0install')
161 self.config.set('compile', 'distdir', '')
163 self.config.read(ENV_FILE)
165 self._selections = None
167 return
169 @property
170 def iface_name(self):
171 iface_name = os.path.basename(self.interface)
172 if iface_name.endswith('.xml'):
173 iface_name = iface_name[:-4]
174 iface_name = iface_name.replace(' ', '-')
175 if iface_name.endswith('-src'):
176 iface_name = iface_name[:-4]
177 return iface_name
179 interface = property(lambda self: model.canonical_iface_uri(self.config.get('compile', 'interface')))
181 @property
182 def distdir(self):
183 distdir_name = self.config.get('compile', 'distdir') or \
184 '%s-%s' % (self.iface_name.lower(), get_arch_name().lower())
185 assert '/' not in distdir_name
186 return os.path.realpath(distdir_name)
188 @property
189 def metadir(self):
190 metadir = self.config.get('compile', 'metadir')
191 assert not metadir.startswith('/')
192 return join(self.distdir, metadir)
194 @property
195 def local_iface_file(self):
196 return join(self.metadir, self.iface_name + '.xml')
198 @property
199 def target_arch(self):
200 return get_arch_name()
202 @property
203 def version_modifier(self):
204 vm = self.config.get('compile', 'version-modifier')
205 if vm: return vm
206 if self.user_srcdir:
207 return '-1'
208 return ''
210 @property
211 def archive_stem(self):
212 # Use the version that we actually built, not the version we would build now
213 feed = self.load_built_feed()
214 assert len(feed.implementations) == 1
215 version = feed.implementations.values()[0].get_version()
217 # Don't use the feed's name, as it may contain the version number
218 name = feed.get_name().lower().replace(' ', '-')
220 return '%s-%s-%s' % (name, self.target_arch.lower(), version)
222 def load_built_feed(self):
223 path = self.local_iface_file
224 stream = file(path)
225 try:
226 feed = model.ZeroInstallFeed(qdom.parse(stream), local_path = path)
227 finally:
228 stream.close()
229 return feed
231 def load_built_selections(self):
232 path = join(self.metadir, 'build-environment.xml')
233 if os.path.exists(path):
234 stream = file(path)
235 try:
236 return selections.Selections(qdom.parse(stream))
237 finally:
238 stream.close()
239 return None
241 @property
242 def download_base_url(self):
243 return self.config.get('compile', 'download-base-url')
245 def chosen_impl(self, uri):
246 sels = self.get_selections()
247 assert uri in sels.selections
248 return sels.selections[uri]
250 @property
251 def local_download_iface(self):
252 impl, = self.load_built_feed().implementations.values()
253 return '%s-%s.xml' % (self.iface_name, impl.get_version())
255 def save(self):
256 stream = file(ENV_FILE, 'w')
257 try:
258 self.config.write(stream)
259 finally:
260 stream.close()
262 def get_selections(self, prompt = False):
263 if self._selections:
264 assert not prompt
265 return self._selections
267 selections_file = self.config.get('compile', 'selections')
268 if selections_file:
269 if prompt:
270 raise SafeException("Selections are fixed by %s" % selections_file)
271 stream = file(selections_file)
272 try:
273 self._selections = selections.Selections(qdom.parse(stream))
274 finally:
275 stream.close()
276 from zeroinstall.injector import fetch
277 from zeroinstall.injector.handler import Handler
278 handler = Handler()
279 fetcher = fetch.Fetcher(handler)
280 blocker = self._selections.download_missing(iface_cache, fetcher)
281 if blocker:
282 print "Waiting for selected implementations to be downloaded..."
283 handler.wait_for_blocker(blocker)
284 else:
285 options = []
286 if prompt and '--console' not in launch_prog:
287 options.append('--gui')
288 child = subprocess.Popen(launch_prog + ['--source', '--get-selections'] + options + [self.interface], stdout = subprocess.PIPE)
289 try:
290 self._selections = selections.Selections(qdom.parse(child.stdout))
291 finally:
292 if child.wait():
293 raise SafeException("0launch --get-selections failed (exit code %d)" % child.returncode)
295 self.root_impl = self._selections.selections[self.interface]
297 self.orig_srcdir = os.path.realpath(lookup(self.root_impl))
298 self.user_srcdir = None
300 if os.path.isdir('src'):
301 self.user_srcdir = os.path.realpath('src')
302 if self.user_srcdir == self.orig_srcdir or \
303 self.user_srcdir.startswith(self.orig_srcdir + '/') or \
304 self.orig_srcdir.startswith(self.user_srcdir + '/'):
305 info("Ignoring 'src' directory because it coincides with %s",
306 self.orig_srcdir)
307 self.user_srcdir = None
309 return self._selections
311 def get_build_changes(self):
312 sels = self.get_selections()
313 old_sels = self.load_built_selections()
314 changes = []
315 if old_sels:
316 # See if things have changed since the last build
317 all_ifaces = set(sels.selections) | set(old_sels.selections)
318 for x in all_ifaces:
319 old_impl = old_sels.selections.get(x, no_impl)
320 new_impl = sels.selections.get(x, no_impl)
321 if old_impl.version != new_impl.version:
322 changes.append("Version change for %s: %s -> %s" % (x, old_impl.version, new_impl.version))
323 elif old_impl.id != new_impl.id:
324 changes.append("Version change for %s: %s -> %s" % (x, old_impl.id, new_impl.id))
325 return changes
327 def depth(node):
328 root = node.ownerDocument.documentElement
329 depth = 0
330 while node and node is not root:
331 node = node.parentNode
332 depth += 1
333 return depth
335 def parse_bool(s):
336 if s == 'true': return True
337 if s == 'false': return False
338 raise SafeException('Expected "true" or "false" but got "%s"' % s)