Release 0.19
[0compile.git] / support.py
blobdc088485d3ae9cdf9e1409717ecef4a04d82b1ee
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(id):
43 if id.startswith('/'):
44 if os.path.isdir(id):
45 return id
46 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % id)
47 try:
48 return iface_cache.stores.lookup(id)
49 except NotStored, ex:
50 raise NotStored(str(ex) + "\nHint: try '0compile setup'")
52 def ensure_dir(d, clean = False):
53 if os.path.isdir(d):
54 if clean:
55 print "Removing", d
56 shutil.rmtree(d)
57 else:
58 return
59 if os.path.exists(d):
60 raise SafeException("'%s' exists, but is not a directory!" % d)
61 os.mkdir(d)
63 def find_in_path(prog):
64 for d in os.environ['PATH'].split(':'):
65 path = os.path.join(d, prog)
66 if os.path.isfile(path):
67 return path
68 return None
70 def spawn_and_check(prog, args):
71 status = os.spawnv(os.P_WAIT, prog, [prog] + args)
72 if status > 0:
73 raise SafeException("Program '%s' failed with exit code %d" % (prog, status))
74 elif status < 0:
75 raise SafeException("Program '%s' failed with signal %d" % (prog, -status))
77 def wait_for_child(child):
78 """Wait for child to exit and reap it. Throw an exception if it doesn't return success."""
79 pid, status = os.waitpid(child, 0)
80 assert pid == child
81 if os.WIFEXITED(status):
82 exit_code = os.WEXITSTATUS(status)
83 if exit_code == 0:
84 return
85 else:
86 raise SafeException('Command failed with exit status %d' % exit_code)
87 else:
88 raise SafeException('Command failed with signal %d' % WTERMSIG(status))
90 def spawn_maybe_sandboxed(readable, writable, tmpdir, prog, args):
91 child = os.fork()
92 if child == 0:
93 try:
94 try:
95 exec_maybe_sandboxed(readable, writable, tmpdir, prog, args)
96 except:
97 traceback.print_exc()
98 finally:
99 print >>sys.stderr, "Exec failed"
100 os._exit(1)
101 wait_for_child(child)
103 def exec_maybe_sandboxed(readable, writable, tmpdir, prog, args):
104 """execl prog, with (only) the 'writable' directories writable if sandboxing is available.
105 The readable directories will be readable, as well as various standard locations.
106 If no sandbox is available, run without a sandbox."""
108 USE_PLASH = 'USE_PLASH_0COMPILE'
110 assert prog.startswith('/')
111 _pola_run = find_in_path('pola-run')
113 if _pola_run is None:
114 print "Not using sandbox (plash not installed)"
115 use_plash = False
116 else:
117 use_plash = os.environ.get(USE_PLASH, '').lower() or 'not set'
118 if use_plash in ('not set', 'false'):
119 print "Not using plash: $%s is %s" % (USE_PLASH, use_plash)
120 use_plash = False
121 elif use_plash == 'true':
122 use_plash = True
123 else:
124 raise Exception('$%s must be "true" or "false", not "%s"' % (USE_PLASH, use_plash))
126 if not use_plash:
127 os.execlp(prog, prog, *args)
129 print "Using plash to sandbox the build..."
131 # We have pola-shell :-)
132 pola_args = ['--prog', prog, '-B']
133 for a in args:
134 pola_args += ['-a', a]
135 for r in readable:
136 pola_args += ['-f', r]
137 for w in writable:
138 pola_args += ['-fw', w]
139 pola_args += ['-tw', '/tmp', tmpdir]
140 os.environ['TMPDIR'] = '/tmp'
141 os.execl(_pola_run, _pola_run, *pola_args)
143 def get_arch_name():
144 uname = os.uname()
145 target_os, target_machine = uname[0], uname[-1]
146 if target_machine in ('i585', 'i686'):
147 target_machine = 'i486' # (sensible default)
148 return target_os + '-' + target_machine
150 class BuildEnv:
151 def __init__(self, need_config = True):
152 if need_config and not os.path.isfile(ENV_FILE):
153 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE)
155 self.config = ConfigParser.RawConfigParser()
156 self.config.add_section('compile')
157 self.config.set('compile', 'download-base-url', '')
158 self.config.set('compile', 'version-modifier', '')
159 self.config.set('compile', 'interface', '')
160 self.config.set('compile', 'selections', '')
161 self.config.set('compile', 'metadir', '0install')
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 = '%s-%s' % (self.iface_name.lower(), get_arch_name().lower())
184 assert '/' not in distdir_name
185 return os.path.realpath(distdir_name)
187 @property
188 def metadir(self):
189 metadir = self.config.get('compile', 'metadir')
190 assert not metadir.startswith('/')
191 return join(self.distdir, metadir)
193 @property
194 def local_iface_file(self):
195 return join(self.metadir, self.iface_name + '.xml')
197 @property
198 def target_arch(self):
199 return get_arch_name()
201 @property
202 def version_modifier(self):
203 vm = self.config.get('compile', 'version-modifier')
204 if vm: return vm
205 if self.user_srcdir:
206 return '-1'
207 return ''
209 @property
210 def archive_stem(self):
211 # Use the version that we actually built, not the version we would build now
212 feed = self.load_built_feed()
213 assert len(feed.implementations) == 1
214 version = feed.implementations.values()[0].get_version()
216 # Don't use the feed's name, as it may contain the version number
217 name = feed.get_name().lower().replace(' ', '-')
219 return '%s-%s-%s' % (name, self.target_arch.lower(), version)
221 def load_built_feed(self):
222 path = self.local_iface_file
223 stream = file(path)
224 try:
225 feed = model.ZeroInstallFeed(qdom.parse(stream), local_path = path)
226 finally:
227 stream.close()
228 return feed
230 def load_built_selections(self):
231 path = join(self.metadir, 'build-environment.xml')
232 if os.path.exists(path):
233 stream = file(path)
234 try:
235 return selections.Selections(qdom.parse(stream))
236 finally:
237 stream.close()
238 return None
240 @property
241 def download_base_url(self):
242 return self.config.get('compile', 'download-base-url')
244 def chosen_impl(self, uri):
245 sels = self.get_selections()
246 assert uri in sels.selections
247 return sels.selections[uri]
249 @property
250 def local_download_iface(self):
251 impl, = self.load_built_feed().implementations.values()
252 return '%s-%s.xml' % (self.iface_name, impl.get_version())
254 def save(self):
255 stream = file(ENV_FILE, 'w')
256 try:
257 self.config.write(stream)
258 finally:
259 stream.close()
261 def get_selections(self, prompt = False):
262 if self._selections:
263 assert not prompt
264 return self._selections
266 selections_file = self.config.get('compile', 'selections')
267 if selections_file:
268 if prompt:
269 raise SafeException("Selections are fixed by %s" % selections_file)
270 stream = file(selections_file)
271 try:
272 self._selections = selections.Selections(qdom.parse(stream))
273 finally:
274 stream.close()
275 from zeroinstall.injector import fetch
276 from zeroinstall.injector.handler import Handler
277 handler = Handler()
278 fetcher = fetch.Fetcher(handler)
279 blocker = self._selections.download_missing(iface_cache, fetcher)
280 if blocker:
281 print "Waiting for selected implementations to be downloaded..."
282 handler.wait_for_blocker(blocker)
283 else:
284 options = []
285 if prompt:
286 options.append('--gui')
287 child = subprocess.Popen(launch_prog + ['--source', '--get-selections'] + options + [self.interface], stdout = subprocess.PIPE)
288 try:
289 self._selections = selections.Selections(qdom.parse(child.stdout))
290 finally:
291 if child.wait():
292 raise SafeException("0launch --get-selections failed (exit code %d)" % child.returncode)
294 self.root_impl = self._selections.selections[self.interface]
296 self.orig_srcdir = os.path.realpath(lookup(self.root_impl.id))
297 self.user_srcdir = None
299 if os.path.isdir('src'):
300 self.user_srcdir = os.path.realpath('src')
301 if self.user_srcdir == self.orig_srcdir or \
302 self.user_srcdir.startswith(self.orig_srcdir + '/') or \
303 self.orig_srcdir.startswith(self.user_srcdir + '/'):
304 info("Ignoring 'src' directory because it coincides with %s",
305 self.orig_srcdir)
306 self.user_srcdir = None
308 return self._selections
310 def get_build_changes(self):
311 sels = self.get_selections()
312 old_sels = self.load_built_selections()
313 changes = []
314 if old_sels:
315 # See if things have changed since the last build
316 all_ifaces = set(sels.selections) | set(old_sels.selections)
317 for x in all_ifaces:
318 old_impl = old_sels.selections.get(x, no_impl)
319 new_impl = sels.selections.get(x, no_impl)
320 if old_impl.version != new_impl.version:
321 changes.append("Version change for %s: %s -> %s" % (x, old_impl.version, new_impl.version))
322 elif old_impl.id != new_impl.id:
323 changes.append("Version change for %s: %s -> %s" % (x, old_impl.id, new_impl.id))
324 return changes
326 def depth(node):
327 root = node.ownerDocument.documentElement
328 depth = 0
329 while node and node is not root:
330 node = node.parentNode
331 depth += 1
332 return depth
334 format_version = model.format_version
335 parse_version = model.parse_version
337 def parse_bool(s):
338 if s == 'true': return True
339 if s == 'false': return False
340 raise SafeException('Expected "true" or "false" but got "%s"' % s)