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')
7 from zeroinstall
.injector
import reader
, writer
16 action_responses
= [RESPONSE_SETUP
, RESPONSE_BUILD
, RESPONSE_PUBLISH
, RESPONSE_REGISTER
]
18 main_path
= os
.path
.abspath(__main__
.__file
__)
20 class CompileBox(gtk
.Dialog
):
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
):
31 button
= ButtonMixed(getattr(gtk
, stock
), name
)
32 button
.set_flags(gtk
.CAN_DEFAULT
)
33 self
.add_action_widget(button
, resp
)
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
)
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)
61 self
.connect('delete-event', lambda box
, dev
: True)
63 def response(box
, resp
):
64 if resp
== RESPONSE_SETUP
:
68 to_delete
= [x
for x
in ['build', buildenv
.distdir
] if os
.path
.isdir(x
)]
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
):
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
)
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
:
84 self
.add_msg('\nBuild successful. Now register or publish the build.')
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')
97 align
.set_padding(8, 8, 8, 8)
99 self
.tv
.add_child_at_anchor(align
, anchor
)
101 def report_bug(button
):
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 []:
117 self
.add_msg("Feed '%s' is already registered for interface '%s'!\n" % (feed
, iface
.uri
))
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
)
128 if resp
== gtk
.RESPONSE_OK
:
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
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
153 self
.add_msg('\nSending SIGTERM to process...')
154 os
.kill(-self
.child
, signal
.SIGTERM
)
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
167 self
.success
= success
168 self
.failure
= failure
169 if isinstance(command
, basestring
):
170 self
.add_msg("Running: " + command
+ "\n")
172 self
.add_msg("Running: " + ' '.join(command
) + "\n")
177 self
.child
= os
.fork()
186 os
.setpgrp() # Become group leader
187 os
.execvp(command
[0], command
)
190 traceback
.print_exc()
199 for resp
in action_responses
:
200 self
.set_response_sensitive(resp
, False)
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
)
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)
226 # TODO: only insert complete UTF-8 sequences, not half sequences
227 self
.insert_at_end_and_scroll(data
)
230 pid
, status
= os
.waitpid(self
.child
, 0)
231 assert pid
== self
.child
234 self
.set_responses_sensitive()
236 if os
.WIFEXITED(status
) and os
.WEXITSTATUS(status
) == 0:
239 self
.add_msg("\nCommand terminated at user's request.")
241 self
.add_msg("\nCommand failed.")
247 def choose_dir(title
, default
):
248 sel
= gtk
.FileSelection(title
)
249 sel
.set_has_separator(False)
250 sel
.set_filename(default
)
253 if resp
== gtk
.RESPONSE_OK
:
254 build_dir
= sel
.get_filename()
255 if not os
.path
.exists(build_dir
):
258 alert(sel
, _("'%s' already exists") % build_dir
)
263 def alert(parent
, msg
):
264 d
= gtk
.MessageDialog(parent
,
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
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)
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
)
306 button
= ButtonMixed(stock_icon
, action
)
308 button
= gtk
.Button(stock
= stock_icon
)
309 button
.set_flags(gtk
.CAN_DEFAULT
)
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
)
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
)
337 self
.archive_dir
.set_text('http://myserver.com/archives')
339 vbox
.pack_start(self
.archive_dir
, False, True, 0)
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