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