When returning up the build stack, redisplay the heading
[0compile.git] / autocompile.py
blobd9855f67ab4a199fb5908e8089ca327ca934994c
1 # Copyright (C) 2009, Thomas Leonard
2 # See http://0install.net/0compile.html
4 import sys, os, __main__, tempfile, shutil, subprocess, signal
5 from xml.dom import minidom
6 from optparse import OptionParser
8 from zeroinstall import SafeException
9 from zeroinstall.injector import arch, handler, policy, model, iface_cache, selections, namespaces, writer, reader
10 from zeroinstall.zerostore import manifest
11 from zeroinstall.support import tasks, basedir
13 from support import BuildEnv
15 # This is a bit hacky...
17 # We invent a new CPU type which is compatible with the host but worse than
18 # every existing type, and we use * for the OS type so that we don't beat 'Any'
19 # binaries either. This means that we always prefer an existing binary of the
20 # desired version to compiling a new one, but we'll compile a new version from source
21 # rather than use an older binary.
22 arch.machine_groups['newbuild'] = arch.machine_groups.get(arch._uname[-1], 0)
23 arch.machine_ranks['newbuild'] = max(arch.machine_ranks.values()) + 1
24 host_arch = '*-newbuild'
26 class AutocompileCache(iface_cache.IfaceCache):
27 def __init__(self):
28 iface_cache.IfaceCache.__init__(self)
29 self.done = set()
31 def get_interface(self, uri):
32 iface = iface_cache.IfaceCache.get_interface(self, uri)
33 if not iface: return None
34 feed = iface._main_feed
36 # Note: when a feed is updated, a new ZeroInstallFeed object is created,
37 # so record whether we've seen the feed, not the interface.
39 if feed not in self.done:
40 self.done.add(feed)
42 # For each source impl, add a corresponding binary
43 # (the binary has no dependencies as we can't predict them here,
44 # but they're not the same as the source's dependencies)
46 srcs = [x for x in feed.implementations.itervalues() if x.arch and x.arch.endswith('-src')]
47 for x in srcs:
48 new_id = '0compile=' + x.id
49 if not new_id in feed.implementations:
50 new = feed._get_impl(new_id)
51 new.set_arch(host_arch)
52 new.version = x.version
54 return iface
56 policy.iface_cache = AutocompileCache()
58 class MyHandler(handler.Handler):
59 def downloads_changed(self):
60 self.compiler.downloads_changed()
62 class AutoCompiler:
63 def __init__(self, iface_uri):
64 self.iface_uri = iface_uri
65 self.handler = MyHandler()
66 self.handler.compiler = self
68 def downloads_changed(self):
69 if self.handler.monitored_downloads:
70 self.note('Downloads in progress:')
71 for x in self.handler.monitored_downloads:
72 self.note('- %s' % x)
73 else:
74 self.note('No downloads remaining.')
76 def pretty_print_plan(self, solver, root, indent = '- '):
77 """Display a tree showing the selected implementations."""
78 iface = solver.iface_cache.get_interface(root)
79 impl = solver.selections[iface]
80 if impl is None:
81 msg = 'Failed to select any suitable version (source or binary)'
82 elif impl.id.startswith('0compile='):
83 real_impl_id = impl.id.split('=', 1)[1]
84 real_impl = impl.feed.implementations[real_impl_id]
85 msg = 'Compile %s (%s)' % (real_impl.get_version(), real_impl.id)
86 elif impl.arch and impl.arch.endswith('-src'):
87 msg = 'Compile %s (%s)' % (impl.get_version(), impl.id)
88 else:
89 if impl.arch:
90 msg = 'Use existing binary %s (%s)' % (impl.get_version(), impl.arch)
91 else:
92 msg = 'Use existing architecture-independent package %s' % impl.get_version()
93 self.note("%s%s: %s" % (indent, iface.get_name(), msg))
95 if impl:
96 indent = ' ' + indent
97 for x in impl.requires:
98 self.pretty_print_plan(solver, x.interface, indent)
100 def print_details(self, solver):
101 """Dump debugging details."""
102 self.note("\nDetails of all components and versions considered:")
103 for iface in solver.details:
104 self.note('\n%s\n' % iface.get_name())
105 for impl, note in solver.details[iface]:
106 self.note('%s (%s) : %s' % (impl.get_version(), impl.arch or '*-*', note or 'OK'))
107 self.note("\nEnd details")
109 @tasks.async
110 def compile_and_register(self, policy):
111 local_feed_dir = basedir.save_config_path('0install.net', '0compile', 'builds', model._pretty_escape(policy.root))
112 s = selections.Selections(policy)
114 buildenv = BuildEnv(need_config = False)
115 buildenv.config.set('compile', 'interface', policy.root)
116 buildenv.config.set('compile', 'selections', 'selections.xml')
118 version = s.selections[policy.root].version
119 local_feed = os.path.join(local_feed_dir, '%s-%s-%s.xml' % (buildenv.iface_name, version, arch._uname[-1]))
120 if os.path.exists(local_feed):
121 raise SafeException("Build metadata file '%s' already exists!" % local_feed)
123 tmpdir = tempfile.mkdtemp(prefix = '0compile-')
124 try:
125 os.chdir(tmpdir)
127 # Write configuration for build...
129 buildenv.save()
131 sel_file = open('selections.xml', 'w')
132 try:
133 doc = s.toDOM()
134 doc.writexml(sel_file)
135 sel_file.write('\n')
136 finally:
137 sel_file.close()
139 # Do the build...
141 build = self.spawn_build(buildenv.iface_name)
142 if build:
143 yield build
144 tasks.check(build)
146 # Register the result...
148 alg = manifest.get_algorithm('sha1new')
149 digest = alg.new_digest()
150 lines = []
151 for line in alg.generate_manifest(buildenv.distdir):
152 line += '\n'
153 digest.update(line)
154 lines.append(line)
155 actual_digest = alg.getID(digest)
157 local_feed_file = file(local_feed, 'w')
158 try:
159 dom = minidom.parse(buildenv.local_iface_file)
160 impl, = dom.getElementsByTagNameNS(namespaces.XMLNS_IFACE, 'implementation')
161 impl.setAttribute('id', actual_digest)
162 dom.writexml(local_feed_file)
163 local_feed_file.write('\n')
164 finally:
165 local_feed_file.close()
167 self.note("Implementation metadata written to %s" % local_feed)
169 self.note("Storing build in cache...")
170 policy.solver.iface_cache.stores.add_dir_to_cache(actual_digest, buildenv.distdir)
172 self.note("Registering feed...")
173 iface = policy.solver.iface_cache.get_interface(policy.root)
174 feed = iface.get_feed(local_feed)
175 if feed:
176 self.note("WARNING: feed %s already registered!" % local_feed)
177 else:
178 iface.extra_feeds.append(model.Feed(local_feed, impl.getAttribute('arch'), user_override = True))
179 writer.save_interface(iface)
181 # We might have cached an old version
182 new_feed = policy.solver.iface_cache.get_interface(local_feed)
183 reader.update_from_cache(new_feed)
184 except:
185 self.note("\nBuild failed: leaving build directory %s for inspection...\n" % tmpdir)
186 raise
187 else:
188 shutil.rmtree(tmpdir)
190 @tasks.async
191 def recursive_build(self, iface_uri, version = None):
192 p = policy.Policy(iface_uri, handler = self.handler, src = True)
193 iface = p.solver.iface_cache.get_interface(iface_uri)
194 p.solver.record_details = True
195 if version:
196 p.solver.extra_restrictions[iface] = [model.VersionRestriction(model.parse_version(version))]
198 # For testing...
199 #p.target_arch = arch.Architecture(os_ranks = {'FreeBSD': 0, None: 1}, machine_ranks = {'i386': 0, None: 1, 'newbuild': 2})
201 while True:
202 self.heading(iface_uri)
203 self.note("\nSelecting versions for %s..." % iface.get_name())
204 solved = p.solve_with_downloads()
205 if solved:
206 yield solved
207 tasks.check(solved)
209 if not p.solver.ready:
210 self.print_details(p.solver)
211 raise SafeException("Can't find all required implementations (source or binary):\n" +
212 '\n'.join(["- %s -> %s" % (iface, p.solver.selections[iface])
213 for iface in p.solver.selections]))
214 self.note("Selection done.")
216 self.note("\nPlan:\n")
217 self.pretty_print_plan(p.solver, p.root)
218 self.note('')
220 for dep_iface, dep_impl in p.solver.selections.iteritems():
221 if dep_impl.id.startswith('0compile='):
222 build = self.recursive_build(dep_iface.uri, dep_impl.get_version())
223 yield build
224 tasks.check(build)
225 break # Try again with that dependency built...
226 else:
227 self.note("No dependencies need compiling... compile %s itself..." % iface.get_name())
228 build = self.compile_and_register(p)
229 yield build
230 tasks.check(build)
231 return
233 def spawn_build(self, iface_name):
234 subprocess.check_call([sys.executable, sys.argv[0], 'build'])
236 def build(self):
237 self.handler.wait_for_blocker(self.recursive_build(self.iface_uri))
239 def heading(self, msg):
240 self.note((' %s ' % msg).center(76, '='))
242 def note(self, msg):
243 print msg
245 def note_error(self, msg):
246 self.overall.insert_at_cursor(msg + '\n')
248 class GTKAutoCompiler(AutoCompiler):
249 def __init__(self, iface_uri):
250 AutoCompiler.__init__(self, iface_uri)
251 self.child = None
253 import pygtk; pygtk.require('2.0')
254 import gtk
256 w = gtk.Dialog('Autocompile %s' % iface_uri, None, 0,
257 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
258 gtk.STOCK_OK, gtk.RESPONSE_OK))
259 self.dialog = w
261 w.set_default_size(int(gtk.gdk.screen_width() * 0.8),
262 int(gtk.gdk.screen_height() * 0.8))
264 vpaned = gtk.VPaned()
265 w.vbox.add(vpaned)
266 w.set_response_sensitive(gtk.RESPONSE_OK, False)
268 class AutoScroller:
269 def __init__(self):
270 tv = gtk.TextView()
271 tv.set_property('left-margin', 8)
272 tv.set_wrap_mode(gtk.WRAP_WORD_CHAR)
273 tv.set_editable(False)
274 swin = gtk.ScrolledWindow()
275 swin.add(tv)
276 swin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
277 buffer = tv.get_buffer()
279 heading = buffer.create_tag('heading')
280 heading.set_property('scale', 1.5)
282 error = buffer.create_tag('error')
283 error.set_property('background', 'white')
284 error.set_property('foreground', 'red')
286 self.tv = tv
287 self.widget = swin
288 self.buffer = buffer
290 def insert_at_end_and_scroll(self, data, *tags):
291 # TODO: data might not be complete UTF-8; buffer until EOL?
292 self.vscroll = self.widget.get_vadjustment()
293 near_end = self.vscroll.upper - self.vscroll.page_size * 1.5 < self.vscroll.value
294 end = self.buffer.get_end_iter()
295 self.buffer.insert_with_tags_by_name(end, data, *tags)
296 if near_end:
297 cursor = self.buffer.get_insert()
298 self.buffer.move_mark(cursor, end)
299 self.tv.scroll_to_mark(cursor, 0, False, 0, 0)
301 self.overall = AutoScroller()
302 self.details = AutoScroller()
304 vpaned.pack1(self.overall.widget, True, False)
305 vpaned.pack2(self.details.widget, True, False)
307 self.closed = tasks.Blocker('Window closed')
309 w.show_all()
310 w.connect('destroy', lambda wd: self.closed.trigger())
312 def response(wd, resp):
313 if self.child is not None:
314 self.note_error('Sending TERM signal to build process group %d...' % self.child.pid)
315 os.kill(-self.child.pid, signal.SIGTERM)
316 else:
317 self.closed.trigger()
318 w.connect('response', response)
320 def heading(self, msg):
321 self.overall.insert_at_end_and_scroll(msg + '\n', 'heading')
323 def note(self, msg):
324 self.overall.insert_at_end_and_scroll(msg + '\n')
326 def note_error(self, msg):
327 self.overall.insert_at_end_and_scroll(msg + '\n', 'error')
329 def build(self):
330 import gtk
331 try:
332 self.handler.wait_for_blocker(self.recursive_build(self.iface_uri))
333 except SafeException, ex:
334 self.note_error(str(ex))
335 else:
336 self.heading('All builds completed successfully!')
337 self.dialog.set_response_sensitive(gtk.RESPONSE_CANCEL, False)
338 self.dialog.set_response_sensitive(gtk.RESPONSE_OK, True)
340 self.handler.wait_for_blocker(self.closed)
342 @tasks.async
343 def spawn_build(self, iface_name):
344 assert self.child is None
346 self.details.insert_at_end_and_scroll('Building %s\n' % iface_name, 'heading')
348 # Group all the child processes so we can kill them easily
349 def become_group_leader():
350 os.setpgid(0, 0)
351 self.child = subprocess.Popen([sys.executable, sys.argv[0], 'build'],
352 stdout = subprocess.PIPE, stderr = subprocess.STDOUT,
353 preexec_fn = become_group_leader)
355 while True:
356 yield tasks.InputBlocker(self.child.stdout, 'output from child')
357 got = os.read(self.child.stdout.fileno(), 100)
358 if not got: break
359 self.details.insert_at_end_and_scroll(got)
361 self.child.wait()
362 code = self.child.returncode
363 self.child = None
364 if code:
365 self.details.insert_at_end_and_scroll('Build process exited with error status %d\n' % code, 'error')
366 raise SafeException('Build process exited with error status %d' % code)
367 self.details.insert_at_end_and_scroll('Build completed successfully\n', 'heading')
369 def do_autocompile(args):
370 """autocompile [--gui] URI"""
372 parser = OptionParser(usage="usage: %prog autocompile [options]")
374 parser.add_option('', "--gui", help="graphical interface", action='store_true')
375 (options, args2) = parser.parse_args(args)
376 if len(args2) != 1:
377 raise __main__.UsageError()
379 iface_uri = model.canonical_iface_uri(args2[0])
380 if options.gui:
381 compiler = GTKAutoCompiler(iface_uri)
382 else:
383 compiler = AutoCompiler(iface_uri)
385 compiler.build()
387 __main__.commands += [do_autocompile]