Cleanup: changed os.system to subprocess.call
[0compile.git] / gui_support.py
blob46b322587f5d66c0fa16ca018179c3bb0e622cba
1 # Copyright (C) 2006, Thomas Leonard
2 # See http://0install.net/0compile.html
4 import sys, os, __main__
5 import pygtk; pygtk.require('2.0')
6 import gtk, gobject
8 from zeroinstall.injector import reader, writer, model
9 from zeroinstall.injector.iface_cache import iface_cache
11 from support import BuildEnv, _
13 RESPONSE_SETUP = 1
14 RESPONSE_BUILD = 2
15 RESPONSE_PUBLISH = 3
16 RESPONSE_REGISTER = 4
18 action_responses = [RESPONSE_SETUP, RESPONSE_BUILD, RESPONSE_PUBLISH, RESPONSE_REGISTER]
20 main_path = os.path.abspath(__main__.__file__)
22 class CompileBox(gtk.Dialog):
23 child = None
25 def __init__(self, interface):
26 assert interface
27 gtk.Dialog.__init__(self, _("Compile '%s'") % interface.split('/')[-1]) # No rsplit on Python 2.3
28 self.set_has_separator(False)
29 self.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
31 def add_action(stock, name, resp):
32 if not hasattr(gtk, stock):
33 stock = 'STOCK_YES'
34 button = ButtonMixed(getattr(gtk, stock), name)
35 button.set_flags(gtk.CAN_DEFAULT)
36 self.add_action_widget(button, resp)
37 return button
39 add_action('STOCK_PROPERTIES', '_Setup', RESPONSE_SETUP)
40 add_action('STOCK_CONVERT', '_Build', RESPONSE_BUILD)
41 add_action('STOCK_ADD', '_Register', RESPONSE_REGISTER)
42 add_action('STOCK_NETWORK', '_Publish', RESPONSE_PUBLISH)
44 self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CANCEL)
46 self.set_default_response(RESPONSE_BUILD)
48 self.buffer = gtk.TextBuffer()
49 self.tv = gtk.TextView(self.buffer)
50 self.tv.set_left_margin(4)
51 self.tv.set_right_margin(4)
52 self.tv.set_wrap_mode(gtk.WRAP_WORD)
53 swin = gtk.ScrolledWindow()
54 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
55 swin.add(self.tv)
56 swin.set_shadow_type(gtk.SHADOW_IN)
57 self.vscroll = swin.get_vadjustment()
58 self.tv.set_editable(False)
59 self.tv.set_cursor_visible(False)
60 self.vbox.pack_start(swin, True, True, 0)
62 self.vbox.show_all()
64 self.connect('delete-event', lambda box, dev: True)
66 def response(box, resp):
67 if resp == RESPONSE_SETUP:
68 def done_setup():
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 os.path.isabs(iface.uri):
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 self.add_msg("Running: " + ' '.join(command) + "\n")
178 r, w = os.pipe()
179 try:
180 try:
181 self.child = os.fork()
182 if not self.child:
183 # We are the child
184 try:
185 try:
186 os.close(r)
187 os.dup2(w, 1)
188 os.dup2(w, 2)
189 os.close(w)
190 os.setpgrp() # Become group leader
191 os.execvp(command[0], command)
192 except:
193 import traceback
194 traceback.print_exc()
195 finally:
196 os._exit(1)
197 finally:
198 os.close(w)
199 except:
200 os.close(r)
201 raise
203 for resp in action_responses:
204 self.set_response_sensitive(resp, False)
206 # We are the parent
207 gobject.io_add_watch(r, gobject.IO_IN | gobject.IO_HUP, self.got_data)
209 def set_responses_sensitive(self):
210 self.set_response_sensitive(RESPONSE_SETUP, True)
211 self.set_response_sensitive(RESPONSE_BUILD, True)
213 buildenv = BuildEnv()
214 have_binary = os.path.exists(buildenv.local_iface_file)
215 self.set_response_sensitive(RESPONSE_REGISTER, have_binary)
216 self.set_response_sensitive(RESPONSE_PUBLISH, have_binary)
218 def insert_at_end_and_scroll(self, data, *tags):
219 near_end = self.vscroll.upper - self.vscroll.page_size * 1.5 < self.vscroll.value
220 end = self.buffer.get_end_iter()
221 self.buffer.insert_with_tags(end, data, *tags)
222 if near_end:
223 cursor = self.buffer.get_insert()
224 self.buffer.move_mark(cursor, end)
225 self.tv.scroll_to_mark(cursor, 0, False, 0, 0)
227 def got_data(self, src, cond):
228 data = os.read(src, 100)
229 if data:
230 # TODO: only insert complete UTF-8 sequences, not half sequences
231 self.insert_at_end_and_scroll(data)
232 return True
233 else:
234 pid, status = os.waitpid(self.child, 0)
235 assert pid == self.child
236 self.child = None
238 self.set_responses_sensitive()
240 if os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0:
241 self.success()
242 elif self.killed:
243 self.add_msg("\nCommand terminated at user's request.")
244 else:
245 self.add_msg("\nCommand failed.")
246 if self.failure:
247 self.failure()
248 return False
251 def choose_dir(title, default):
252 sel = gtk.FileSelection(title)
253 sel.set_has_separator(False)
254 sel.set_filename(default)
255 while True:
256 resp = sel.run()
257 if resp == gtk.RESPONSE_OK:
258 build_dir = sel.get_filename()
259 if not os.path.exists(build_dir):
260 sel.destroy()
261 return build_dir
262 alert(sel, _("'%s' already exists") % build_dir)
263 else:
264 sel.destroy()
265 return None
267 def alert(parent, msg):
268 d = gtk.MessageDialog(parent,
269 gtk.DIALOG_MODAL,
270 gtk.MESSAGE_ERROR,
271 gtk.BUTTONS_OK,
272 msg)
273 d.run()
274 d.destroy()
276 class ButtonMixed(gtk.Button):
277 """A button with a standard stock icon, but any label. This is useful
278 when you want to express a concept similar to one of the stock ones."""
279 def __init__(self, stock, message):
280 """Specify the icon and text for the new button. The text
281 may specify the mnemonic for the widget by putting a _ before
282 the letter, eg:
283 button = ButtonMixed(gtk.STOCK_DELETE, '_Delete message')."""
284 gtk.Button.__init__(self)
286 label = gtk.Label('')
287 label.set_text_with_mnemonic(message)
288 label.set_mnemonic_widget(self)
290 image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_BUTTON)
291 box = gtk.HBox(False, 2)
292 align = gtk.Alignment(0.5, 0.5, 0.0, 0.0)
294 box.pack_start(image, False, False, 0)
295 box.pack_end(label, False, False, 0)
297 self.add(align)
298 align.add(box)
299 align.show_all()
301 def get_build_options(parent, message):
302 box = gtk.MessageDialog(parent, 0, gtk.MESSAGE_QUESTION,
303 gtk.BUTTONS_CANCEL, message)
305 button = ButtonMixed(gtk.STOCK_GO_FORWARD, 'Force')
306 button.show()
307 box.add_action_widget(button, 2)
309 button = ButtonMixed(gtk.STOCK_CLEAR, 'Clean')
310 button.set_flags(gtk.CAN_DEFAULT)
311 button.show()
312 box.add_action_widget(button, 1)
314 box.set_position(gtk.WIN_POS_CENTER)
315 box.set_title(_('Confirm:'))
316 box.set_default_response(1)
317 resp = box.run()
318 box.destroy()
320 if resp == 1:
321 return ['--clean']
322 elif resp == 2:
323 return ['--force']
324 return None
326 class PublishBox(gtk.MessageDialog):
327 def __init__(self, parent, buildenv):
328 gtk.MessageDialog.__init__(self, parent,
329 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
330 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL,
331 'Enter the directory on your HTTP or FTP server to which '
332 'the archive file will be uploaded:')
333 vbox = gtk.VBox(True, 4)
334 self.vbox.pack_start(vbox, False, True, 0)
335 vbox.set_border_width(8)
337 self.archive_dir = gtk.Entry()
338 self.archive_dir.set_activates_default(True)
339 self.set_default_response(gtk.RESPONSE_OK)
341 if buildenv.download_base_url:
342 self.archive_dir.set_text(buildenv.download_base_url)
343 else:
344 self.archive_dir.set_text('http://myserver.com/archives')
346 vbox.pack_start(self.archive_dir, False, True, 0)
347 vbox.show_all()
349 instructions = """Instructions
351 Compiling a program takes the program's human readable source code, and generates a binary which a computer can run.
353 To choose a different version of the source code, or the versions of any other programs needed to compile, use Setup.
355 To compile the chosen source code into a binary, use Build.
357 To add the new binary to the list of available versions for the program, use Register.
359 To publish the binary on the web, so that other people can run it, use Publish.
361 For further information, including details of how to make changes to the source code before compiling, visit: http://0install.net/0compile.html