Added support for <command> element
[zeroinstall.git] / zeroinstall / injector / selections.py
blob9d202af9619aee59bd4f670fc18420cf8ddcc372
1 """
2 Load and save a set of chosen implementations.
3 @since: 0.27
4 """
6 # Copyright (C) 2009, Thomas Leonard
7 # See the README file for details, or visit http://0install.net.
9 from zeroinstall import _
10 from zeroinstall.injector.policy import Policy
11 from zeroinstall.injector.model import process_binding, process_depends, binding_names, Command
12 from zeroinstall.injector.namespaces import XMLNS_IFACE
13 from zeroinstall.injector.qdom import Element, Prefixes
14 from zeroinstall.support import tasks
16 class Selection(object):
17 """A single selected implementation in a L{Selections} set.
18 @ivar dependencies: list of dependencies
19 @type dependencies: [L{model.Dependency}]
20 @ivar attrs: XML attributes map (name is in the format "{namespace} {localName}")
21 @type attrs: {str: str}
22 @ivar digests: a list of manifest digests
23 @type digests: [str]
24 @ivar version: the implementation's version number
25 @type version: str"""
27 interface = property(lambda self: self.attrs['interface'])
28 id = property(lambda self: self.attrs['id'])
29 version = property(lambda self: self.attrs['version'])
30 feed = property(lambda self: self.attrs.get('from-feed', self.interface))
31 main = property(lambda self: self.attrs.get('main', None))
33 @property
34 def local_path(self):
35 local_path = self.attrs.get('local-path', None)
36 if local_path:
37 return local_path
38 if self.id.startswith('/'):
39 return self.id
40 return None
42 def __repr__(self):
43 return self.id
45 class ImplSelection(Selection):
46 __slots__ = ['impl', 'dependencies', 'attrs']
48 def __init__(self, iface_uri, impl, dependencies):
49 assert impl
50 self.impl = impl
51 self.dependencies = dependencies
53 attrs = impl.metadata.copy()
54 attrs['id'] = impl.id
55 attrs['version'] = impl.get_version()
56 attrs['interface'] = iface_uri
57 attrs['from-feed'] = impl.feed.url
58 if impl.local_path:
59 attrs['local-path'] = impl.local_path
60 self.attrs = attrs
62 @property
63 def bindings(self): return self.impl.bindings
65 @property
66 def digests(self): return self.impl.digests
68 class XMLSelection(Selection):
69 __slots__ = ['bindings', 'dependencies', 'attrs', 'digests']
71 def __init__(self, dependencies, bindings = None, attrs = None, digests = None):
72 if bindings is None: bindings = []
73 if digests is None: digests = []
74 self.dependencies = dependencies
75 self.bindings = bindings
76 self.attrs = attrs
77 self.digests = digests
79 assert self.interface
80 assert self.id
81 assert self.version
82 assert self.feed
84 class Selections(object):
85 """
86 A selected set of components which will make up a complete program.
87 @ivar interface: the interface of the program
88 @type interface: str
89 @ivar command: how to run this selection
90 @type command: {L{Command}}
91 @ivar selections: the selected implementations
92 @type selections: {str: L{Selection}}
93 """
94 __slots__ = ['interface', 'selections', 'command']
96 def __init__(self, source):
97 """Constructor.
98 @param source: a map of implementations, policy or selections document
99 @type source: {str: L{Selection}} | L{Policy} | L{Element}
101 self.selections = {}
103 if source is None:
104 pass
105 elif isinstance(source, Policy):
106 self._init_from_policy(source)
107 elif isinstance(source, Element):
108 self._init_from_qdom(source)
109 else:
110 raise Exception(_("Source not a Policy or qdom.Element!"))
112 def _init_from_policy(self, policy):
113 """Set the selections from a policy.
114 @deprecated: use Solver.selections instead
115 @param policy: the policy giving the selected implementations."""
116 self.interface = policy.root
117 self.selections = policy.solver.selections.selections
118 self.command = policy.solver.selections.command
120 def _init_from_qdom(self, root):
121 """Parse and load a selections document.
122 @param root: a saved set of selections."""
123 self.interface = root.getAttribute('interface')
124 assert self.interface
125 self.command = None
127 for selection in root.childNodes:
128 if selection.uri != XMLNS_IFACE:
129 continue
130 if selection.name != 'selection':
131 if selection.name == 'command':
132 self.command = Command(selection)
133 continue
135 requires = []
136 bindings = []
137 digests = []
138 for dep_elem in selection.childNodes:
139 if dep_elem.uri != XMLNS_IFACE:
140 continue
141 if dep_elem.name in binding_names:
142 bindings.append(process_binding(dep_elem))
143 elif dep_elem.name == 'requires':
144 dep = process_depends(dep_elem)
145 requires.append(dep)
146 elif dep_elem.name == 'manifest-digest':
147 for aname, avalue in dep_elem.attrs.iteritems():
148 digests.append('%s=%s' % (aname, avalue))
150 # For backwards compatibility, allow getting the digest from the ID
151 sel_id = selection.attrs['id']
152 local_path = selection.attrs.get("local-path", None)
153 if (not digests and not local_path) and '=' in sel_id:
154 alg = sel_id.split('=', 1)[0]
155 if alg in ('sha1', 'sha1new', 'sha256'):
156 digests.append(sel_id)
158 iface_uri = selection.attrs['interface']
159 if iface_uri == self.interface:
160 main = selection.attrs.get('main', None)
161 if main is not None:
162 self.command = Command(Element(XMLNS_IFACE, 'command', {'path': main}))
164 s = XMLSelection(requires, bindings, selection.attrs, digests)
165 self.selections[iface_uri] = s
167 def toDOM(self):
168 """Create a DOM document for the selected implementations.
169 The document gives the URI of the root, plus each selected implementation.
170 For each selected implementation, we record the ID, the version, the URI and
171 (if different) the feed URL. We also record all the bindings needed.
172 @return: a new DOM Document"""
173 from xml.dom import minidom, XMLNS_NAMESPACE
175 assert self.interface
177 impl = minidom.getDOMImplementation()
179 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
181 root = doc.documentElement
182 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
184 root.setAttributeNS(None, 'interface', self.interface)
186 prefixes = Prefixes()
188 for iface, selection in sorted(self.selections.items()):
189 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
190 selection_elem.setAttributeNS(None, 'interface', selection.interface)
191 root.appendChild(selection_elem)
193 for name, value in selection.attrs.iteritems():
194 if ' ' in name:
195 ns, localName = name.split(' ', 1)
196 selection_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, value)
197 elif name == 'from-feed':
198 # Don't bother writing from-feed attr if it's the same as the interface
199 if value != selection.attrs['interface']:
200 selection_elem.setAttributeNS(None, name, value)
201 elif name not in ('main', 'self-test'): # (replaced by <command>)
202 selection_elem.setAttributeNS(None, name, value)
204 if selection.digests:
205 manifest_digest = doc.createElementNS(XMLNS_IFACE, 'manifest-digest')
206 for digest in selection.digests:
207 aname, avalue = digest.split('=', 1)
208 assert ':' not in aname
209 manifest_digest.setAttribute(aname, avalue)
210 selection_elem.appendChild(manifest_digest)
212 for b in selection.bindings:
213 selection_elem.appendChild(b._toxml(doc))
215 for dep in selection.dependencies:
216 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
217 dep_elem.setAttributeNS(None, 'interface', dep.interface)
218 selection_elem.appendChild(dep_elem)
220 for m in dep.metadata:
221 parts = m.split(' ', 1)
222 if len(parts) == 1:
223 ns = None
224 localName = parts[0]
225 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
226 else:
227 ns, localName = parts
228 dep_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, dep.metadata[m])
230 for b in dep.bindings:
231 dep_elem.appendChild(b._toxml(doc))
233 if self.command:
234 root.appendChild(self.command._toxml(doc, prefixes))
236 for ns, prefix in prefixes.prefixes.items():
237 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
239 return doc
241 def __repr__(self):
242 return "Selections for " + self.interface
244 def download_missing(self, iface_cache, fetcher):
245 """Check all selected implementations are available.
246 Download any that are not present.
247 Note: package implementations (distribution packages) are ignored.
248 @param iface_cache: cache to find feeds with download information
249 @param fetcher: used to download missing implementations
250 @return: a L{tasks.Blocker} or None"""
251 from zeroinstall.zerostore import NotStored
253 # Check that every required selection is cached
254 needed_downloads = []
255 for sel in self.selections.values():
256 if (not sel.local_path) and (not sel.id.startswith('package:')):
257 try:
258 iface_cache.stores.lookup_any(sel.digests)
259 except NotStored, ex:
260 needed_downloads.append(sel)
261 if not needed_downloads:
262 return
264 @tasks.async
265 def download():
266 # We're missing some. For each one, get the feed it came from
267 # and find the corresponding <implementation> in that. This will
268 # tell us where to get it from.
269 # Note: we look for an implementation with the same ID. Maybe we
270 # should check it has the same digest(s) too?
271 needed_impls = []
272 for sel in needed_downloads:
273 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
274 feed = iface_cache.get_feed(feed_url)
275 if feed is None or sel.id not in feed.implementations:
276 fetch_feed = fetcher.download_and_import_feed(feed_url, iface_cache)
277 yield fetch_feed
278 tasks.check(fetch_feed)
280 feed = iface_cache.get_feed(feed_url)
281 assert feed, "Failed to get feed for %s" % feed_url
282 impl = feed.implementations[sel.id]
283 needed_impls.append(impl)
285 fetch_impls = fetcher.download_impls(needed_impls, iface_cache.stores)
286 yield fetch_impls
287 tasks.check(fetch_impls)
288 return download()
290 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
292 def __getitem__(self, key):
293 # Deprecated
294 if isinstance(key, basestring):
295 return self.selections[key]
296 sel = self.selections[key.uri]
297 return sel and sel.impl
299 def iteritems(self):
300 # Deprecated
301 from zeroinstall.injector.iface_cache import iface_cache
302 for (uri, sel) in self.selections.iteritems():
303 yield (iface_cache.get_interface(uri), sel and sel.impl)
305 def values(self):
306 # Deprecated
307 from zeroinstall.injector.iface_cache import iface_cache
308 for (uri, sel) in self.selections.iteritems():
309 yield sel and sel.impl
311 def __iter__(self):
312 # Deprecated
313 from zeroinstall.injector.iface_cache import iface_cache
314 for (uri, sel) in self.selections.iteritems():
315 yield iface_cache.get_interface(uri)
317 def get(self, iface, if_missing):
318 # Deprecated
319 sel = self.selections.get(iface.uri, None)
320 if sel:
321 return sel.impl
322 return if_missing
324 def copy(self):
325 # Deprecated
326 s = Selections(None)
327 s.interface = self.interface
328 s.selections = self.selections.copy()
329 return s
331 def items(self):
332 # Deprecated
333 return list(self.iteritems())