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
, ro_rmtree
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 DummyDownloadSource(model
.RetrievalMethod
):
27 """0launch >= 0.46 won't select implementations without a download source."""
30 class AutocompileCache(iface_cache
.IfaceCache
):
32 iface_cache
.IfaceCache
.__init
__(self
)
35 def get_feed(self
, url
, force
= False):
36 feed
= iface_cache
.IfaceCache
.get_feed(self
, url
, force
)
37 if not feed
: return None
39 if feed
not in self
.done
:
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')]
48 new_id
= '0compile=' + x
.id
49 if not new_id
in feed
.implementations
:
50 new
= model
.ZeroInstallImplementation(feed
, new_id
, None)
51 new
.download_sources
.append(DummyDownloadSource())
52 feed
.implementations
[new_id
] = new
53 new
.digests
.append(new_id
)
54 new
.set_arch(host_arch
)
55 new
.version
= x
.version
59 policy
.iface_cache
= AutocompileCache()
62 def __init__(self
, iface_uri
, options
, handler
):
63 self
.iface_uri
= iface_uri
64 self
.options
= options
65 self
.handler
= handler
67 def pretty_print_plan(self
, solver
, root
, indent
= '- '):
68 """Display a tree showing the selected implementations."""
69 iface
= solver
.iface_cache
.get_interface(root
)
70 impl
= solver
.selections
[iface
]
72 msg
= 'Failed to select any suitable version (source or binary)'
73 elif impl
.id.startswith('0compile='):
74 real_impl_id
= impl
.id.split('=', 1)[1]
75 real_impl
= impl
.feed
.implementations
[real_impl_id
]
76 msg
= 'Compile %s (%s)' % (real_impl
.get_version(), real_impl
.id)
77 elif impl
.arch
and impl
.arch
.endswith('-src'):
78 msg
= 'Compile %s (%s)' % (impl
.get_version(), impl
.id)
81 msg
= 'Use existing binary %s (%s)' % (impl
.get_version(), impl
.arch
)
83 msg
= 'Use existing architecture-independent package %s' % impl
.get_version()
84 self
.note("%s%s: %s" % (indent
, iface
.get_name(), msg
))
88 for x
in impl
.requires
:
89 self
.pretty_print_plan(solver
, x
.interface
, indent
)
91 def print_details(self
, solver
):
92 """Dump debugging details."""
93 self
.note("\nFailed. Details of all components and versions considered:")
94 for iface
in solver
.details
:
95 self
.note('\n%s\n' % iface
.get_name())
96 for impl
, note
in solver
.details
[iface
]:
97 self
.note('%s (%s) : %s' % (impl
.get_version(), impl
.arch
or '*-*', note
or 'OK'))
98 self
.note("\nEnd details\n")
101 def compile_and_register(self
, policy
):
102 local_feed_dir
= basedir
.save_config_path('0install.net', '0compile', 'builds', model
._pretty
_escape
(policy
.root
))
103 s
= selections
.Selections(policy
)
105 buildenv
= BuildEnv(need_config
= False)
106 buildenv
.config
.set('compile', 'interface', policy
.root
)
107 buildenv
.config
.set('compile', 'selections', 'selections.xml')
109 version
= s
.selections
[policy
.root
].version
110 local_feed
= os
.path
.join(local_feed_dir
, '%s-%s-%s.xml' % (buildenv
.iface_name
, version
, arch
._uname
[-1]))
111 if os
.path
.exists(local_feed
):
112 raise SafeException("Build metadata file '%s' already exists!" % local_feed
)
114 tmpdir
= tempfile
.mkdtemp(prefix
= '0compile-')
118 # Write configuration for build...
122 sel_file
= open('selections.xml', 'w')
125 doc
.writexml(sel_file
)
132 build
= self
.spawn_build(buildenv
.iface_name
)
137 # Register the result...
139 alg
= manifest
.get_algorithm('sha1new')
140 digest
= alg
.new_digest()
142 for line
in alg
.generate_manifest(buildenv
.distdir
):
146 actual_digest
= alg
.getID(digest
)
148 local_feed_file
= file(local_feed
, 'w')
150 dom
= minidom
.parse(buildenv
.local_iface_file
)
151 impl
, = dom
.getElementsByTagNameNS(namespaces
.XMLNS_IFACE
, 'implementation')
152 impl
.setAttribute('id', actual_digest
)
153 dom
.writexml(local_feed_file
)
154 local_feed_file
.write('\n')
156 local_feed_file
.close()
158 self
.note("Implementation metadata written to %s" % local_feed
)
160 # No point adding it to the system store when only the user has the feed...
161 store
= policy
.solver
.iface_cache
.stores
.stores
[0]
162 self
.note("Storing build in user cache %s..." % store
.dir)
163 policy
.solver
.iface_cache
.stores
.add_dir_to_cache(actual_digest
, buildenv
.distdir
)
165 self
.note("Registering feed...")
166 iface
= policy
.solver
.iface_cache
.get_interface(policy
.root
)
167 feed
= iface
.get_feed(local_feed
)
169 self
.note("WARNING: feed %s already registered!" % local_feed
)
171 iface
.extra_feeds
.append(model
.Feed(local_feed
, impl
.getAttribute('arch'), user_override
= True))
172 writer
.save_interface(iface
)
174 # We might have cached an old version
175 new_feed
= policy
.solver
.iface_cache
.get_interface(local_feed
)
176 reader
.update_from_cache(new_feed
)
178 self
.note("\nBuild failed: leaving build directory %s for inspection...\n" % tmpdir
)
184 def recursive_build(self
, iface_uri
, version
= None):
185 p
= policy
.Policy(iface_uri
, handler
= self
.handler
, src
= True)
186 iface
= p
.solver
.iface_cache
.get_interface(iface_uri
)
187 p
.solver
.record_details
= True
189 p
.solver
.extra_restrictions
[iface
] = [model
.VersionRestriction(model
.parse_version(version
))]
192 #p.target_arch = arch.Architecture(os_ranks = {'FreeBSD': 0, None: 1}, machine_ranks = {'i386': 0, None: 1, 'newbuild': 2})
195 self
.heading(iface_uri
)
196 self
.note("\nSelecting versions for %s..." % iface
.get_name())
197 solved
= p
.solve_with_downloads()
202 if not p
.solver
.ready
:
203 self
.print_details(p
.solver
)
204 raise SafeException("Can't find all required implementations (source or binary):\n" +
205 '\n'.join(["- %s -> %s" % (iface
, p
.solver
.selections
[iface
])
206 for iface
in p
.solver
.selections
]))
207 self
.note("Selection done.")
209 self
.note("\nPlan:\n")
210 self
.pretty_print_plan(p
.solver
, p
.root
)
213 for dep_iface
, dep_impl
in p
.solver
.selections
.iteritems():
214 if dep_impl
.id.startswith('0compile='):
215 build
= self
.recursive_build(dep_iface
.uri
, dep_impl
.get_version())
218 break # Try again with that dependency built...
220 self
.note("No dependencies need compiling... compile %s itself..." % iface
.get_name())
221 build
= self
.compile_and_register(p
)
226 def spawn_build(self
, iface_name
):
227 subprocess
.check_call([sys
.executable
, sys
.argv
[0], 'build'])
230 self
.handler
.wait_for_blocker(self
.recursive_build(self
.iface_uri
))
232 def heading(self
, msg
):
233 self
.note((' %s ' % msg
).center(76, '='))
238 def note_error(self
, msg
):
239 self
.overall
.insert_at_cursor(msg
+ '\n')
241 class GUIHandler(handler
.Handler
):
242 def downloads_changed(self
):
243 self
.compiler
.downloads_changed()
245 def confirm_import_feed(self
, pending
, valid_sigs
):
246 return handler
.Handler
.confirm_import_feed(self
, pending
, valid_sigs
)
248 class GTKAutoCompiler(AutoCompiler
):
249 def __init__(self
, iface_uri
, options
):
250 handler
= GUIHandler()
251 handler
.compiler
= self
253 AutoCompiler
.__init
__(self
, iface_uri
, options
, handler
)
256 import pygtk
; pygtk
.require('2.0')
259 w
= gtk
.Dialog('Autocompile %s' % iface_uri
, None, 0,
260 (gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
,
261 gtk
.STOCK_OK
, gtk
.RESPONSE_OK
))
264 w
.set_default_size(int(gtk
.gdk
.screen_width() * 0.8),
265 int(gtk
.gdk
.screen_height() * 0.8))
267 vpaned
= gtk
.VPaned()
269 w
.set_response_sensitive(gtk
.RESPONSE_OK
, False)
274 tv
.set_property('left-margin', 8)
275 tv
.set_wrap_mode(gtk
.WRAP_WORD_CHAR
)
276 tv
.set_editable(False)
277 swin
= gtk
.ScrolledWindow()
279 swin
.set_policy(gtk
.POLICY_NEVER
, gtk
.POLICY_AUTOMATIC
)
280 buffer = tv
.get_buffer()
282 heading
= buffer.create_tag('heading')
283 heading
.set_property('scale', 1.5)
285 error
= buffer.create_tag('error')
286 error
.set_property('background', 'white')
287 error
.set_property('foreground', 'red')
293 def insert_at_end_and_scroll(self
, data
, *tags
):
294 vscroll
= self
.widget
.get_vadjustment()
296 # Widget has been destroyed
299 near_end
= vscroll
.upper
- vscroll
.page_size
* 1.5 < vscroll
.value
300 end
= self
.buffer.get_end_iter()
301 self
.buffer.insert_with_tags_by_name(end
, data
, *tags
)
303 cursor
= self
.buffer.get_insert()
304 self
.buffer.move_mark(cursor
, end
)
305 self
.tv
.scroll_to_mark(cursor
, 0, False, 0, 0)
307 self
.overall
= AutoScroller()
308 self
.details
= AutoScroller()
310 vpaned
.pack1(self
.overall
.widget
, True, False)
311 vpaned
.pack2(self
.details
.widget
, True, False)
313 self
.closed
= tasks
.Blocker('Window closed')
316 w
.connect('destroy', lambda wd
: self
.closed
.trigger())
318 def response(wd
, resp
):
319 if self
.child
is not None:
320 self
.note_error('Sending TERM signal to build process group %d...' % self
.child
.pid
)
321 os
.kill(-self
.child
.pid
, signal
.SIGTERM
)
323 self
.closed
.trigger()
324 w
.connect('response', response
)
326 def downloads_changed(self
):
327 if self
.handler
.monitored_downloads
:
328 self
.note('Downloads in progress:')
329 for x
in self
.handler
.monitored_downloads
:
330 self
.note('- %s' % x
)
332 self
.note('No downloads remaining.')
334 def heading(self
, msg
):
335 self
.overall
.insert_at_end_and_scroll(msg
+ '\n', 'heading')
338 self
.overall
.insert_at_end_and_scroll(msg
+ '\n')
340 def note_error(self
, msg
):
341 self
.overall
.insert_at_end_and_scroll(msg
+ '\n', 'error')
346 self
.handler
.wait_for_blocker(self
.recursive_build(self
.iface_uri
))
347 except SafeException
, ex
:
348 self
.note_error(str(ex
))
350 self
.heading('All builds completed successfully!')
351 self
.dialog
.set_response_sensitive(gtk
.RESPONSE_CANCEL
, False)
352 self
.dialog
.set_response_sensitive(gtk
.RESPONSE_OK
, True)
354 self
.handler
.wait_for_blocker(self
.closed
)
357 def spawn_build(self
, iface_name
):
358 assert self
.child
is None
360 self
.details
.insert_at_end_and_scroll('Building %s\n' % iface_name
, 'heading')
362 # Group all the child processes so we can kill them easily
363 def become_group_leader():
365 self
.child
= subprocess
.Popen([sys
.executable
, '-u', sys
.argv
[0], 'build'],
366 stdout
= subprocess
.PIPE
, stderr
= subprocess
.STDOUT
,
367 preexec_fn
= become_group_leader
)
370 decoder
= codecs
.getincrementaldecoder('utf-8')(errors
= 'replace')
373 yield tasks
.InputBlocker(self
.child
.stdout
, 'output from child')
374 got
= os
.read(self
.child
.stdout
.fileno(), 100)
375 chars
= decoder
.decode(got
, final
= not got
)
376 self
.details
.insert_at_end_and_scroll(chars
)
380 code
= self
.child
.returncode
383 self
.details
.insert_at_end_and_scroll('Build process exited with error status %d\n' % code
, 'error')
384 raise SafeException('Build process exited with error status %d' % code
)
385 self
.details
.insert_at_end_and_scroll('Build completed successfully\n', 'heading')
388 def confirm_import_feed(self
, pending
, valid_sigs
):
389 from zeroinstall
.gtkui
import trust_box
390 box
= trust_box
.TrustBox(pending
, valid_sigs
, parent
= self
.dialog
)
394 def do_autocompile(args
):
395 """autocompile [--gui] URI"""
397 parser
= OptionParser(usage
="usage: %prog autocompile [options]")
399 parser
.add_option('', "--gui", help="graphical interface", action
='store_true')
400 (options
, args2
) = parser
.parse_args(args
)
402 raise __main__
.UsageError()
404 iface_uri
= model
.canonical_iface_uri(args2
[0])
406 compiler
= GTKAutoCompiler(iface_uri
, options
)
408 h
= handler
.ConsoleHandler()
409 compiler
= AutoCompiler(iface_uri
, options
, h
)
413 __main__
.commands
+= [do_autocompile
]