Propagate errors correctly from Selections.download_missing()
[zeroinstall.git] / zeroinstall / injector / selections.py
blobacd8a647537306e97d3bec8e8a692eafdf5c0b10
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
12 from zeroinstall.injector.namespaces import XMLNS_IFACE
13 from zeroinstall.injector.qdom import Element
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"""
26 __slots__ = ['bindings', 'dependencies', 'attrs', 'digests']
28 def __init__(self, dependencies, bindings = None, attrs = None, digests = None):
29 if bindings is None: bindings = []
30 if digests is None: digests = []
31 self.dependencies = dependencies
32 self.bindings = bindings
33 self.attrs = attrs
34 self.digests = digests
36 assert self.interface
37 assert self.id
38 assert self.version
39 assert self.feed
41 interface = property(lambda self: self.attrs['interface'])
42 id = property(lambda self: self.attrs['id'])
43 version = property(lambda self: self.attrs['version'])
44 feed = property(lambda self: self.attrs.get('from-feed', self.interface))
45 main = property(lambda self: self.attrs.get('main', None))
47 @property
48 def local_path(self):
49 local_path = self.attrs.get('local-path', None)
50 if local_path:
51 return local_path
52 if self.id.startswith('/'):
53 return self.id
54 return None
56 def __repr__(self):
57 return self.id
59 class Selections(object):
60 """
61 A selected set of components which will make up a complete program.
62 @ivar interface: the interface of the program
63 @type interface: str
64 @ivar selections: the selected implementations
65 @type selections: {str: L{Selection}}
66 """
67 __slots__ = ['interface', 'selections']
69 def __init__(self, source):
70 """Constructor.
71 @param source: a map of implementations, policy or selections document
72 @type source: {str: L{Selection}} | L{Policy} | L{Element}
73 """
74 if isinstance(source, dict):
75 self.selections = source
76 elif isinstance(source, Policy):
77 self.selections = {}
78 self._init_from_policy(source)
79 elif isinstance(source, Element):
80 self.selections = {}
81 self._init_from_qdom(source)
82 else:
83 raise Exception(_("Source not a Policy or qdom.Element!"))
85 def _init_from_policy(self, policy):
86 """Set the selections from a policy.
87 @param policy: the policy giving the selected implementations."""
88 self.interface = policy.root
89 solver_requires = policy.solver.requires
91 for needed_iface in policy.implementation:
92 impl = policy.implementation[needed_iface]
93 assert impl
95 attrs = impl.metadata.copy()
96 attrs['id'] = impl.id
97 attrs['version'] = impl.get_version()
98 attrs['interface'] = needed_iface.uri
99 attrs['from-feed'] = impl.feed.url
100 if impl.local_path:
101 attrs['local-path'] = impl.local_path
103 self.selections[needed_iface.uri] = Selection(solver_requires[needed_iface], impl.bindings, attrs, impl.digests)
105 def _init_from_qdom(self, root):
106 """Parse and load a selections document.
107 @param root: a saved set of selections."""
108 self.interface = root.getAttribute('interface')
109 assert self.interface
111 for selection in root.childNodes:
112 if selection.uri != XMLNS_IFACE:
113 continue
114 if selection.name != 'selection':
115 continue
117 requires = []
118 bindings = []
119 digests = []
120 for dep_elem in selection.childNodes:
121 if dep_elem.uri != XMLNS_IFACE:
122 continue
123 if dep_elem.name in binding_names:
124 bindings.append(process_binding(dep_elem))
125 elif dep_elem.name == 'requires':
126 dep = process_depends(dep_elem)
127 requires.append(dep)
128 elif dep_elem.name == 'manifest-digest':
129 for aname, avalue in dep_elem.attrs.iteritems():
130 digests.append('%s=%s' % (aname, avalue))
132 # For backwards compatibility, allow getting the digest from the ID
133 sel_id = selection.attrs['id']
134 local_path = selection.attrs.get("local-path", None)
135 if (not digests and not local_path) and '=' in sel_id:
136 alg = sel_id.split('=', 1)[0]
137 if alg in ('sha1', 'sha1new', 'sha256'):
138 digests.append(sel_id)
140 s = Selection(requires, bindings, selection.attrs, digests)
141 self.selections[selection.attrs['interface']] = s
143 def toDOM(self):
144 """Create a DOM document for the selected implementations.
145 The document gives the URI of the root, plus each selected implementation.
146 For each selected implementation, we record the ID, the version, the URI and
147 (if different) the feed URL. We also record all the bindings needed.
148 @return: a new DOM Document"""
149 from xml.dom import minidom, XMLNS_NAMESPACE
151 assert self.interface
153 impl = minidom.getDOMImplementation()
155 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
157 root = doc.documentElement
158 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
160 root.setAttributeNS(None, 'interface', self.interface)
162 def ensure_prefix(prefixes, ns):
163 prefix = prefixes.get(ns, None)
164 if prefix:
165 return prefix
166 prefix = 'ns%d' % len(prefixes)
167 prefixes[ns] = prefix
168 return prefix
170 prefixes = {}
172 for iface, selection in sorted(self.selections.items()):
173 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
174 selection_elem.setAttributeNS(None, 'interface', selection.interface)
175 root.appendChild(selection_elem)
177 for name, value in selection.attrs.iteritems():
178 if ' ' in name:
179 ns, localName = name.split(' ', 1)
180 selection_elem.setAttributeNS(ns, ensure_prefix(prefixes, ns) + ':' + localName, value)
181 elif name != 'from-feed':
182 selection_elem.setAttributeNS(None, name, value)
183 elif value != selection.attrs['interface']:
184 # Don't bother writing from-feed attr if it's the same as the interface
185 selection_elem.setAttributeNS(None, name, value)
187 if selection.digests:
188 manifest_digest = doc.createElementNS(XMLNS_IFACE, 'manifest-digest')
189 for digest in selection.digests:
190 aname, avalue = digest.split('=', 1)
191 assert ':' not in aname
192 manifest_digest.setAttribute(aname, avalue)
193 selection_elem.appendChild(manifest_digest)
195 for b in selection.bindings:
196 selection_elem.appendChild(b._toxml(doc))
198 for dep in selection.dependencies:
199 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
200 dep_elem.setAttributeNS(None, 'interface', dep.interface)
201 selection_elem.appendChild(dep_elem)
203 for m in dep.metadata:
204 parts = m.split(' ', 1)
205 if len(parts) == 1:
206 ns = None
207 localName = parts[0]
208 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
209 else:
210 ns, localName = parts
211 dep_elem.setAttributeNS(ns, ensure_prefix(prefixes, ns) + ':' + localName, dep.metadata[m])
213 for b in dep.bindings:
214 dep_elem.appendChild(b._toxml(doc))
216 for ns, prefix in prefixes.items():
217 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
219 return doc
221 def __repr__(self):
222 return "Selections for " + self.interface
224 def download_missing(self, iface_cache, fetcher):
225 """Check all selected implementations are available.
226 Download any that are not present.
227 Note: package implementations (distribution packages) are ignored.
228 @param iface_cache: cache to find feeds with download information
229 @param fetcher: used to download missing implementations
230 @return: a L{tasks.Blocker} or None"""
231 from zeroinstall.zerostore import NotStored
233 # Check that every required selection is cached
234 needed_downloads = []
235 for sel in self.selections.values():
236 if (not sel.local_path) and (not sel.id.startswith('package:')):
237 try:
238 iface_cache.stores.lookup_any(sel.digests)
239 except NotStored, ex:
240 needed_downloads.append(sel)
241 if not needed_downloads:
242 return
244 @tasks.async
245 def download():
246 # We're missing some. For each one, get the feed it came from
247 # and find the corresponding <implementation> in that. This will
248 # tell us where to get it from.
249 # Note: we look for an implementation with the same ID. Maybe we
250 # should check it has the same digest(s) too?
251 needed_impls = []
252 for sel in needed_downloads:
253 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
254 feed = iface_cache.get_feed(feed_url)
255 if feed is None or sel.id not in feed.implementations:
256 fetch_feed = fetcher.download_and_import_feed(feed_url, iface_cache)
257 yield fetch_feed
258 tasks.check(fetch_feed)
260 feed = iface_cache.get_feed(feed_url)
261 assert feed, "Failed to get feed for %s" % feed_url
262 impl = feed.implementations[sel.id]
263 needed_impls.append(impl)
265 fetch_impls = fetcher.download_impls(needed_impls, iface_cache.stores)
266 yield fetch_impls
267 tasks.check(fetch_impls)
268 return download()