Updated publishing instructions to use 0publish --local
[0compile.git] / gui_support.py
blobe674b3a44b22accd63c458642b77d1de3177aeb9
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.split('/')[-1]) # No rsplit on Python 2.3
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_left_margin(4)
48 self.tv.set_right_margin(4)
49 self.tv.set_wrap_mode(gtk.WRAP_WORD)
50 swin = gtk.ScrolledWindow()
51 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
52 swin.add(self.tv)
53 swin.set_shadow_type(gtk.SHADOW_IN)
54 self.vscroll = swin.get_vadjustment()
55 self.tv.set_editable(False)
56 self.tv.set_cursor_visible(False)
57 self.vbox.pack_start(swin, True, True, 0)
59 self.vbox.show_all()
61 self.connect('delete-event', lambda box, dev: True)
63 def response(box, resp):
64 if resp == RESPONSE_SETUP:
65 import setup
66 def done_setup():
67 buildenv = BuildEnv()
68 to_delete = [x for x in ['build', buildenv.distdir] if os.path.isdir(x)]
69 if not to_delete:
70 pass
71 elif confirm(self, ("After changing source versions, it's a "
72 "good idea to delete the existing build files (%s). Delete existing directories?") %
73 ' and '.join(['"%s"' % d for d in to_delete]), gtk.STOCK_CLEAR):
74 def done_clean():
75 self.add_msg('Now use Build to compile the chosen source code.')
76 self.run_command([find_in_path('rm'), '-rf'] + to_delete, done_clean)
77 return
78 else:
79 self.add_msg('Not cleaning up existing build files.')
80 self.add_msg('Now use Build to compile the chosen source code.')
81 self.run_command((sys.executable, main_path, 'setup'), done_setup)
82 elif resp == RESPONSE_BUILD:
83 def done_build():
84 self.add_msg('\nBuild successful. Now register or publish the build.')
85 def build_failed():
86 self.add_msg('\nIf the messages displayed above indicate a missing dependency (e.g. no C compiler '
87 "or a library that isn't available through Zero Install) then install it using your "
88 'normal package manager and click on Build again. Note that for libraries you often '
89 'need the -dev version of the package. '
90 '\nOtherwise, please notify the developers of this problem (this will transmit '
91 'the contents of the build/build-failure.log file):')
92 end = self.buffer.get_end_iter()
93 anchor = self.buffer.create_child_anchor(end)
94 align = gtk.Alignment(0.0, 0.0, 1.0, 1.0)
95 button = ButtonMixed(gtk.STOCK_YES, 'Notify developers')
96 align.add(button)
97 align.set_padding(8, 8, 8, 8)
98 align.show_all()
99 self.tv.add_child_at_anchor(align, anchor)
100 self.add_msg('\n')
101 def report_bug(button):
102 def done_notify():
103 self.add_msg("\nReport sent. Thank you! (note: you won't get a reply, as "
104 "no contact details were sent; write to the project's mailing "
105 "list if you want to discuss the problem)")
106 self.run_command((sys.executable, main_path, 'report-bug'), done_notify)
107 button.connect('clicked', report_bug)
108 box.run_command((sys.executable, main_path, 'build'), done_build, build_failed)
109 elif resp == RESPONSE_REGISTER:
110 buildenv = BuildEnv()
112 iface = iface_cache.get_interface(interface)
113 reader.update_from_cache(iface)
114 feed = buildenv.local_iface_file
115 for f in iface.feeds or []:
116 if f.uri == feed:
117 self.add_msg("Feed '%s' is already registered for interface '%s'!\n" % (feed, iface.uri))
118 return
119 box.buffer.insert_at_cursor("Registering feed '%s'\n" % feed)
120 iface.feeds.append(model.Feed(feed, arch = None, user_override = True))
121 writer.save_interface(iface)
122 box.buffer.insert_at_cursor("You can now close this window.\n")
123 elif resp == RESPONSE_PUBLISH:
124 buildenv = BuildEnv()
125 box = PublishBox(self, buildenv)
126 resp = box.run()
127 box.destroy()
128 if resp == gtk.RESPONSE_OK:
129 def done_publish():
130 self.add_msg("\nYou can use '0publish --local' to add this "
131 "into the main feed. If you don't have a main feed then this "
132 "will create one. See"
133 "http://0install.net/injector-packagers.html for more information.")
134 self.run_command((sys.executable, main_path,
135 'publish', box.archive_dir.get_text()), done_publish)
136 elif resp == gtk.RESPONSE_CANCEL or resp == gtk.RESPONSE_DELETE_EVENT:
137 if self.kill_child(): return
138 self.destroy()
139 else:
140 self.add_msg('Unknown response: %s' % resp)
142 self.connect('response', response)
144 self.system_tag = self.buffer.create_tag('system', foreground = 'blue', background = 'white')
145 self.add_msg(instructions)
146 self.set_responses_sensitive()
148 def kill_child(self):
149 if self.child is None: return False
151 import signal
152 self.killed = True
153 self.add_msg('\nSending SIGTERM to process...')
154 os.kill(-self.child, signal.SIGTERM)
155 return True
157 def add_msg(self, msg):
158 self.insert_at_end_and_scroll(msg + '\n', self.system_tag)
160 """Run command in a sub-process.
161 Calls success() if the command exits with status zero.
162 Calls failure() if it fails for other reasons.
163 (neither is called if the user aborts the command)"""
164 def run_command(self, command, success, failure = None):
165 assert self.child is None
166 self.killed = False
167 self.success = success
168 self.failure = failure
169 if isinstance(command, basestring):
170 self.add_msg("Running: " + command + "\n")
171 else:
172 self.add_msg("Running: " + ' '.join(command) + "\n")
174 r, w = os.pipe()
175 try:
176 try:
177 self.child = os.fork()
178 if not self.child:
179 # We are the child
180 try:
181 try:
182 os.close(r)
183 os.dup2(w, 1)
184 os.dup2(w, 2)
185 os.close(w)
186 os.setpgrp() # Become group leader
187 os.execvp(command[0], command)
188 except:
189 import traceback
190 traceback.print_exc()
191 finally:
192 os._exit(1)
193 finally:
194 os.close(w)
195 except:
196 os.close(r)
197 raise
199 for resp in action_responses:
200 self.set_response_sensitive(resp, False)
202 # We are the parent
203 gobject.io_add_watch(r, gobject.IO_IN | gobject.IO_HUP, self.got_data)
205 def set_responses_sensitive(self):
206 self.set_response_sensitive(RESPONSE_SETUP, True)
207 self.set_response_sensitive(RESPONSE_BUILD, True)
209 buildenv = BuildEnv()
210 have_binary = os.path.exists(buildenv.local_iface_file)
211 self.set_response_sensitive(RESPONSE_REGISTER, have_binary)
212 self.set_response_sensitive(RESPONSE_PUBLISH, have_binary)
214 def insert_at_end_and_scroll(self, data, *tags):
215 near_end = self.vscroll.upper - self.vscroll.page_size * 1.5 < self.vscroll.value
216 end = self.buffer.get_end_iter()
217 self.buffer.insert_with_tags(end, data, *tags)
218 if near_end:
219 cursor = self.buffer.get_insert()
220 self.buffer.move_mark(cursor, end)
221 self.tv.scroll_to_mark(cursor, 0, False, 0, 0)
223 def got_data(self, src, cond):
224 data = os.read(src, 100)
225 if data:
226 # TODO: only insert complete UTF-8 sequences, not half sequences
227 self.insert_at_end_and_scroll(data)
228 return True
229 else:
230 pid, status = os.waitpid(self.child, 0)
231 assert pid == self.child
232 self.child = None
234 self.set_responses_sensitive()
236 if os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0:
237 self.success()
238 elif self.killed:
239 self.add_msg("\nCommand terminated at user's request.")
240 else:
241 self.add_msg("\nCommand failed.")
242 if self.failure:
243 self.failure()
244 return False
247 def choose_dir(title, default):
248 sel = gtk.FileSelection(title)
249 sel.set_has_separator(False)
250 sel.set_filename(default)
251 while True:
252 resp = sel.run()
253 if resp == gtk.RESPONSE_OK:
254 build_dir = sel.get_filename()
255 if not os.path.exists(build_dir):
256 sel.destroy()
257 return build_dir
258 alert(sel, _("'%s' already exists") % build_dir)
259 else:
260 sel.destroy()
261 return None
263 def alert(parent, msg):
264 d = gtk.MessageDialog(parent,
265 gtk.DIALOG_MODAL,
266 gtk.MESSAGE_ERROR,
267 gtk.BUTTONS_OK,
268 msg)
269 d.run()
270 d.destroy()
272 class ButtonMixed(gtk.Button):
273 """A button with a standard stock icon, but any label. This is useful
274 when you want to express a concept similar to one of the stock ones."""
275 def __init__(self, stock, message):
276 """Specify the icon and text for the new button. The text
277 may specify the mnemonic for the widget by putting a _ before
278 the letter, eg:
279 button = ButtonMixed(gtk.STOCK_DELETE, '_Delete message')."""
280 gtk.Button.__init__(self)
282 label = gtk.Label('')
283 label.set_text_with_mnemonic(message)
284 label.set_mnemonic_widget(self)
286 image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_BUTTON)
287 box = gtk.HBox(False, 2)
288 align = gtk.Alignment(0.5, 0.5, 0.0, 0.0)
290 box.pack_start(image, False, False, 0)
291 box.pack_end(label, False, False, 0)
293 self.add(align)
294 align.add(box)
295 align.show_all()
297 def confirm(parent, message, stock_icon, action = None):
298 """Display a <Cancel>/<Action> dialog. Result is true if the user
299 chooses the action, false otherwise. If action is given then that
300 is used as the text instead of the default for the stock item. Eg:
301 if rox.confirm('Really delete everything?', gtk.STOCK_DELETE): delete()
303 box = gtk.MessageDialog(parent, 0, gtk.MESSAGE_QUESTION,
304 gtk.BUTTONS_CANCEL, message)
305 if action:
306 button = ButtonMixed(stock_icon, action)
307 else:
308 button = gtk.Button(stock = stock_icon)
309 button.set_flags(gtk.CAN_DEFAULT)
310 button.show()
311 box.add_action_widget(button, gtk.RESPONSE_OK)
312 box.set_position(gtk.WIN_POS_CENTER)
313 box.set_title(_('Confirm:'))
314 box.set_default_response(gtk.RESPONSE_OK)
315 resp = box.run()
316 box.destroy()
317 return resp == int(gtk.RESPONSE_OK)
319 class PublishBox(gtk.MessageDialog):
320 def __init__(self, parent, buildenv):
321 gtk.MessageDialog.__init__(self, parent,
322 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
323 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL,
324 'Enter the directory on your HTTP or FTP server to which '
325 'the archive file will be uploaded:')
326 vbox = gtk.VBox(True, 4)
327 self.vbox.pack_start(vbox, False, True, 0)
328 vbox.set_border_width(8)
330 self.archive_dir = gtk.Entry()
331 self.archive_dir.set_activates_default(True)
332 self.set_default_response(gtk.RESPONSE_OK)
334 if buildenv.download_base_url:
335 self.archive_dir.set_text(buildenv.download_base_url)
336 else:
337 self.archive_dir.set_text('http://myserver.com/archives')
339 vbox.pack_start(self.archive_dir, False, True, 0)
340 vbox.show_all()
342 instructions = """Instructions
344 Compiling a program takes the program's human readable source code, and generates a binary which a computer can run.
346 To choose a different version of the source code, or the versions of any other programs needed to compile, use Setup.
348 To compile the chosen source code into a binary, use Build.
350 To add the new binary to the list of available versions for the program, use Register.
352 To publish the binary on the web, so that other people can run it, use Publish.
354 For further information, including details of how to make changes to the source code before compiling, visit: http://0install.net/0compile.html