Updated test for new GNU Hello
[0compile.git] / support.py
blob779df3e6e0aec644a1d18345af6355d70271ab41
1 # Copyright (C) 2006, Thomas Leonard
2 # See http://0install.net/0compile.html
4 import os, sys, tempfile, 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
11 from zeroinstall.injector.model import Interface, Implementation, EnvironmentBinding, escape
12 from zeroinstall.injector import namespaces, reader
13 from zeroinstall.support import basedir
15 from zeroinstall.injector.iface_cache import iface_cache
16 from zeroinstall import SafeException
17 from zeroinstall.injector import run
18 from zeroinstall.zerostore import Stores, Store, NotStored
20 ENV_FILE = '0compile.properties'
21 XMLNS_0COMPILE = 'http://zero-install.sourceforge.net/2006/namespaces/0compile'
23 zeroinstall_dir = os.environ.get('0COMPILE_ZEROINSTALL', None)
24 if zeroinstall_dir:
25 launch_prog = [os.path.join(zeroinstall_dir, '0launch')]
26 else:
27 launch_prog = ['0launch']
29 if os.path.isdir('dependencies'):
30 dep_dir = os.path.realpath('dependencies')
31 iface_cache.stores.stores.append(Store(dep_dir))
32 launch_prog += ['--with-store', dep_dir]
34 class NoImpl:
35 id = "none"
36 version = "none"
37 no_impl = NoImpl()
39 def is_package_impl(impl):
40 return impl.id.startswith("package:")
42 def lookup(impl_or_sel):
43 id = impl_or_sel.id
44 if id.startswith('/'):
45 if os.path.isdir(id):
46 return id
47 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % id)
48 try:
49 return iface_cache.stores.lookup_any(impl_or_sel.digests)
50 except NotStored, ex:
51 raise NotStored(str(ex) + "\nHint: try '0compile setup'")
53 def ensure_dir(d, clean = False):
54 if os.path.isdir(d):
55 if clean:
56 print "Removing", d
57 shutil.rmtree(d)
58 else:
59 return
60 if os.path.exists(d):
61 raise SafeException("'%s' exists, but is not a directory!" % d)
62 os.mkdir(d)
64 def find_in_path(prog):
65 for d in os.environ['PATH'].split(':'):
66 path = os.path.join(d, prog)
67 if os.path.isfile(path):
68 return path
69 return None
71 def spawn_and_check(prog, args):
72 status = os.spawnv(os.P_WAIT, prog, [prog] + args)
73 if status > 0:
74 raise SafeException("Program '%s' failed with exit code %d" % (prog, status))
75 elif status < 0:
76 raise SafeException("Program '%s' failed with signal %d" % (prog, -status))
78 def wait_for_child(child):
79 """Wait for child to exit and reap it. Throw an exception if it doesn't return success."""
80 pid, status = os.waitpid(child, 0)
81 assert pid == child
82 if os.WIFEXITED(status):
83 exit_code = os.WEXITSTATUS(status)
84 if exit_code == 0:
85 return
86 else:
87 raise SafeException('Command failed with exit status %d' % exit_code)
88 else:
89 raise SafeException('Command failed with signal %d' % WTERMSIG(status))
91 def spawn_maybe_sandboxed(readable, writable, tmpdir, prog, args):
92 child = os.fork()
93 if child == 0:
94 try:
95 try:
96 exec_maybe_sandboxed(readable, writable, tmpdir, prog, args)
97 except:
98 traceback.print_exc()
99 finally:
100 print >>sys.stderr, "Exec failed"
101 os._exit(1)
102 wait_for_child(child)
104 def exec_maybe_sandboxed(readable, writable, tmpdir, prog, args):
105 """execl prog, with (only) the 'writable' directories writable if sandboxing is available.
106 The readable directories will be readable, as well as various standard locations.
107 If no sandbox is available, run without a sandbox."""
109 USE_PLASH = 'USE_PLASH_0COMPILE'
111 assert prog.startswith('/')
112 _pola_run = find_in_path('pola-run')
114 if _pola_run is None:
115 print "Not using sandbox (plash not installed)"
116 use_plash = False
117 else:
118 use_plash = os.environ.get(USE_PLASH, '').lower() or 'not set'
119 if use_plash in ('not set', 'false'):
120 print "Not using plash: $%s is %s" % (USE_PLASH, use_plash)
121 use_plash = False
122 elif use_plash == 'true':
123 use_plash = True
124 else:
125 raise Exception('$%s must be "true" or "false", not "%s"' % (USE_PLASH, use_plash))
127 if not use_plash:
128 os.execlp(prog, prog, *args)
130 print "Using plash to sandbox the build..."
132 # We have pola-shell :-)
133 pola_args = ['--prog', prog, '-B']
134 for a in args:
135 pola_args += ['-a', a]
136 for r in readable:
137 pola_args += ['-f', r]
138 for w in writable:
139 pola_args += ['-fw', w]
140 pola_args += ['-tw', '/tmp', tmpdir]
141 os.environ['TMPDIR'] = '/tmp'
142 os.execl(_pola_run, _pola_run, *pola_args)
144 def get_arch_name():
145 uname = os.uname()
146 target_os, target_machine = uname[0], uname[-1]
147 if target_machine in ('i585', 'i686'):
148 target_machine = 'i486' # (sensible default)
149 return target_os + '-' + target_machine
151 class BuildEnv:
152 def __init__(self, need_config = True):
153 if need_config and not os.path.isfile(ENV_FILE):
154 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE)
156 self.config = ConfigParser.RawConfigParser()
157 self.config.add_section('compile')
158 self.config.set('compile', 'download-base-url', '')
159 self.config.set('compile', 'version-modifier', '')
160 self.config.set('compile', 'interface', '')
161 self.config.set('compile', 'selections', '')
162 self.config.set('compile', 'metadir', '0install')
164 self.config.read(ENV_FILE)
166 self._selections = None
168 return
170 @property
171 def iface_name(self):
172 iface_name = os.path.basename(self.interface)
173 if iface_name.endswith('.xml'):
174 iface_name = iface_name[:-4]
175 iface_name = iface_name.replace(' ', '-')
176 if iface_name.endswith('-src'):
177 iface_name = iface_name[:-4]
178 return iface_name
180 interface = property(lambda self: model.canonical_iface_uri(self.config.get('compile', 'interface')))
182 @property
183 def distdir(self):
184 distdir_name = '%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:
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 format_version = model.format_version
336 parse_version = model.parse_version
338 def parse_bool(s):
339 if s == 'true': return True
340 if s == 'false': return False
341 raise SafeException('Expected "true" or "false" but got "%s"' % s)