Release 1.4
[0compile.git] / support.py
blob85d6d3df3e601fbfb327b4a2787e7f55143bbb57
1 # Copyright (C) 2006, Thomas Leonard
2 # See http://0install.net/0compile.html
4 import os, sys, shutil
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, arch
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
16 from zeroinstall.support import find_in_path
18 Prefixes = qdom.Prefixes
20 def _(x): return x
23 # This is An os.uname() substitute that uses as much of ZI's
24 # arch._uname as is available and yet has all four elements one
25 # normally expects from os.uname() on Posix (on Windows, arch._uname
26 # has only two elements).
27 import platform
28 uname = arch._uname + platform.uname()[len(arch._uname):]
30 ENV_FILE = '0compile.properties'
31 XMLNS_0COMPILE = 'http://zero-install.sourceforge.net/2006/namespaces/0compile'
33 install_path = os.environ.get("0COMPILE_0INSTALL", None)
34 if install_path is None:
35 zeroinstall_dir = os.environ.get('0COMPILE_ZEROINSTALL', None)
36 if zeroinstall_dir:
37 # We've been run by an old version of 0install.
38 # We assuming that, if installed through 0install, 0launch requires
39 # the same version of Python as 0compile.
40 install_prog = [sys.executable, os.path.join(zeroinstall_dir, '0install')]
41 if not os.path.exists(install_prog[1]):
42 # For the Windows version...
43 install_prog[1] = os.path.join(zeroinstall_dir, 'zeroinstall', 'scripts', 'install.py')
44 else:
45 install_prog = ['0install']
46 else:
47 install_prog = [install_path]
49 if os.path.isdir('dependencies'):
50 dep_dir = os.path.realpath('dependencies')
51 iface_cache.stores.stores.append(Store(dep_dir))
52 install_prog.append('--with-store='+ dep_dir)
54 class NoImpl:
55 id = "none"
56 version = "none"
57 no_impl = NoImpl()
59 def is_package_impl(impl):
60 return impl.id.startswith("package:")
62 def lookup(impl_or_sel):
63 id = impl_or_sel.id
64 if id.startswith('package:'):
65 return None
66 local_path = impl_or_sel.local_path
67 if local_path is not None:
68 if os.path.isdir(local_path):
69 return local_path
70 raise SafeException("Directory '%s' no longer exists. Try '0compile setup'" % local_path)
71 try:
72 return iface_cache.stores.lookup_any(impl_or_sel.digests)
73 except NotStored, ex:
74 raise NotStored(str(ex) + "\nHint: try '0compile setup'")
76 def ensure_dir(d, clean = False):
77 if os.path.isdir(d):
78 if clean:
79 print "Removing", d
80 shutil.rmtree(d)
81 else:
82 return
83 if os.path.exists(d):
84 raise SafeException("'%s' exists, but is not a directory!" % d)
85 os.mkdir(d)
87 def spawn_and_check(prog, args):
88 status = os.spawnv(os.P_WAIT, prog, [prog] + args)
89 if status > 0:
90 raise SafeException("Program '%s' failed with exit code %d" % (prog, status))
91 elif status < 0:
92 raise SafeException("Program '%s' failed with signal %d" % (prog, -status))
94 def spawn_and_check_maybe_sandboxed(readable, writable, tmpdir, prog, args):
95 child = spawn_maybe_sandboxed(readable, writable, tmpdir, prog, args)
96 status = child.wait()
97 if status > 0:
98 raise SafeException('Command failed with exit status %d' % status)
99 elif status < 0:
100 raise SafeException('Command failed with signal %d' % -status)
102 def spawn_maybe_sandboxed(readable, writable, tmpdir, prog, args):
103 """spawn prog, with (only) the 'writable' directories writable if sandboxing is available.
104 The readable directories will be readable, as well as various standard locations.
105 If no sandbox is available, run without a sandbox."""
107 USE_PLASH = 'USE_PLASH_0COMPILE'
109 assert os.path.isabs(prog)
110 _pola_run = find_in_path('pola-run')
112 if _pola_run is None:
113 #print "Not using sandbox (plash not installed)"
114 use_plash = False
115 else:
116 use_plash = os.environ.get(USE_PLASH, '').lower() or 'not set'
117 if use_plash in ('not set', 'false'):
118 print "Not using plash: $%s is %s" % (USE_PLASH, use_plash)
119 use_plash = False
120 elif use_plash == 'true':
121 use_plash = True
122 else:
123 raise Exception('$%s must be "true" or "false", not "%s"' % (USE_PLASH, use_plash))
125 if not use_plash:
126 return subprocess.Popen([prog] + args)
128 print "Using plash to sandbox the build..."
130 # We have pola-shell :-)
131 pola_args = ['--prog', prog, '-B']
132 for a in args:
133 pola_args += ['-a', a]
134 for r in readable:
135 pola_args += ['-f', r]
136 for w in writable:
137 pola_args += ['-fw', w]
138 pola_args += ['-tw', '/tmp', tmpdir]
139 os.environ['TMPDIR'] = '/tmp'
140 return subprocess.Popen([_pola_run] + pola_args)
142 def get_arch_name():
143 target_os = canonicalize_os(uname[0])
144 target_machine = canonicalize_machine(uname[4])
145 if target_os == 'Darwin' and target_machine == 'i386':
146 # this system detection shell script comes from config.guess (20090918):
147 CC = os.getenv("CC_FOR_BUILD") or os.getenv("CC") or os.getenv("HOST_CC") or "cc"
148 process = subprocess.Popen("(echo '#ifdef __LP64__'; echo IS_64BIT_ARCH; echo '#endif') | " +
149 "(CCOPTS= %s -E - 2>/dev/null) | " % CC +
150 "grep IS_64BIT_ARCH >/dev/null", stdout=subprocess.PIPE, shell=True)
151 output, error = process.communicate()
152 retcode = process.poll()
153 if retcode == 0:
154 target_machine='x86_64'
155 if target_machine in ('i585', 'i686'):
156 target_machine = 'i486' # (sensible default)
157 return target_os + '-' + target_machine
159 class BuildEnv:
160 def __init__(self, need_config = True):
161 if need_config and not os.path.isfile(ENV_FILE):
162 raise SafeException("Run 0compile from a directory containing a '%s' file" % ENV_FILE)
164 self.config = ConfigParser.RawConfigParser()
165 self.config.add_section('compile')
166 self.config.set('compile', 'download-base-url', '')
167 self.config.set('compile', 'version-modifier', '')
168 self.config.set('compile', 'interface', '')
169 self.config.set('compile', 'selections', '')
170 self.config.set('compile', 'metadir', '0install')
171 self.config.set('compile', 'distdir', '')
173 self.config.read(ENV_FILE)
175 self._selections = None
177 return
179 @property
180 def iface_name(self):
181 iface_name = os.path.basename(self.interface)
182 if iface_name.endswith('.xml'):
183 iface_name = iface_name[:-4]
184 iface_name = iface_name.replace(' ', '-')
185 if iface_name.endswith('-src'):
186 iface_name = iface_name[:-4]
187 return iface_name
189 interface = property(lambda self: model.canonical_iface_uri(self.config.get('compile', 'interface')))
191 @property
192 def distdir(self):
193 distdir_name = self.config.get('compile', 'distdir')
194 if not distdir_name:
195 arch = self.target_arch.replace('*', 'any')
196 distdir_name = self.iface_name.lower()
197 distdir_name += '-' + arch.lower()
198 assert os.path.dirname(distdir_name) == ''
199 return os.path.realpath(distdir_name)
201 def get_binary_template(self):
202 """Find the <compile:implementation> element for the selected compile command, if any"""
203 sels = self.get_selections()
204 if sels.commands:
205 for elem in sels.commands[0].qdom.childNodes:
206 if elem.uri == XMLNS_0COMPILE and elem.name == 'implementation':
207 return elem
208 return None
210 @property
211 def metadir(self):
212 metadir = self.config.get('compile', 'metadir')
213 assert not os.path.isabs(metadir)
214 return join(self.distdir, metadir)
216 @property
217 def local_iface_file(self):
218 return join(self.metadir, 'feed.xml')
220 @property
221 def target_arch(self):
222 temp = self.get_binary_template()
223 arch = temp and temp.getAttribute('arch')
224 return arch or get_arch_name()
226 @property
227 def version_modifier(self):
228 vm = self.config.get('compile', 'version-modifier')
229 if vm: return vm
230 if self.user_srcdir:
231 return '-1'
232 return ''
234 @property
235 def archive_stem(self):
236 # Use the version that we actually built, not the version we would build now
237 feed = self.load_built_feed()
238 assert len(feed.implementations) == 1
239 version = feed.implementations.values()[0].get_version()
241 # Don't use the feed's name, as it may contain the version number
242 name = feed.get_name().lower().replace(' ', '-')
243 arch = self.target_arch.lower().replace('*-*', 'bin').replace('*', 'any')
245 return '%s-%s-%s' % (name, arch, version)
247 def load_built_feed(self):
248 path = self.local_iface_file
249 stream = file(path)
250 try:
251 feed = model.ZeroInstallFeed(qdom.parse(stream), local_path = path)
252 finally:
253 stream.close()
254 return feed
256 def load_built_selections(self):
257 path = join(self.metadir, 'build-environment.xml')
258 if os.path.exists(path):
259 stream = file(path)
260 try:
261 return selections.Selections(qdom.parse(stream))
262 finally:
263 stream.close()
264 return None
266 @property
267 def download_base_url(self):
268 return self.config.get('compile', 'download-base-url')
270 def chosen_impl(self, uri):
271 sels = self.get_selections()
272 assert uri in sels.selections
273 return sels.selections[uri]
275 @property
276 def local_download_iface(self):
277 impl, = self.load_built_feed().implementations.values()
278 return '%s-%s.xml' % (self.iface_name, impl.get_version())
280 def save(self):
281 stream = file(ENV_FILE, 'w')
282 try:
283 self.config.write(stream)
284 finally:
285 stream.close()
287 def get_selections(self, prompt = False):
288 if self._selections:
289 assert not prompt
290 return self._selections
292 selections_file = self.config.get('compile', 'selections')
293 if selections_file:
294 if prompt:
295 raise SafeException("Selections are fixed by %s" % selections_file)
296 stream = file(selections_file)
297 try:
298 self._selections = selections.Selections(qdom.parse(stream))
299 finally:
300 stream.close()
301 from zeroinstall.injector import handler
302 from zeroinstall.injector.config import load_config
303 if os.isatty(1):
304 h = handler.ConsoleHandler()
305 else:
306 h = handler.Handler()
307 config = load_config(h)
308 blocker = self._selections.download_missing(config)
309 if blocker:
310 print "Waiting for selected implementations to be downloaded..."
311 h.wait_for_blocker(blocker)
312 else:
313 command = install_prog + ['download', '--source', '--xml']
314 if prompt and '--console' not in install_prog:
315 if os.name == 'nt':
316 command[0] += '-win'
317 command.append('--gui')
318 command.append(self.interface)
319 child = subprocess.Popen(command, stdout = subprocess.PIPE)
320 try:
321 self._selections = selections.Selections(qdom.parse(child.stdout))
322 finally:
323 if child.wait():
324 raise SafeException(' '.join(repr(x) for x in command) + " failed (exit code %d)" % child.returncode)
326 self.root_impl = self._selections.selections[self.interface]
328 self.orig_srcdir = os.path.realpath(lookup(self.root_impl))
329 self.user_srcdir = None
331 if os.path.isdir('src'):
332 self.user_srcdir = os.path.realpath('src')
333 if self.user_srcdir == self.orig_srcdir or \
334 self.user_srcdir.startswith(os.path.join(self.orig_srcdir, '')) or \
335 self.orig_srcdir.startswith(os.path.join(self.user_srcdir, '')):
336 info("Ignoring 'src' directory because it coincides with %s",
337 self.orig_srcdir)
338 self.user_srcdir = None
340 return self._selections
342 def get_build_changes(self):
343 sels = self.get_selections()
344 old_sels = self.load_built_selections()
345 changes = []
346 if old_sels:
347 # See if things have changed since the last build
348 all_ifaces = set(sels.selections) | set(old_sels.selections)
349 for x in all_ifaces:
350 old_impl = old_sels.selections.get(x, no_impl)
351 new_impl = sels.selections.get(x, no_impl)
352 if old_impl.version != new_impl.version:
353 changes.append("Version change for %s: %s -> %s" % (x, old_impl.version, new_impl.version))
354 elif old_impl.id != new_impl.id:
355 changes.append("Version change for %s: %s -> %s" % (x, old_impl.id, new_impl.id))
356 return changes
358 def depth(node):
359 root = node.ownerDocument.documentElement
360 depth = 0
361 while node and node is not root:
362 node = node.parentNode
363 depth += 1
364 return depth
366 def parse_bool(s):
367 if s == 'true': return True
368 if s == 'false': return False
369 raise SafeException('Expected "true" or "false" but got "%s"' % s)