Use the feed's <name> to generate the archive name
[0compile.git] / gui_support.py
blob64525b563d001c9706150e3e5d35804874ed8ede
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)
110 feed = buildenv.local_iface_file
111 for f in iface.feeds or []:
112 if f.uri == feed:
113 self.add_msg("Feed '%s' is already registered for interface '%s'!\n" % (feed, iface.uri))
114 return
115 box.buffer.insert_at_cursor("Registering feed '%s'\n" % feed)
116 iface.extra_feeds.append(model.Feed(feed, arch = None, user_override = True))
117 writer.save_interface(iface)
118 box.buffer.insert_at_cursor("Done. You can now close this window.\n")
119 elif resp == RESPONSE_PUBLISH:
120 buildenv = BuildEnv()
121 box = PublishBox(self, buildenv)
122 resp = box.run()
123 box.destroy()
124 if resp == gtk.RESPONSE_OK:
125 def done_publish():
126 self.add_msg("\nYou can use '0publish --local' to add this "
127 "into the main feed. If you don't have a main feed then this "
128 "will create one. See "
129 "http://0install.net/injector-packagers.html for more information.")
130 self.run_command((sys.executable, main_path,
131 'publish', box.archive_dir.get_text()), done_publish)
132 elif resp == gtk.RESPONSE_CANCEL or resp == gtk.RESPONSE_DELETE_EVENT:
133 if self.kill_child(): return
134 self.destroy()
135 else:
136 self.add_msg('Unknown response: %s' % resp)
138 self.connect('response', response)
140 self.system_tag = self.buffer.create_tag('system', foreground = 'blue', background = 'white')
141 self.add_msg(instructions)
142 self.set_responses_sensitive()
144 def kill_child(self):
145 if self.child is None: return False
147 import signal
148 self.killed = True
149 self.add_msg('\nSending SIGTERM to process...')
150 os.kill(-self.child, signal.SIGTERM)
151 return True
153 def add_msg(self, msg):
154 self.insert_at_end_and_scroll(msg + '\n', self.system_tag)
156 """Run command in a sub-process.
157 Calls success() if the command exits with status zero.
158 Calls failure() if it fails for other reasons.
159 (neither is called if the user aborts the command)"""
160 def run_command(self, command, success, failure = None):
161 assert self.child is None
162 self.killed = False
163 self.success = success
164 self.failure = failure
165 if isinstance(command, basestring):
166 self.add_msg("Running: " + command + "\n")
167 else:
168 self.add_msg("Running: " + ' '.join(command) + "\n")
170 r, w = os.pipe()
171 try:
172 try:
173 self.child = os.fork()
174 if not self.child:
175 # We are the child
176 try:
177 try:
178 os.close(r)
179 os.dup2(w, 1)
180 os.dup2(w, 2)
181 os.close(w)
182 os.setpgrp() # Become group leader
183 os.execvp(command[0], command)
184 except:
185 import traceback
186 traceback.print_exc()
187 finally:
188 os._exit(1)
189 finally:
190 os.close(w)
191 except:
192 os.close(r)
193 raise
195 for resp in action_responses:
196 self.set_response_sensitive(resp, False)
198 # We are the parent
199 gobject.io_add_watch(r, gobject.IO_IN | gobject.IO_HUP, self.got_data)
201 def set_responses_sensitive(self):
202 self.set_response_sensitive(RESPONSE_SETUP, True)
203 self.set_response_sensitive(RESPONSE_BUILD, True)
205 buildenv = BuildEnv()
206 have_binary = os.path.exists(buildenv.local_iface_file)
207 self.set_response_sensitive(RESPONSE_REGISTER, have_binary)
208 self.set_response_sensitive(RESPONSE_PUBLISH, have_binary)
210 def insert_at_end_and_scroll(self, data, *tags):
211 near_end = self.vscroll.upper - self.vscroll.page_size * 1.5 < self.vscroll.value
212 end = self.buffer.get_end_iter()
213 self.buffer.insert_with_tags(end, data, *tags)
214 if near_end:
215 cursor = self.buffer.get_insert()
216 self.buffer.move_mark(cursor, end)
217 self.tv.scroll_to_mark(cursor, 0, False, 0, 0)
219 def got_data(self, src, cond):
220 data = os.read(src, 100)
221 if data:
222 # TODO: only insert complete UTF-8 sequences, not half sequences
223 self.insert_at_end_and_scroll(data)
224 return True
225 else:
226 pid, status = os.waitpid(self.child, 0)
227 assert pid == self.child
228 self.child = None
230 self.set_responses_sensitive()
232 if os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0:
233 self.success()
234 elif self.killed:
235 self.add_msg("\nCommand terminated at user's request.")
236 else:
237 self.add_msg("\nCommand failed.")
238 if self.failure:
239 self.failure()
240 return False
243 def choose_dir(title, default):
244 sel = gtk.FileSelection(title)
245 sel.set_has_separator(False)
246 sel.set_filename(default)
247 while True:
248 resp = sel.run()
249 if resp == gtk.RESPONSE_OK:
250 build_dir = sel.get_filename()
251 if not os.path.exists(build_dir):
252 sel.destroy()
253 return build_dir
254 alert(sel, _("'%s' already exists") % build_dir)
255 else:
256 sel.destroy()
257 return None
259 def alert(parent, msg):
260 d = gtk.MessageDialog(parent,
261 gtk.DIALOG_MODAL,
262 gtk.MESSAGE_ERROR,
263 gtk.BUTTONS_OK,
264 msg)
265 d.run()
266 d.destroy()
268 class ButtonMixed(gtk.Button):
269 """A button with a standard stock icon, but any label. This is useful
270 when you want to express a concept similar to one of the stock ones."""
271 def __init__(self, stock, message):
272 """Specify the icon and text for the new button. The text
273 may specify the mnemonic for the widget by putting a _ before
274 the letter, eg:
275 button = ButtonMixed(gtk.STOCK_DELETE, '_Delete message')."""
276 gtk.Button.__init__(self)
278 label = gtk.Label('')
279 label.set_text_with_mnemonic(message)
280 label.set_mnemonic_widget(self)
282 image = gtk.image_new_from_stock(stock, gtk.ICON_SIZE_BUTTON)
283 box = gtk.HBox(False, 2)
284 align = gtk.Alignment(0.5, 0.5, 0.0, 0.0)
286 box.pack_start(image, False, False, 0)
287 box.pack_end(label, False, False, 0)
289 self.add(align)
290 align.add(box)
291 align.show_all()
293 def get_build_options(parent, message):
294 box = gtk.MessageDialog(parent, 0, gtk.MESSAGE_QUESTION,
295 gtk.BUTTONS_CANCEL, message)
297 button = ButtonMixed(gtk.STOCK_GO_FORWARD, 'Force')
298 button.show()
299 box.add_action_widget(button, 2)
301 button = ButtonMixed(gtk.STOCK_CLEAR, 'Clean')
302 button.set_flags(gtk.CAN_DEFAULT)
303 button.show()
304 box.add_action_widget(button, 1)
306 box.set_position(gtk.WIN_POS_CENTER)
307 box.set_title(_('Confirm:'))
308 box.set_default_response(1)
309 resp = box.run()
310 box.destroy()
312 if resp == 1:
313 return ['--clean']
314 elif resp == 2:
315 return ['--force']
316 return None
318 class PublishBox(gtk.MessageDialog):
319 def __init__(self, parent, buildenv):
320 gtk.MessageDialog.__init__(self, parent,
321 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
322 gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL,
323 'Enter the directory on your HTTP or FTP server to which '
324 'the archive file will be uploaded:')
325 vbox = gtk.VBox(True, 4)
326 self.vbox.pack_start(vbox, False, True, 0)
327 vbox.set_border_width(8)
329 self.archive_dir = gtk.Entry()
330 self.archive_dir.set_activates_default(True)
331 self.set_default_response(gtk.RESPONSE_OK)
333 if buildenv.download_base_url:
334 self.archive_dir.set_text(buildenv.download_base_url)
335 else:
336 self.archive_dir.set_text('http://myserver.com/archives')
338 vbox.pack_start(self.archive_dir, False, True, 0)
339 vbox.show_all()
341 instructions = """Instructions
343 Compiling a program takes the program's human readable source code, and generates a binary which a computer can run.
345 To choose a different version of the source code, or the versions of any other programs needed to compile, use Setup.
347 To compile the chosen source code into a binary, use Build.
349 To add the new binary to the list of available versions for the program, use Register.
351 To publish the binary on the web, so that other people can run it, use Publish.
353 For further information, including details of how to make changes to the source code before compiling, visit: http://0install.net/0compile.html