Added publish --target-feed option
[0compile.git] / support.py
blobcd47a45123b9f3cfc139a7ce94876dfa8ababb8d
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: 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()
212 return '%s-%s-%s' % (self.iface_name.lower(), self.target_arch.lower(), version)
214 def load_built_feed(self):
215 path = self.local_iface_file
216 stream = file(path)
217 try:
218 feed = model.ZeroInstallFeed(qdom.parse(stream), local_path = path)
219 finally:
220 stream.close()
221 return feed
223 def load_built_selections(self):
224 path = join(self.metadir, 'build-environment.xml')
225 if os.path.exists(path):
226 stream = file(path)
227 try:
228 return selections.Selections(qdom.parse(stream))
229 finally:
230 stream.close()
231 return None
233 @property
234 def download_base_url(self):
235 return self.config.get('compile', 'download-base-url')
237 def chosen_impl(self, uri):
238 sels = self.get_selections()
239 assert uri in sels.selections
240 return sels.selections[uri]
242 @property
243 def local_download_iface(self):
244 impl, = self.load_built_feed().implementations.values()
245 return '%s-%s.xml' % (self.iface_name, impl.get_version())
247 def save(self):
248 stream = file(ENV_FILE, 'w')
249 try:
250 self.config.write(stream)
251 finally:
252 stream.close()
254 def get_selections(self, prompt = False):
255 if self._selections:
256 assert not prompt
257 return self._selections
259 selections_file = self.config.get('compile', 'selections')
260 if selections_file:
261 if prompt:
262 raise SafeException("Selections are fixed by %s" % selections_file)
263 stream = file(selections_file)
264 try:
265 self._selections = selections.Selections(qdom.parse(stream))
266 finally:
267 stream.close()
268 from zeroinstall.injector import fetch
269 from zeroinstall.injector.handler import Handler
270 handler = Handler()
271 fetcher = fetch.Fetcher(handler)
272 blocker = self._selections.download_missing(iface_cache, fetcher)
273 if blocker:
274 print "Waiting for selected implementations to be downloaded..."
275 handler.wait_for_blocker(blocker)
276 else:
277 options = []
278 if prompt:
279 options.append('--gui')
280 child = subprocess.Popen(launch_prog + ['--source', '--get-selections'] + options + [self.interface], stdout = subprocess.PIPE)
281 try:
282 self._selections = selections.Selections(qdom.parse(child.stdout))
283 finally:
284 if child.wait():
285 raise SafeException("0launch --get-selections failed (exit code %d)" % child.returncode)
287 self.root_impl = self._selections.selections[self.interface]
289 self.orig_srcdir = os.path.realpath(lookup(self.root_impl.id))
290 self.user_srcdir = None
292 if os.path.isdir('src'):
293 self.user_srcdir = os.path.realpath('src')
294 if self.user_srcdir == self.orig_srcdir or \
295 self.user_srcdir.startswith(self.orig_srcdir + '/') or \
296 self.orig_srcdir.startswith(self.user_srcdir + '/'):
297 info("Ignoring 'src' directory because it coincides with %s",
298 self.orig_srcdir)
299 self.user_srcdir = None
301 return self._selections
303 def get_build_changes(self):
304 sels = self.get_selections()
305 old_sels = self.load_built_selections()
306 changes = []
307 if old_sels:
308 # See if things have changed since the last build
309 all_ifaces = set(sels.selections) | set(old_sels.selections)
310 for x in all_ifaces:
311 old_impl = old_sels.selections.get(x, no_impl)
312 new_impl = sels.selections.get(x, no_impl)
313 if old_impl.version != new_impl.version:
314 changes.append("Version change for %s: %s -> %s" % (x, old_impl.version, new_impl.version))
315 elif old_impl.id != new_impl.id:
316 changes.append("Version change for %s: %s -> %s" % (x, old_impl.id, new_impl.id))
317 return changes
319 def depth(node):
320 root = node.ownerDocument.documentElement
321 depth = 0
322 while node and node is not root:
323 node = node.parentNode
324 depth += 1
325 return depth
327 format_version = model.format_version
328 parse_version = model.parse_version
330 def parse_bool(s):
331 if s == 'true': return True
332 if s == 'false': return False
333 raise SafeException('Expected "true" or "false" but got "%s"' % s)