1 from xml
.dom
import Node
, minidom
3 from StringIO
import StringIO
5 import rox
, os
, pango
, sys
, textwrap
, traceback
, subprocess
, shutil
6 from rox
import g
, tasks
, loading
11 from implementation
import ImplementationProperties
12 from requires
import Requires
13 from xmltools
import *
15 from zeroinstall
.injector
import model
, qdom
16 from zeroinstall
.zerostore
import Stores
19 RESPONSE_SAVE_AND_TEST
= 1
21 xml_header
= """<?xml version="1.0" ?>
23 xml_stylesheet_header
= """<?xml-stylesheet type='text/xsl' href='interface.xsl'?>
26 gladefile
= os
.path
.join(rox
.app_dir
, '0publish-gui.glade')
28 # Zero Install implementation cache
31 stylesheet_src
= os
.path
.join(os
.path
.dirname(__file__
), 'interface.xsl')
33 def available_in_path(prog
):
34 for d
in os
.environ
['PATH'].split(':'):
35 path
= os
.path
.join(d
, prog
)
36 if os
.path
.isfile(path
):
40 def get_terminal_emulator():
41 terminal_emulators
= ['x-terminal-emulator', 'xterm', 'konsole']
42 for xterm
in terminal_emulators
:
43 if available_in_path(xterm
):
48 tree
= gtk
.glade
.XML(gladefile
, 'no_file_specified')
49 box
= tree
.get_widget('no_file_specified')
50 tree
.get_widget('new_button').grab_focus()
54 chooser
= g
.FileChooserDialog('Choose a location for the new feed',
55 None, g
.FILE_CHOOSER_ACTION_SAVE
)
56 chooser
.set_current_name('MyProg.xml')
57 chooser
.add_button(g
.STOCK_CANCEL
, g
.RESPONSE_CANCEL
)
58 chooser
.add_button(g
.STOCK_NEW
, g
.RESPONSE_OK
)
60 chooser
= g
.FileChooserDialog('Choose the feed to edit',
61 None, g
.FILE_CHOOSER_ACTION_OPEN
)
62 chooser
.add_button(g
.STOCK_CANCEL
, g
.RESPONSE_CANCEL
)
63 chooser
.add_button(g
.STOCK_OPEN
, g
.RESPONSE_OK
)
66 chooser
.set_default_response(g
.RESPONSE_OK
)
67 if chooser
.run() != g
.RESPONSE_OK
:
69 path
= chooser
.get_filename()
71 return FeedEditor(path
)
73 def combo_set_text(combo
, text
):
74 if combo
.get_active_text() or text
:
75 model
= combo
.get_model()
82 combo
.append_text(text
)
87 def list_attrs(element
):
88 attrs
= element
.attributes
90 for x
in range(attrs
.length
):
93 if attr
.name
in ['id', 'version-modifier']: continue
94 if element
.localName
== 'implementation' and attr
.name
== 'version': continue
96 if attr
.name
in ('stability', 'arch'):
97 names
.append(attr
.value
)
99 names
.append(attr
.name
)
101 return ' (%s)' % ', '.join(names
)
105 emptyFeed
= """<?xml version='1.0'?>
106 <interface xmlns="%s">
109 """ % (XMLNS_INTERFACE
)
111 element_target
= ('INTERNAL:FeedEditor/Element', gtk
.TARGET_SAME_WIDGET
, 0)
113 class FeedEditor(loading
.XDSLoader
):
114 def __init__(self
, pathname
):
115 loading
.XDSLoader
.__init
__(self
, None)
117 self
.pathname
= pathname
119 self
.wTree
= gtk
.glade
.XML(gladefile
, 'main')
120 self
.window
= self
.wTree
.get_widget('main')
121 self
.window
.connect('destroy', rox
.toplevel_unref
)
122 self
.xds_proxy_for(self
.window
)
124 help = gtk
.glade
.XML(gladefile
, 'main_help')
125 help_box
= help.get_widget('main_help')
126 help_box
.set_default_size(g
.gdk
.screen_width() / 4,
127 g
.gdk
.screen_height() / 4)
128 help_box
.connect('delete-event', lambda box
, ev
: True)
129 help_box
.connect('response', lambda box
, resp
: box
.hide())
132 if resp
== g
.RESPONSE_HELP
:
134 elif resp
== RESPONSE_SAVE_AND_TEST
:
136 elif resp
== RESPONSE_SAVE
:
140 self
.window
.connect('response', resp
)
143 key_menu
= self
.wTree
.get_widget('feed_key')
144 key_model
= g
.ListStore(str, str)
145 key_menu
.set_model(key_model
)
146 cell
= g
.CellRendererText()
148 if gtk
.pygtk_version
>= (2, 8, 0):
149 # Crashes with pygtk 2.6.1
150 cell
.set_property('ellipsize', pango
.ELLIPSIZE_MIDDLE
)
152 key_menu
.pack_start(cell
)
153 key_menu
.add_attribute(cell
, 'text', 1)
155 self
.update_key_model()
157 self
.impl_model
= g
.TreeStore(str, object)
158 impl_tree
= self
.wTree
.get_widget('impl_tree')
159 impl_tree
.set_model(self
.impl_model
)
160 text
= g
.CellRendererText()
161 column
= g
.TreeViewColumn('', text
)
162 column
.add_attribute(text
, 'text', 0)
163 impl_tree
.append_column(column
)
165 impl_tree
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, [element_target
], gtk
.gdk
.ACTION_MOVE
)
166 impl_tree
.enable_model_drag_dest([element_target
], gtk
.gdk
.ACTION_MOVE
)
168 sel
= impl_tree
.get_selection()
169 sel
.set_mode(g
.SELECTION_BROWSE
)
171 if os
.path
.exists(self
.pathname
):
172 data
, _
, self
.key
= signing
.check_signature(self
.pathname
)
173 self
.doc
= minidom
.parseString(data
)
176 # Default to showing the versions tab
177 self
.wTree
.get_widget('notebook').next_page()
179 default_name
= os
.path
.basename(self
.pathname
)
180 if default_name
.endswith('.xml'):
181 default_name
= default_name
[:-4]
182 self
.doc
= minidom
.parseString(emptyFeed
)
184 key_menu
.set_active(0)
187 self
.wTree
.get_widget('feed_name').set_text(default_name
)
189 root
= self
.impl_model
.get_iter_root()
191 sel
.select_iter(root
)
193 self
.wTree
.get_widget('generate_key').connect('clicked', lambda b
: self
.generate_key())
195 self
.wTree
.get_widget('add_implementation').connect('clicked', lambda b
: self
.add_version())
196 self
.wTree
.get_widget('add_archive').connect('clicked', lambda b
: self
.add_archive())
197 self
.wTree
.get_widget('add_requires').connect('clicked', lambda b
: self
.add_requires())
198 self
.wTree
.get_widget('add_group').connect('clicked', lambda b
: self
.add_group())
199 self
.wTree
.get_widget('edit_properties').connect('clicked', lambda b
: self
.edit_properties())
200 self
.wTree
.get_widget('remove').connect('clicked', lambda b
: self
.remove_version())
201 impl_tree
.connect('row-activated', lambda tv
, path
, col
: self
.edit_properties(path
))
202 impl_tree
.connect('drag-data-received', self
.tree_drag_data_received
)
204 def update_key_model(self
):
205 key_menu
= self
.wTree
.get_widget('feed_key')
206 key_model
= key_menu
.get_model()
207 keys
= signing
.get_secret_keys()
209 key_model
.append((None, '(unsigned)'))
213 def generate_key(self
):
214 for x
in ['xterm', 'konsole']:
215 if available_in_path(x
):
216 child
= subprocess
.Popen([x
, '-e', 'gpg', '--gen-key'], stderr
= subprocess
.PIPE
)
219 child
= subprocess
.Popen(['gnome-terminal', '-e', 'gpg --gen-key'], stderr
= subprocess
.PIPE
)
221 def get_keygen_out():
224 yield tasks
.InputBlocker(child
.stderr
)
225 data
= os
.read(child
.stderr
.fileno(), 100)
229 self
.update_key_model()
231 rox
.alert('Errors from terminal: %s' % errors
)
233 tasks
.Task(get_keygen_out())
235 def tree_drag_data_received(self
, treeview
, context
, x
, y
, selection
, info
, time
):
236 if not selection
: return
237 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
239 model
= treeview
.get_model()
240 path
, position
= drop_info
242 src
= self
.get_selected()
243 dest
= model
[path
][1]
245 def is_ancestor_or_self(a
, b
):
247 if b
is a
: return True
251 if is_ancestor_or_self(src
, dest
):
252 # Can't move an element into itself!
255 if position
in (gtk
.TREE_VIEW_DROP_BEFORE
, gtk
.TREE_VIEW_DROP_AFTER
):
256 new_parent
= dest
.parentNode
260 if src
.namespaceURI
!= XMLNS_INTERFACE
: return
261 if new_parent
.namespaceURI
!= XMLNS_INTERFACE
: return
263 if new_parent
.localName
== 'group':
264 if src
.localName
not in ('implementation', 'group', 'requires'):
266 elif new_parent
.localName
== 'interface':
267 if src
.localName
not in ('implementation', 'group'):
269 elif new_parent
.localName
== 'implementation':
270 if src
.localName
not in ['requires']:
277 if position
== gtk
.TREE_VIEW_DROP_BEFORE
:
278 insert_before(src
, dest
)
279 elif position
== gtk
.TREE_VIEW_DROP_AFTER
:
280 next
= dest
.nextSibling
281 while next
and not next
.nodeType
== Node
.ELEMENT_NODE
:
282 next
= next
.nextSibling
284 insert_before(src
, next
)
286 insert_element(src
, new_parent
)
288 for next
in child_elements(new_parent
):
289 insert_before(src
, next
)
292 insert_element(src
, new_parent
)
293 self
.update_version_model()
295 def add_version(self
):
296 ImplementationProperties(self
)
299 ImplementationProperties(self
, is_group
= True)
301 def add_requires(self
):
302 elem
= self
.get_selected()
303 if elem
.namespaceURI
== XMLNS_INTERFACE
:
304 if elem
.localName
not in ('group', 'implementation'):
305 elem
= elem
.parentNode
306 if elem
.localName
in ('group', 'implementation'):
307 Requires(self
, parent
= elem
)
309 rox
.alert('Select a group or implementation!')
311 def edit_properties(self
, path
= None, element
= None):
312 assert not (path
and element
)
317 element
= self
.get_selected()
319 element
= self
.impl_model
[path
][1]
321 if element
.namespaceURI
!= XMLNS_INTERFACE
:
322 rox
.alert("Sorry, I don't known how to edit %s elements!" % element
.namespaceURI
)
324 if element
.localName
in ('group', 'implementation'):
325 ImplementationProperties(self
, element
)
326 elif element
.localName
== 'requires':
327 Requires(self
, parent
= element
.parentNode
, element
= element
)
329 rox
.alert("Sorry, I don't known how to edit %s elements!" % element
.localName
)
331 def update_fields(self
):
332 root
= self
.doc
.documentElement
335 value
= singleton_text(root
, name
)
337 self
.wTree
.get_widget('feed_' + name
).set_text(value
)
342 needs_terminal
= len(list(children(root
, 'needs-terminal'))) > 0
343 self
.wTree
.get_widget('feed_needs_terminal').set_active(needs_terminal
)
345 category_widget
= self
.wTree
.get_widget('feed_category')
346 category
= singleton_text(root
, 'category')
348 combo_set_text(category_widget
, category
)
350 category_widget
.set_active(0)
352 uri
= root
.getAttribute('uri')
354 self
.wTree
.get_widget('feed_url').set_text(uri
)
356 for feed_for
in children(root
, 'feed-for'):
357 self
.wTree
.get_widget('feed_feed_for').set_text(feed_for
.getAttribute('interface'))
359 for icon
in children(root
, 'icon'):
360 if icon
.getAttribute('type') == 'image/png':
361 href
= icon
.getAttribute('href')
362 self
.wTree
.get_widget('feed_icon').set_text(href
)
365 description
= singleton_text(root
, 'description') or ''
366 paragraphs
= [format_para(p
) for p
in description
.split('\n\n')]
367 buffer = self
.wTree
.get_widget('feed_description').get_buffer()
368 buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
369 buffer.insert_at_cursor('\n'.join(paragraphs
))
371 key_menu
= self
.wTree
.get_widget('feed_key')
372 model
= key_menu
.get_model()
376 if line
[0] == self
.key
:
380 model
.append((self
.key
, 'Missing key (%s)' % self
.key
))
381 key_menu
.set_active(i
)
383 key_menu
.set_active(0)
385 self
.update_version_model()
387 def add_archives(self
, impl_element
, iter):
388 for child
in child_elements(impl_element
):
389 if child
.namespaceURI
!= XMLNS_INTERFACE
: continue
390 if child
.localName
== 'archive':
391 self
.impl_model
.append(iter, ['Archive ' + child
.getAttribute('href'), child
])
392 elif child
.localName
== 'requires':
393 req_iface
= child
.getAttribute('interface')
394 self
.impl_model
.append(iter, ['Impl requires %s' % req_iface
, child
])
396 self
.impl_model
.append(iter, ['<%s>' % child
.localName
, child
])
398 def update_version_model(self
):
399 impl_tree
= self
.wTree
.get_widget('impl_tree')
401 # Remember which ones are open
402 expanded_elements
= set()
403 impl_tree
.map_expanded_rows(lambda tv
, path
: expanded_elements
.add(self
.impl_model
[path
][1]))
405 initial_build
= not self
.impl_model
.get_iter_root()
407 self
.impl_model
.clear()
409 def add_impls(elem
, iter, attrs
):
410 """Add all groups, implementations and requirements in elem"""
412 for x
in child_elements(elem
):
413 if x
.namespaceURI
!= XMLNS_INTERFACE
: continue
415 if x
.localName
== 'requires':
416 req_iface
= x
.getAttribute('interface')
417 new
= self
.impl_model
.append(iter, ['Group requires %s' % req_iface
, x
])
419 if x
.localName
not in ('implementation', 'group'): continue
421 new_attrs
= attrs
.copy()
422 attributes
= x
.attributes
423 for i
in range(attributes
.length
):
424 a
= attributes
.item(i
)
425 new_attrs
[str(a
.name
)] = a
.value
427 if x
.localName
== 'implementation':
428 version
= new_attrs
.get('version', '(missing version number)') + \
429 (new_attrs
.get('version-modifier') or '')
430 new
= self
.impl_model
.append(iter, ['Version %s%s' % (version
, list_attrs(x
)), x
])
431 self
.add_archives(x
, new
)
432 elif x
.localName
== 'group':
433 new
= self
.impl_model
.append(iter, ['Group%s' % list_attrs(x
), x
])
435 expanded_elements
.add(x
)
436 add_impls(x
, new
, new_attrs
)
438 add_impls(self
.doc
.documentElement
, None, attrs
= {})
440 def may_expand(model
, path
, iter):
441 if model
[iter][1] in expanded_elements
:
442 impl_tree
.expand_row(path
, False)
443 self
.impl_model
.foreach(may_expand
)
445 def test(self
, args
= []):
451 # Spawn a grandchild and exit
452 command
= ['0launch', '--gui'] + args
+ [self
.pathname
]
453 if self
.wTree
.get_widget('feed_needs_terminal').get_active():
454 command
= [get_terminal_emulator(), '-e'] + command
455 subprocess
.Popen(command
)
458 traceback
.print_exc()
461 pid
, status
= os
.waitpid(child
, 0)
464 raise Exception('Failed to run 0launch - status code %d' % status
)
466 def test_compile(self
, args
= []):
472 # Spawn a grandchild and exit
473 subprocess
.Popen(['0launch',
474 'http://0install.net/2006/interfaces/0compile.xml', 'gui'] +
475 args
+ [self
.pathname
])
478 traceback
.print_exc()
481 pid
, status
= os
.waitpid(child
, 0)
484 raise Exception('Failed to run 0compile - status code %d' % status
)
486 def update_doc(self
):
487 root
= self
.doc
.documentElement
488 def update(name
, required
= False, attrs
= {}, value_attr
= None):
489 widget
= self
.wTree
.get_widget('feed_' + name
.replace('-', '_'))
490 if isinstance(widget
, g
.TextView
):
491 buffer = widget
.get_buffer()
492 text
= buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter())
493 paras
= ['\n'.join(textwrap
.wrap(para
, 80)) for para
in text
.split('\n') if para
.strip()]
494 value
= '\n' + '\n\n'.join(paras
)
495 elif isinstance(widget
, g
.ComboBox
):
496 if widget
.get_active() == 0:
499 value
= widget
.get_active_text()
500 elif isinstance(widget
, g
.ToggleButton
):
501 value
= widget
.get_active()
503 value
= widget
.get_text()
504 elems
= list(children(root
, name
, attrs
= attrs
))
509 elem
= create_element(root
, name
,
510 before
= ['group', 'implementation', 'requires'])
512 elem
.setAttribute(x
, attrs
[x
])
515 elem
.setAttribute(value_attr
, value
)
517 elif isinstance(widget
, g
.ToggleButton
):
521 set_data(elem
, value
)
524 raise Exception('Missing required field "%s"' % name
)
529 update('summary', True)
530 update('description', True)
533 update('feed-for', value_attr
= 'interface')
534 update('needs-terminal')
535 update('icon', attrs
= {'type': 'image/png'}, value_attr
= 'href')
537 uri
= self
.wTree
.get_widget('feed_url').get_text()
539 root
.setAttribute('uri', uri
)
540 elif root
.hasAttribute('uri'):
541 root
.removeAttribute('uri')
543 key_menu
= self
.wTree
.get_widget('feed_key')
544 key_model
= key_menu
.get_model()
545 self
.key
= key_model
[key_menu
.get_active()][0]
547 def export_stylesheet_and_key(self
):
548 dir = os
.path
.dirname(os
.path
.abspath(self
.pathname
))
549 stylesheet
= os
.path
.join(dir, 'interface.xsl')
550 if not os
.path
.exists(stylesheet
):
551 shutil
.copyfile(stylesheet_src
, stylesheet
)
552 rox
.info("I have saved a stylesheet as '%s'. You should upload "
553 "this to your web-server in the same directory as the feed file. "
554 "This allows browsers to display the feed nicely." % stylesheet
)
556 if os
.path
.abspath(self
.pathname
).endswith('/feed.xml'):
557 # Probably the feed's URL is the directory, so we'll get the key from the parent.
558 dir = os
.path
.dirname(dir)
560 exported
= signing
.export_key(dir, self
.key
)
562 rox
.info("I have exported your public key as '%s'. You should upload "
563 "this to your web-server in the same directory as the feed file. "
564 "This allows people to check the signature on your feed." % exported
)
566 def save(self
, callback
= None):
570 sign
= signing
.sign_xml
571 self
.export_stylesheet_and_key()
572 data
+= xml_stylesheet_header
574 sign
= signing
.sign_unsigned
575 data
+= self
.doc
.documentElement
.toxml() + '\n'
577 gen
= sign(self
.pathname
, data
, self
.key
, callback
)
578 # May require interaction to get the pass-phrase, so run in the background...
582 def add_archive(self
):
583 archive
.AddArchiveBox(self
)
585 def xds_load_from_file(self
, path
):
586 archive
.AddArchiveBox(self
, local_archive
= path
)
588 def remove_version(self
, path
= None):
589 elem
= self
.get_selected()
591 self
.update_version_model()
593 def get_selected(self
):
594 tree
= self
.wTree
.get_widget('impl_tree')
595 sel
= tree
.get_selection()
596 model
, iter = sel
.get_selected()
598 raise Exception('Select something first!')
599 return model
[iter][1]
601 def find_implementation(self
, id):
602 def find_impl(parent
):
603 for x
in child_elements(parent
):
604 if x
.namespaceURI
!= XMLNS_INTERFACE
: continue
605 if x
.localName
== 'group':
608 elif x
.localName
== 'implementation':
609 if x
.getAttribute('id') == id:
611 return find_impl(self
.doc
.documentElement
)
613 def list_versions(self
):
614 """Return a list of (version, element) pairs, one for each <implementation>."""
617 def add_versions(parent
, version
):
618 for x
in child_elements(parent
):
619 if x
.namespaceURI
!= XMLNS_INTERFACE
: continue
620 if x
.hasAttribute('version'): version
= x
.getAttribute('version')
621 if x
.localName
== 'group':
622 add_versions(x
, version
)
623 elif x
.localName
== 'implementation':
624 versions
.append((model
.parse_version(version
), x
))
626 add_versions(self
.doc
.documentElement
, version
= None)
630 def get_as_feed(self
):
632 xml
= self
.doc
.documentElement
.toxml(encoding
= 'utf-8')
633 return model
.ZeroInstallFeed(qdom
.parse(StringIO(xml
)), local_path
= os
.path
.abspath(self
.pathname
))