Merged 0.51.1 branch
[zeroinstall/zeroinstall-afb.git] / zeroinstall / injector / selections.py
blobfaeba7787a183f07f73a52fc49153ceca02397fd
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 commands: how to run this selection (will contain more than one item if runners are used)
90 @type commands: [{L{Command}}]
91 @ivar selections: the selected implementations
92 @type selections: {str: L{Selection}}
93 """
94 __slots__ = ['interface', 'selections', 'commands']
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 self.commands = []
105 # (Solver will fill everything in)
106 elif isinstance(source, Policy):
107 self._init_from_policy(source)
108 elif isinstance(source, Element):
109 self._init_from_qdom(source)
110 else:
111 raise Exception(_("Source not a Policy or qdom.Element!"))
113 def _init_from_policy(self, policy):
114 """Set the selections from a policy.
115 @deprecated: use Solver.selections instead
116 @param policy: the policy giving the selected implementations."""
117 self.interface = policy.root
118 self.selections = policy.solver.selections.selections
119 self.commands = policy.solver.selections.commands
121 def _init_from_qdom(self, root):
122 """Parse and load a selections document.
123 @param root: a saved set of selections."""
124 self.interface = root.getAttribute('interface')
125 assert self.interface
126 self.commands = []
128 for selection in root.childNodes:
129 if selection.uri != XMLNS_IFACE:
130 continue
131 if selection.name != 'selection':
132 if selection.name == 'command':
133 self.commands.append(Command(selection, None))
134 continue
136 requires = []
137 bindings = []
138 digests = []
139 for dep_elem in selection.childNodes:
140 if dep_elem.uri != XMLNS_IFACE:
141 continue
142 if dep_elem.name in binding_names:
143 bindings.append(process_binding(dep_elem))
144 elif dep_elem.name == 'requires':
145 dep = process_depends(dep_elem, None)
146 requires.append(dep)
147 elif dep_elem.name == 'manifest-digest':
148 for aname, avalue in dep_elem.attrs.iteritems():
149 digests.append('%s=%s' % (aname, avalue))
151 # For backwards compatibility, allow getting the digest from the ID
152 sel_id = selection.attrs['id']
153 local_path = selection.attrs.get("local-path", None)
154 if (not digests and not local_path) and '=' in sel_id:
155 alg = sel_id.split('=', 1)[0]
156 if alg in ('sha1', 'sha1new', 'sha256'):
157 digests.append(sel_id)
159 iface_uri = selection.attrs['interface']
161 s = XMLSelection(requires, bindings, selection.attrs, digests)
162 self.selections[iface_uri] = s
164 if not self.commands:
165 # Old-style selections document; use the main attribute
166 if iface_uri == self.interface:
167 root_sel = self.selections[self.interface]
168 main = root_sel.attrs.get('main', None)
169 if main is not None:
170 self.commands = [Command(Element(XMLNS_IFACE, 'command', {'path': main}), None)]
172 def toDOM(self):
173 """Create a DOM document for the selected implementations.
174 The document gives the URI of the root, plus each selected implementation.
175 For each selected implementation, we record the ID, the version, the URI and
176 (if different) the feed URL. We also record all the bindings needed.
177 @return: a new DOM Document"""
178 from xml.dom import minidom, XMLNS_NAMESPACE
180 assert self.interface
182 impl = minidom.getDOMImplementation()
184 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
186 root = doc.documentElement
187 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
189 root.setAttributeNS(None, 'interface', self.interface)
191 prefixes = Prefixes()
193 for iface, selection in sorted(self.selections.items()):
194 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
195 selection_elem.setAttributeNS(None, 'interface', selection.interface)
196 root.appendChild(selection_elem)
198 for name, value in selection.attrs.iteritems():
199 if ' ' in name:
200 ns, localName = name.split(' ', 1)
201 selection_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, value)
202 elif name == 'from-feed':
203 # Don't bother writing from-feed attr if it's the same as the interface
204 if value != selection.attrs['interface']:
205 selection_elem.setAttributeNS(None, name, value)
206 elif name not in ('main', 'self-test'): # (replaced by <command>)
207 selection_elem.setAttributeNS(None, name, value)
209 if selection.digests:
210 manifest_digest = doc.createElementNS(XMLNS_IFACE, 'manifest-digest')
211 for digest in selection.digests:
212 aname, avalue = digest.split('=', 1)
213 assert ':' not in aname
214 manifest_digest.setAttribute(aname, avalue)
215 selection_elem.appendChild(manifest_digest)
217 for b in selection.bindings:
218 selection_elem.appendChild(b._toxml(doc))
220 for dep in selection.dependencies:
221 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
222 dep_elem.setAttributeNS(None, 'interface', dep.interface)
223 selection_elem.appendChild(dep_elem)
225 for m in dep.metadata:
226 parts = m.split(' ', 1)
227 if len(parts) == 1:
228 ns = None
229 localName = parts[0]
230 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
231 else:
232 ns, localName = parts
233 dep_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, dep.metadata[m])
235 for b in dep.bindings:
236 dep_elem.appendChild(b._toxml(doc))
238 for command in self.commands:
239 root.appendChild(command._toxml(doc, prefixes))
241 for ns, prefix in prefixes.prefixes.items():
242 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
244 return doc
246 def __repr__(self):
247 return "Selections for " + self.interface
249 def download_missing(self, iface_cache, fetcher):
250 """Check all selected implementations are available.
251 Download any that are not present.
252 Note: package implementations (distribution packages) are ignored.
253 @param iface_cache: cache to find feeds with download information
254 @param fetcher: used to download missing implementations
255 @return: a L{tasks.Blocker} or None"""
256 from zeroinstall.zerostore import NotStored
258 # Check that every required selection is cached
259 needed_downloads = []
260 for sel in self.selections.values():
261 if (not sel.local_path) and (not sel.id.startswith('package:')):
262 try:
263 iface_cache.stores.lookup_any(sel.digests)
264 except NotStored, ex:
265 needed_downloads.append(sel)
266 if not needed_downloads:
267 return
269 @tasks.async
270 def download():
271 # We're missing some. For each one, get the feed it came from
272 # and find the corresponding <implementation> in that. This will
273 # tell us where to get it from.
274 # Note: we look for an implementation with the same ID. Maybe we
275 # should check it has the same digest(s) too?
276 needed_impls = []
277 for sel in needed_downloads:
278 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
279 feed = iface_cache.get_feed(feed_url)
280 if feed is None or sel.id not in feed.implementations:
281 fetch_feed = fetcher.download_and_import_feed(feed_url, iface_cache)
282 yield fetch_feed
283 tasks.check(fetch_feed)
285 feed = iface_cache.get_feed(feed_url)
286 assert feed, "Failed to get feed for %s" % feed_url
287 impl = feed.implementations[sel.id]
288 needed_impls.append(impl)
290 fetch_impls = fetcher.download_impls(needed_impls, iface_cache.stores)
291 yield fetch_impls
292 tasks.check(fetch_impls)
293 return download()
295 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
297 def __getitem__(self, key):
298 # Deprecated
299 if isinstance(key, basestring):
300 return self.selections[key]
301 sel = self.selections[key.uri]
302 return sel and sel.impl
304 def iteritems(self):
305 # Deprecated
306 from zeroinstall.injector.iface_cache import iface_cache
307 for (uri, sel) in self.selections.iteritems():
308 yield (iface_cache.get_interface(uri), sel and sel.impl)
310 def values(self):
311 # Deprecated
312 from zeroinstall.injector.iface_cache import iface_cache
313 for (uri, sel) in self.selections.iteritems():
314 yield sel and sel.impl
316 def __iter__(self):
317 # Deprecated
318 from zeroinstall.injector.iface_cache import iface_cache
319 for (uri, sel) in self.selections.iteritems():
320 yield iface_cache.get_interface(uri)
322 def get(self, iface, if_missing):
323 # Deprecated
324 sel = self.selections.get(iface.uri, None)
325 if sel:
326 return sel.impl
327 return if_missing
329 def copy(self):
330 # Deprecated
331 s = Selections(None)
332 s.interface = self.interface
333 s.selections = self.selections.copy()
334 return s
336 def items(self):
337 # Deprecated
338 return list(self.iteritems())