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
.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
):
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_wrap_mode(gtk
.WRAP_WORD
)
48 swin
= gtk
.ScrolledWindow()
49 swin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_ALWAYS
)
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)
59 self
.connect('delete-event', lambda box
, dev
: True)
61 def response(box
, resp
):
62 if resp
== RESPONSE_SETUP
:
66 to_delete
= [x
for x
in ['build', buildenv
.distdir
] if os
.path
.isdir(x
)]
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
):
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
)
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
:
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
:
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 []:
92 self
.add_msg("Feed '%s' is already registered for interface '%s'!\n" % (feed
, iface
.uri
))
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
:
100 box
= PublishBox(self
, buildenv
)
103 if resp
== gtk
.RESPONSE_OK
:
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 "
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
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
129 self
.add_msg('\nSending SIGTERM to process...')
130 os
.kill(-self
.child
, signal
.SIGTERM
)
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
139 self
.success
= success
140 if isinstance(command
, basestring
):
141 self
.add_msg("Running: " + command
+ "\n")
143 self
.add_msg("Running: " + ' '.join(command
) + "\n")
148 self
.child
= os
.fork()
157 os
.setpgrp() # Become group leader
158 os
.execvp(command
[0], command
)
161 traceback
.print_exc()
170 for resp
in action_responses
:
171 self
.set_response_sensitive(resp
, False)
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
)
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)
197 # TODO: only insert complete UTF-8 sequences, not half sequences
198 self
.insert_at_end_and_scroll(data
)
201 pid
, status
= os
.waitpid(self
.child
, 0)
202 assert pid
== self
.child
205 self
.set_responses_sensitive()
207 if os
.WIFEXITED(status
) and os
.WEXITSTATUS(status
) == 0:
210 self
.add_msg("\nCommand terminated at user's request.")
212 self
.add_msg("\nCommand failed.")
216 def choose_dir(title
, default
):
217 sel
= gtk
.FileSelection(title
)
218 sel
.set_has_separator(False)
219 sel
.set_filename(default
)
222 if resp
== gtk
.RESPONSE_OK
:
223 build_dir
= sel
.get_filename()
224 if not os
.path
.exists(build_dir
):
227 alert(sel
, _("'%s' already exists") % build_dir
)
232 def alert(parent
, msg
):
233 d
= gtk
.MessageDialog(parent
,
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
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)
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
)
275 button
= ButtonMixed(stock_icon
, action
)
277 button
= gtk
.Button(stock
= stock_icon
)
278 button
.set_flags(gtk
.CAN_DEFAULT
)
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
)
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
)
306 self
.archive_dir
.set_text('http://myserver.com/archives')
308 vbox
.pack_start(self
.archive_dir
, False, True, 0)
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