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