The GUI's Publish button now works.
[0compile.git] / gui_support.py
blobb4dbe64ce55e2c7dcf7e13400b6cb5798a1743f0
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 gtk.Dialog.__init__(self, _("Compile '%s'") % interface.rsplit('/', 1)[1])
25 self.set_has_separator(False)
26 self.set_default_size(gtk.gdk.screen_width() / 2, gtk.gdk.screen_height() / 2)
28 def add_action(stock, name, resp):
29 if not hasattr(gtk, stock):
30 stock = 'STOCK_YES'
31 button = ButtonMixed(getattr(gtk, stock), name)
32 button.set_flags(gtk.CAN_DEFAULT)
33 self.add_action_widget(button, resp)
34 return button
36 add_action('STOCK_PROPERTIES', '_Setup', RESPONSE_SETUP)
37 add_action('STOCK_CONVERT', '_Build', RESPONSE_BUILD)
38 add_action('STOCK_ADD', '_Register', RESPONSE_REGISTER)
39 add_action('STOCK_NETWORK', '_Publish', RESPONSE_PUBLISH)
41 self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CANCEL)
43 self.set_default_response(RESPONSE_BUILD)
45 self.buffer = gtk.TextBuffer()
46 self.tv = gtk.TextView(self.buffer)
47 self.tv.set_wrap_mode(gtk.WRAP_WORD)
48 swin = gtk.ScrolledWindow()
49 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
50 swin.add(self.tv)
51 swin.set_shadow_type(gtk.SHADOW_IN)
52 self.vscroll = swin.get_vadjustment()
53 self.tv.set_editable(False)
54 self.tv.set_cursor_visible(False)
55 self.vbox.pack_start(swin, True, True, 0)
57 self.vbox.show_all()
59 self.connect('delete-event', lambda box, dev: True)
61 def response(box, resp):
62 if resp == RESPONSE_SETUP:
63 import setup
64 def done_setup():
65 buildenv = BuildEnv()
66 to_delete = [x for x in ['build', buildenv.distdir] if os.path.isdir(x)]
67 if not to_delete:
68 pass
69 elif confirm(self, ("After changing source versions, it's a "
70 "good idea to delete the existing build files (%s). Delete existing directories?") %
71 ' and '.join(['"%s"' % d for d in to_delete]), gtk.STOCK_CLEAR):
72 def done_clean():
73 self.add_msg('Now use Build to compile the chosen source code.')
74 self.run_command([find_in_path('rm'), '-rf'] + to_delete, done_clean)
75 return
76 else:
77 self.add_msg('Not cleaning up existing build files.')
78 self.add_msg('Now use Build to compile the chosen source code.')
79 self.run_command((sys.executable, main_path, 'setup'), done_setup)
80 elif resp == RESPONSE_BUILD:
81 def done_build():
82 self.add_msg('\nBuild successful. Now register or publish the build.')
83 box.run_command((sys.executable, main_path, 'build'), done_build)
84 elif resp == RESPONSE_REGISTER:
85 buildenv = BuildEnv()
87 iface = iface_cache.get_interface(interface)
88 reader.update_from_cache(iface)
89 feed = buildenv.local_iface_file
90 for f in iface.feeds or []:
91 if f.uri == feed:
92 self.add_msg("Feed '%s' is already registered for interface '%s'!\n" % (feed, iface.uri))
93 return
94 box.buffer.insert_at_cursor("Registering feed '%s'\n" % feed)
95 iface.feeds.append(model.Feed(feed, arch = None, user_override = True))
96 writer.save_interface(iface)
97 box.buffer.insert_at_cursor("You can now close this window.\n")
98 elif resp == RESPONSE_PUBLISH:
99 buildenv = BuildEnv()
100 box = PublishBox(self, buildenv)
101 resp = box.run()
102 box.destroy()
103 if resp == gtk.RESPONSE_OK:
104 def done_publish():
105 self.add_msg("\nYou can copy-and-paste the <group> from this file "
106 "into the main feed. If you don't have a main feed then see "
107 "http://0install.net/injector-packagers.html for instructions on "
108 "creating one "
109 "(basically, you need to create a GPG key and then sign the XML file).")
110 self.run_command((sys.executable, main_path,
111 'publish', box.archive_dir.get_text()), done_publish)
112 elif resp == gtk.RESPONSE_CANCEL or resp == gtk.RESPONSE_DELETE_EVENT:
113 if self.kill_child(): return
114 self.destroy()
115 else:
116 self.add_msg('Unknown response: %s' % resp)
118 self.connect('response', response)
120 self.system_tag = self.buffer.create_tag('system', foreground = 'blue', background = 'white')
121 self.add_msg(instructions)
122 self.set_responses_sensitive()
124 def kill_child(self):
125 if self.child is None: return False
127 import signal
128 self.killed = True
129 self.add_msg('\nSending SIGTERM to process...')
130 os.kill(-self.child, signal.SIGTERM)
131 return True
133 def add_msg(self, msg):
134 self.insert_at_end_and_scroll(msg + '\n', self.system_tag)
136 def run_command(self, command, success):
137 assert self.child is None
138 self.killed = False
139 self.success = success
140 if isinstance(command, basestring):
141 self.add_msg("Running: " + command + "\n")
142 else:
143 self.add_msg("Running: " + ' '.join(command) + "\n")
145 r, w = os.pipe()
146 try:
147 try:
148 self.child = os.fork()
149 if not self.child:
150 # We are the child
151 try:
152 try:
153 os.close(r)
154 os.dup2(w, 1)
155 os.dup2(w, 2)
156 os.close(w)
157 os.setpgrp() # Become group leader
158 os.execvp(command[0], command)
159 except:
160 import traceback
161 traceback.print_exc()
162 finally:
163 os._exit(1)
164 finally:
165 os.close(w)
166 except:
167 os.close(r)
168 raise
170 for resp in action_responses:
171 self.set_response_sensitive(resp, False)
173 # We are the parent
174 gobject.io_add_watch(r, gobject.IO_IN | gobject.IO_HUP, self.got_data)
176 def set_responses_sensitive(self):
177 self.set_response_sensitive(RESPONSE_SETUP, True)
178 self.set_response_sensitive(RESPONSE_BUILD, True)
180 buildenv = BuildEnv()
181 have_binary = os.path.exists(buildenv.local_iface_file)
182 self.set_response_sensitive(RESPONSE_REGISTER, have_binary)
183 self.set_response_sensitive(RESPONSE_PUBLISH, have_binary)
185 def insert_at_end_and_scroll(self, data, *tags):
186 near_end = self.vscroll.upper - self.vscroll.page_size * 1.5 < self.vscroll.value
187 end = self.buffer.get_end_iter()
188 self.buffer.insert_with_tags(end, data, *tags)
189 if near_end:
190 cursor = self.buffer.get_insert()
191 self.buffer.move_mark(cursor, end)
192 self.tv.scroll_to_mark(cursor, 0, False, 0, 0)
194 def got_data(self, src, cond):
195 data = os.read(src, 100)
196 if data:
197 # TODO: only insert complete UTF-8 sequences, not half sequences
198 self.insert_at_end_and_scroll(data)
199 return True
200 else:
201 pid, status = os.waitpid(self.child, 0)
202 assert pid == self.child
203 self.child = None
205 self.set_responses_sensitive()
207 if os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0:
208 self.success()
209 elif self.killed:
210 self.add_msg("\nCommand terminated at user's request.")
211 else:
212 self.add_msg("\nCommand failed.")
213 return False
216 def choose_dir(title, default):
217 sel = gtk.FileSelection(title)
218 sel.set_has_separator(False)
219 sel.set_filename(default)
220 while True:
221 resp = sel.run()
222 if resp == gtk.RESPONSE_OK:
223 build_dir = sel.get_filename()
224 if not os.path.exists(build_dir):
225 sel.destroy()
226 return build_dir
227 alert(sel, _("'%s' already exists") % build_dir)
228 else:
229 sel.destroy()
230 return None
232 def alert(parent, msg):
233 d = gtk.MessageDialog(parent,
234 gtk.DIALOG_MODAL,
235 gtk.MESSAGE_ERROR,
236 gtk.BUTTONS_OK,
237 msg)
238 d.run()
239 d.destroy()
241 class ButtonMixed(gtk.Button):
242 """A button with a standard stock icon, but any label. This is useful
243 when you want to express a concept similar to one of the stock ones."""
244 def __init__(self, stock, message):
245 """Specify the icon and text for the new button. The text
246 may specify the mnemonic for the widget by putting a _ before
247 the letter, eg:
248 button = ButtonMixed(gtk.STOCK_DELETE, '_Delete message')."""
249 gtk.Button.__init__(self)
251 label = gtk.Label('')
252 label.set_text_with_mnemonic(message)
253 label.set_mnemonic_widget(self)
255 image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_BUTTON)
256 box = gtk.HBox(False, 2)
257 align = gtk.Alignment(0.5, 0.5, 0.0, 0.0)
259 box.pack_start(image, False, False, 0)
260 box.pack_end(label, False, False, 0)
262 self.add(align)
263 align.add(box)
264 align.show_all()
266 def confirm(parent, message, stock_icon, action = None):
267 """Display a <Cancel>/<Action> dialog. Result is true if the user
268 chooses the action, false otherwise. If action is given then that
269 is used as the text instead of the default for the stock item. Eg:
270 if rox.confirm('Really delete everything?', gtk.STOCK_DELETE): delete()
272 box = gtk.MessageDialog(parent, 0, gtk.MESSAGE_QUESTION,
273 gtk.BUTTONS_CANCEL, message)
274 if action:
275 button = ButtonMixed(stock_icon, action)
276 else:
277 button = gtk.Button(stock = stock_icon)
278 button.set_flags(gtk.CAN_DEFAULT)
279 button.show()
280 box.add_action_widget(button, gtk.RESPONSE_OK)
281 box.set_position(gtk.WIN_POS_CENTER)
282 box.set_title(_('Confirm:'))
283 box.set_default_response(gtk.RESPONSE_OK)
284 resp = box.run()
285 box.destroy()
286 return resp == int(gtk.RESPONSE_OK)
288 class PublishBox(gtk.MessageDialog):
289 def __init__(self, parent, buildenv):
290 gtk.MessageDialog.__init__(self, parent,
291 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
292 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL,
293 'Enter the directory on your HTTP or FTP server to which '
294 'the archive file will be uploaded:')
295 vbox = gtk.VBox(True, 4)
296 self.vbox.pack_start(vbox, False, True, 0)
297 vbox.set_border_width(8)
299 self.archive_dir = gtk.Entry()
300 self.archive_dir.set_activates_default(True)
301 self.set_default_response(gtk.RESPONSE_OK)
303 if buildenv.download_base_url:
304 self.archive_dir.set_text(buildenv.download_base_url)
305 else:
306 self.archive_dir.set_text('http://myserver.com/archives')
308 vbox.pack_start(self.archive_dir, False, True, 0)
309 vbox.show_all()
311 instructions = """Instructions
313 Compiling a program takes the program's human readable source code, and generates a binary which a computer can run.
315 To choose a different version of the source code, or the versions of any other programs needed to compile, use Setup.
317 To compile the chosen source code into a binary, use Build.
319 To add the new binary to the list of available versions for the program, use Register.
321 To publish the binary on the web, so that other people can run it, use Publish.
323 For further information, including details of how to make changes to the source code before compiling, visit: http://0install.net/0compile.html