Release 0.18
[0compile.git] / gui_support.py
blob39bf4626d6247af66bded58698e8e754a1a233e4
1 # Copyright (C) 2006, Thomas Leonard
2 # See http://0install.net/0compile.html
4 import sys, os, __main__, popen2
5 import pygtk; pygtk.require('2.0')
6 import gtk, gobject
7 from zeroinstall.injector import reader, writer
9 from support import *
11 RESPONSE_SETUP = 1
12 RESPONSE_BUILD = 2
13 RESPONSE_PUBLISH = 3
14 RESPONSE_REGISTER = 4
16 action_responses = [RESPONSE_SETUP, RESPONSE_BUILD, RESPONSE_PUBLISH, RESPONSE_REGISTER]
18 main_path = os.path.abspath(__main__.__file__)
20 class CompileBox(gtk.Dialog):
21 child = None
23 def __init__(self, interface):
24 assert interface
25 gtk.Dialog.__init__(self, _("Compile '%s'") % interface.split('/')[-1]) # No rsplit on Python 2.3
26 self.set_has_separator(False)
27 self.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
29 def add_action(stock, name, resp):
30 if not hasattr(gtk, stock):
31 stock = 'STOCK_YES'
32 button = ButtonMixed(getattr(gtk, stock), name)
33 button.set_flags(gtk.CAN_DEFAULT)
34 self.add_action_widget(button, resp)
35 return button
37 add_action('STOCK_PROPERTIES', '_Setup', RESPONSE_SETUP)
38 add_action('STOCK_CONVERT', '_Build', RESPONSE_BUILD)
39 add_action('STOCK_ADD', '_Register', RESPONSE_REGISTER)
40 add_action('STOCK_NETWORK', '_Publish', RESPONSE_PUBLISH)
42 self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CANCEL)
44 self.set_default_response(RESPONSE_BUILD)
46 self.buffer = gtk.TextBuffer()
47 self.tv = gtk.TextView(self.buffer)
48 self.tv.set_left_margin(4)
49 self.tv.set_right_margin(4)
50 self.tv.set_wrap_mode(gtk.WRAP_WORD)
51 swin = gtk.ScrolledWindow()
52 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
53 swin.add(self.tv)
54 swin.set_shadow_type(gtk.SHADOW_IN)
55 self.vscroll = swin.get_vadjustment()
56 self.tv.set_editable(False)
57 self.tv.set_cursor_visible(False)
58 self.vbox.pack_start(swin, True, True, 0)
60 self.vbox.show_all()
62 self.connect('delete-event', lambda box, dev: True)
64 def response(box, resp):
65 if resp == RESPONSE_SETUP:
66 import setup
67 def done_setup():
68 buildenv = BuildEnv()
69 self.add_msg('Now use Build to compile the chosen source code.')
70 self.run_command((sys.executable, main_path, 'setup'), done_setup)
71 elif resp == RESPONSE_BUILD:
72 def done_build():
73 self.add_msg('\nBuild successful. Now register or publish the build.')
74 def build_failed():
75 self.add_msg('\nIf the messages displayed above indicate a missing dependency (e.g. no C compiler '
76 "or a library that isn't available through Zero Install) then install it using your "
77 'normal package manager and click on Build again. Note that for libraries you often '
78 'need the -dev version of the package. '
79 '\nOtherwise, please notify the developers of this problem (this will transmit '
80 'the contents of the build/build-failure.log file):')
81 end = self.buffer.get_end_iter()
82 anchor = self.buffer.create_child_anchor(end)
83 align = gtk.Alignment(0.0, 0.0, 1.0, 1.0)
84 button = ButtonMixed(gtk.STOCK_YES, 'Notify developers')
85 align.add(button)
86 align.set_padding(8, 8, 8, 8)
87 align.show_all()
88 self.tv.add_child_at_anchor(align, anchor)
89 self.add_msg('\n')
90 def report_bug(button):
91 def done_notify():
92 self.add_msg("\nReport sent. Thank you! (note: you won't get a reply, as "
93 "no contact details were sent; write to the project's mailing "
94 "list if you want to discuss the problem)")
95 self.run_command((sys.executable, main_path, 'report-bug'), done_notify)
96 button.connect('clicked', report_bug)
97 buildenv = BuildEnv()
98 changes = buildenv.get_build_changes()
99 if changes:
100 options = get_build_options(box, '\n'.join(changes) + '\n\nIt would be best to do a clean (full) build.')
101 else:
102 options = []
103 if options is not None:
104 box.run_command([sys.executable, main_path, 'build'] + options, done_build, build_failed)
105 elif resp == RESPONSE_REGISTER:
106 buildenv = BuildEnv()
108 iface = iface_cache.get_interface(interface)
109 reader.update_from_cache(iface)
111 # Register using the feed-for, if available
112 real_iface = iface
113 for uri in iface.feed_for or []:
114 real_iface = iface_cache.get_interface(uri)
115 self.add_msg("Registering as a feed for %s" % real_iface.uri)
116 break
117 else:
118 if iface.uri.startswith('/'):
119 self.add_msg("Warning: no <feed-for> in local feed %s!" % iface.uri)
121 feed = buildenv.local_iface_file
122 for f in real_iface.feeds or []:
123 if f.uri == feed:
124 self.add_msg("Feed '%s' is already registered for interface '%s'!\n" % (feed, real_iface.uri))
125 return
126 box.buffer.insert_at_cursor("Registering feed '%s'\n" % feed)
127 real_iface.extra_feeds.append(model.Feed(feed, arch = None, user_override = True))
128 writer.save_interface(real_iface)
129 box.buffer.insert_at_cursor("Done. You can now close this window.\n")
130 elif resp == RESPONSE_PUBLISH:
131 buildenv = BuildEnv()
132 box = PublishBox(self, buildenv)
133 resp = box.run()
134 box.destroy()
135 if resp == gtk.RESPONSE_OK:
136 def done_publish():
137 self.add_msg("\nYou can use '0publish --local' to add this "
138 "into the main feed. If you don't have a main feed then this "
139 "will create one. See "
140 "http://0install.net/injector-packagers.html for more information.")
141 self.run_command((sys.executable, main_path,
142 'publish', box.archive_dir.get_text()), done_publish)
143 elif resp == gtk.RESPONSE_CANCEL or resp == gtk.RESPONSE_DELETE_EVENT:
144 if self.kill_child(): return
145 self.destroy()
146 else:
147 self.add_msg('Unknown response: %s' % resp)
149 self.connect('response', response)
151 self.system_tag = self.buffer.create_tag('system', foreground = 'blue', background = 'white')
152 self.add_msg(instructions)
153 self.set_responses_sensitive()
155 def kill_child(self):
156 if self.child is None: return False
158 import signal
159 self.killed = True
160 self.add_msg('\nSending SIGTERM to process...')
161 os.kill(-self.child, signal.SIGTERM)
162 return True
164 def add_msg(self, msg):
165 self.insert_at_end_and_scroll(msg + '\n', self.system_tag)
167 """Run command in a sub-process.
168 Calls success() if the command exits with status zero.
169 Calls failure() if it fails for other reasons.
170 (neither is called if the user aborts the command)"""
171 def run_command(self, command, success, failure = None):
172 assert self.child is None
173 self.killed = False
174 self.success = success
175 self.failure = failure
176 if isinstance(command, basestring):
177 self.add_msg("Running: " + command + "\n")
178 else:
179 self.add_msg("Running: " + ' '.join(command) + "\n")
181 r, w = os.pipe()
182 try:
183 try:
184 self.child = os.fork()
185 if not self.child:
186 # We are the child
187 try:
188 try:
189 os.close(r)
190 os.dup2(w, 1)
191 os.dup2(w, 2)
192 os.close(w)
193 os.setpgrp() # Become group leader
194 os.execvp(command[0], command)
195 except:
196 import traceback
197 traceback.print_exc()
198 finally:
199 os._exit(1)
200 finally:
201 os.close(w)
202 except:
203 os.close(r)
204 raise
206 for resp in action_responses:
207 self.set_response_sensitive(resp, False)
209 # We are the parent
210 gobject.io_add_watch(r, gobject.IO_IN | gobject.IO_HUP, self.got_data)
212 def set_responses_sensitive(self):
213 self.set_response_sensitive(RESPONSE_SETUP, True)
214 self.set_response_sensitive(RESPONSE_BUILD, True)
216 buildenv = BuildEnv()
217 have_binary = os.path.exists(buildenv.local_iface_file)
218 self.set_response_sensitive(RESPONSE_REGISTER, have_binary)
219 self.set_response_sensitive(RESPONSE_PUBLISH, have_binary)
221 def insert_at_end_and_scroll(self, data, *tags):
222 near_end = self.vscroll.upper - self.vscroll.page_size * 1.5 < self.vscroll.value
223 end = self.buffer.get_end_iter()
224 self.buffer.insert_with_tags(end, data, *tags)
225 if near_end:
226 cursor = self.buffer.get_insert()
227 self.buffer.move_mark(cursor, end)
228 self.tv.scroll_to_mark(cursor, 0, False, 0, 0)
230 def got_data(self, src, cond):
231 data = os.read(src, 100)
232 if data:
233 # TODO: only insert complete UTF-8 sequences, not half sequences
234 self.insert_at_end_and_scroll(data)
235 return True
236 else:
237 pid, status = os.waitpid(self.child, 0)
238 assert pid == self.child
239 self.child = None
241 self.set_responses_sensitive()
243 if os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0:
244 self.success()
245 elif self.killed:
246 self.add_msg("\nCommand terminated at user's request.")
247 else:
248 self.add_msg("\nCommand failed.")
249 if self.failure:
250 self.failure()
251 return False
254 def choose_dir(title, default):
255 sel = gtk.FileSelection(title)
256 sel.set_has_separator(False)
257 sel.set_filename(default)
258 while True:
259 resp = sel.run()
260 if resp == gtk.RESPONSE_OK:
261 build_dir = sel.get_filename()
262 if not os.path.exists(build_dir):
263 sel.destroy()
264 return build_dir
265 alert(sel, _("'%s' already exists") % build_dir)
266 else:
267 sel.destroy()
268 return None
270 def alert(parent, msg):
271 d = gtk.MessageDialog(parent,
272 gtk.DIALOG_MODAL,
273 gtk.MESSAGE_ERROR,
274 gtk.BUTTONS_OK,
275 msg)
276 d.run()
277 d.destroy()
279 class ButtonMixed(gtk.Button):
280 """A button with a standard stock icon, but any label. This is useful
281 when you want to express a concept similar to one of the stock ones."""
282 def __init__(self, stock, message):
283 """Specify the icon and text for the new button. The text
284 may specify the mnemonic for the widget by putting a _ before
285 the letter, eg:
286 button = ButtonMixed(gtk.STOCK_DELETE, '_Delete message')."""
287 gtk.Button.__init__(self)
289 label = gtk.Label('')
290 label.set_text_with_mnemonic(message)
291 label.set_mnemonic_widget(self)
293 image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_BUTTON)
294 box = gtk.HBox(False, 2)
295 align = gtk.Alignment(0.5, 0.5, 0.0, 0.0)
297 box.pack_start(image, False, False, 0)
298 box.pack_end(label, False, False, 0)
300 self.add(align)
301 align.add(box)
302 align.show_all()
304 def get_build_options(parent, message):
305 box = gtk.MessageDialog(parent, 0, gtk.MESSAGE_QUESTION,
306 gtk.BUTTONS_CANCEL, message)
308 button = ButtonMixed(gtk.STOCK_GO_FORWARD, 'Force')
309 button.show()
310 box.add_action_widget(button, 2)
312 button = ButtonMixed(gtk.STOCK_CLEAR, 'Clean')
313 button.set_flags(gtk.CAN_DEFAULT)
314 button.show()
315 box.add_action_widget(button, 1)
317 box.set_position(gtk.WIN_POS_CENTER)
318 box.set_title(_('Confirm:'))
319 box.set_default_response(1)
320 resp = box.run()
321 box.destroy()
323 if resp == 1:
324 return ['--clean']
325 elif resp == 2:
326 return ['--force']
327 return None
329 class PublishBox(gtk.MessageDialog):
330 def __init__(self, parent, buildenv):
331 gtk.MessageDialog.__init__(self, parent,
332 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
333 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL,
334 'Enter the directory on your HTTP or FTP server to which '
335 'the archive file will be uploaded:')
336 vbox = gtk.VBox(True, 4)
337 self.vbox.pack_start(vbox, False, True, 0)
338 vbox.set_border_width(8)
340 self.archive_dir = gtk.Entry()
341 self.archive_dir.set_activates_default(True)
342 self.set_default_response(gtk.RESPONSE_OK)
344 if buildenv.download_base_url:
345 self.archive_dir.set_text(buildenv.download_base_url)
346 else:
347 self.archive_dir.set_text('http://myserver.com/archives')
349 vbox.pack_start(self.archive_dir, False, True, 0)
350 vbox.show_all()
352 instructions = """Instructions
354 Compiling a program takes the program's human readable source code, and generates a binary which a computer can run.
356 To choose a different version of the source code, or the versions of any other programs needed to compile, use Setup.
358 To compile the chosen source code into a binary, use Build.
360 To add the new binary to the list of available versions for the program, use Register.
362 To publish the binary on the web, so that other people can run it, use Publish.
364 For further information, including details of how to make changes to the source code before compiling, visit: http://0install.net/0compile.html