Fixed test to put binary in DISTDIR
[0compile.git] / support.py
blob878e20f0f00d662b249f58fb6375bbf5f0652fd5
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
11 from zeroinstall.injector.arch import canonicalize_os, canonicalize_machine
13 from zeroinstall.injector.iface_cache import iface_cache
14 from zeroinstall import SafeException
15 from zeroinstall.zerostore import Store, NotStored
17 def _(x): return x
19 ENV_FILE = '0compile.properties'
20 XMLNS_0COMPILE = 'http://zero-install.sourceforge.net/2006/namespaces/0compile'
22 zeroinstall_dir = os.environ.get('0COMPILE_ZEROINSTALL', None)
23 if zeroinstall_dir:
24 launch_prog = [os.path.join(zeroinstall_dir, '0launch')]
25 else:
26 launch_prog = ['0launch']
28 if os.path.isdir('dependencies'):
29 dep_dir = os.path.realpath('dependencies')
30 iface_cache.stores.stores.append(Store(dep_dir))
31 launch_prog += ['--with-store', dep_dir]
33 class NoImpl:
34 id = "none"
35 version = "none"
36 no_impl = NoImpl()
38 def is_package_impl(impl):
39 return impl.id.startswith("package:")
41 def lookup(impl_or_sel):
42 id = impl_or_sel.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_any(impl_or_sel.digests)
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' % os.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 = canonicalize_os(uname[0])
146 target_machine = canonicalize_machine(uname[4])
147 if target_os == 'Darwin' and target_machine == 'i386':
148 # this system detection shell script comes from config.guess (20090918):
149 CC = os.getenv("CC_FOR_BUILD") or os.getenv("CC") or os.getenv("HOST_CC") or "cc"
150 process = subprocess.Popen("(echo '#ifdef __LP64__'; echo IS_64BIT_ARCH; echo '#endif') | " +
151 "(CCOPTS= %s -E - 2>/dev/null) | " % CC +
152 "grep IS_64BIT_ARCH >/dev/null", stdout=subprocess.PIPE, shell=True)
153 output, error = process.communicate()
154 retcode = process.poll()
155 if retcode == 0:
156 target_machine='x86_64'
157 if target_machine in ('i585', 'i686'):
158 target_machine = 'i486' # (sensible default)
159 return target_os + '-' + target_machine
161 class BuildEnv:
162 def __init__(self, need_config = True):
163 if need_config and not os.path.isfile(ENV_FILE):
164 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE)
166 self.config = ConfigParser.RawConfigParser()
167 self.config.add_section('compile')
168 self.config.set('compile', 'download-base-url', '')
169 self.config.set('compile', 'version-modifier', '')
170 self.config.set('compile', 'interface', '')
171 self.config.set('compile', 'selections', '')
172 self.config.set('compile', 'metadir', '0install')
173 self.config.set('compile', 'distdir', '')
175 self.config.read(ENV_FILE)
177 self._selections = None
179 return
181 @property
182 def iface_name(self):
183 iface_name = os.path.basename(self.interface)
184 if iface_name.endswith('.xml'):
185 iface_name = iface_name[:-4]
186 iface_name = iface_name.replace(' ', '-')
187 if iface_name.endswith('-src'):
188 iface_name = iface_name[:-4]
189 return iface_name
191 interface = property(lambda self: model.canonical_iface_uri(self.config.get('compile', 'interface')))
193 @property
194 def distdir(self):
195 distdir_name = self.config.get('compile', 'distdir') or \
196 '%s-%s' % (self.iface_name.lower(), get_arch_name().lower())
197 assert '/' not in distdir_name
198 return os.path.realpath(distdir_name)
200 @property
201 def metadir(self):
202 metadir = self.config.get('compile', 'metadir')
203 assert not metadir.startswith('/')
204 return join(self.distdir, metadir)
206 @property
207 def local_iface_file(self):
208 return join(self.metadir, self.iface_name + '.xml')
210 @property
211 def target_arch(self):
212 return get_arch_name()
214 @property
215 def version_modifier(self):
216 vm = self.config.get('compile', 'version-modifier')
217 if vm: return vm
218 if self.user_srcdir:
219 return '-1'
220 return ''
222 @property
223 def archive_stem(self):
224 # Use the version that we actually built, not the version we would build now
225 feed = self.load_built_feed()
226 assert len(feed.implementations) == 1
227 version = feed.implementations.values()[0].get_version()
229 # Don't use the feed's name, as it may contain the version number
230 name = feed.get_name().lower().replace(' ', '-')
232 return '%s-%s-%s' % (name, self.target_arch.lower(), version)
234 def load_built_feed(self):
235 path = self.local_iface_file
236 stream = file(path)
237 try:
238 feed = model.ZeroInstallFeed(qdom.parse(stream), local_path = path)
239 finally:
240 stream.close()
241 return feed
243 def load_built_selections(self):
244 path = join(self.metadir, 'build-environment.xml')
245 if os.path.exists(path):
246 stream = file(path)
247 try:
248 return selections.Selections(qdom.parse(stream))
249 finally:
250 stream.close()
251 return None
253 @property
254 def download_base_url(self):
255 return self.config.get('compile', 'download-base-url')
257 def chosen_impl(self, uri):
258 sels = self.get_selections()
259 assert uri in sels.selections
260 return sels.selections[uri]
262 @property
263 def local_download_iface(self):
264 impl, = self.load_built_feed().implementations.values()
265 return '%s-%s.xml' % (self.iface_name, impl.get_version())
267 def save(self):
268 stream = file(ENV_FILE, 'w')
269 try:
270 self.config.write(stream)
271 finally:
272 stream.close()
274 def get_selections(self, prompt = False):
275 if self._selections:
276 assert not prompt
277 return self._selections
279 selections_file = self.config.get('compile', 'selections')
280 if selections_file:
281 if prompt:
282 raise SafeException("Selections are fixed by %s" % selections_file)
283 stream = file(selections_file)
284 try:
285 self._selections = selections.Selections(qdom.parse(stream))
286 finally:
287 stream.close()
288 from zeroinstall.injector import fetch
289 from zeroinstall.injector import handler
290 if os.isatty(1):
291 h = handler.ConsoleHandler()
292 else:
293 h = handler.Handler()
295 fetcher = fetch.Fetcher(h)
296 blocker = self._selections.download_missing(iface_cache, fetcher)
297 if blocker:
298 print "Waiting for selected implementations to be downloaded..."
299 h.wait_for_blocker(blocker)
300 else:
301 options = []
302 if prompt and '--console' not in launch_prog:
303 options.append('--gui')
304 child = subprocess.Popen(launch_prog + ['--source', '--get-selections'] + options + [self.interface], stdout = subprocess.PIPE)
305 try:
306 self._selections = selections.Selections(qdom.parse(child.stdout))
307 finally:
308 if child.wait():
309 raise SafeException("0launch --get-selections failed (exit code %d)" % child.returncode)
311 self.root_impl = self._selections.selections[self.interface]
313 self.orig_srcdir = os.path.realpath(lookup(self.root_impl))
314 self.user_srcdir = None
316 if os.path.isdir('src'):
317 self.user_srcdir = os.path.realpath('src')
318 if self.user_srcdir == self.orig_srcdir or \
319 self.user_srcdir.startswith(self.orig_srcdir + '/') or \
320 self.orig_srcdir.startswith(self.user_srcdir + '/'):
321 info("Ignoring 'src' directory because it coincides with %s",
322 self.orig_srcdir)
323 self.user_srcdir = None
325 return self._selections
327 def get_build_changes(self):
328 sels = self.get_selections()
329 old_sels = self.load_built_selections()
330 changes = []
331 if old_sels:
332 # See if things have changed since the last build
333 all_ifaces = set(sels.selections) | set(old_sels.selections)
334 for x in all_ifaces:
335 old_impl = old_sels.selections.get(x, no_impl)
336 new_impl = sels.selections.get(x, no_impl)
337 if old_impl.version != new_impl.version:
338 changes.append("Version change for %s: %s -> %s" % (x, old_impl.version, new_impl.version))
339 elif old_impl.id != new_impl.id:
340 changes.append("Version change for %s: %s -> %s" % (x, old_impl.id, new_impl.id))
341 return changes
343 def depth(node):
344 root = node.ownerDocument.documentElement
345 depth = 0
346 while node and node is not root:
347 node = node.parentNode
348 depth += 1
349 return depth
351 def parse_bool(s):
352 if s == 'true': return True
353 if s == 'false': return False
354 raise SafeException('Expected "true" or "false" but got "%s"' % s)