Added Implementation.is_available and Selection.is_available
[zeroinstall.git] / zeroinstall / injector / selections.py
blobd5cf11101bf9f713326429e9c3024e78c3d334df
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 import model
11 from zeroinstall.injector.policy import Policy, get_deprecated_singleton_config
12 from zeroinstall.injector.model import process_binding, process_depends, binding_names, Command
13 from zeroinstall.injector.namespaces import XMLNS_IFACE
14 from zeroinstall.injector.qdom import Element, Prefixes
15 from zeroinstall.support import tasks
17 class Selection(object):
18 """A single selected implementation in a L{Selections} set.
19 @ivar dependencies: list of dependencies
20 @type dependencies: [L{model.Dependency}]
21 @ivar attrs: XML attributes map (name is in the format "{namespace} {localName}")
22 @type attrs: {str: str}
23 @ivar digests: a list of manifest digests
24 @type digests: [str]
25 @ivar version: the implementation's version number
26 @type version: str"""
28 interface = property(lambda self: self.attrs['interface'])
29 id = property(lambda self: self.attrs['id'])
30 version = property(lambda self: self.attrs['version'])
31 feed = property(lambda self: self.attrs.get('from-feed', self.interface))
32 main = property(lambda self: self.attrs.get('main', None))
34 @property
35 def local_path(self):
36 local_path = self.attrs.get('local-path', None)
37 if local_path:
38 return local_path
39 if self.id.startswith('/'):
40 return self.id
41 return None
43 def __repr__(self):
44 return self.id
46 def is_available(self, stores):
47 """@since 0.53"""
48 path = self.local_path
49 if path is not None:
50 return os.path.exists(path)
51 path = stores.lookup_maybe(self.digests)
52 return path is not None
54 class ImplSelection(Selection):
55 __slots__ = ['impl', 'dependencies', 'attrs']
57 def __init__(self, iface_uri, impl, dependencies):
58 assert impl
59 self.impl = impl
60 self.dependencies = dependencies
62 attrs = impl.metadata.copy()
63 attrs['id'] = impl.id
64 attrs['version'] = impl.get_version()
65 attrs['interface'] = iface_uri
66 attrs['from-feed'] = impl.feed.url
67 if impl.local_path:
68 attrs['local-path'] = impl.local_path
69 self.attrs = attrs
71 @property
72 def bindings(self): return self.impl.bindings
74 @property
75 def digests(self): return self.impl.digests
77 class XMLSelection(Selection):
78 __slots__ = ['bindings', 'dependencies', 'attrs', 'digests']
80 def __init__(self, dependencies, bindings = None, attrs = None, digests = None):
81 if bindings is None: bindings = []
82 if digests is None: digests = []
83 self.dependencies = dependencies
84 self.bindings = bindings
85 self.attrs = attrs
86 self.digests = digests
88 assert self.interface
89 assert self.id
90 assert self.version
91 assert self.feed
93 class Selections(object):
94 """
95 A selected set of components which will make up a complete program.
96 @ivar interface: the interface of the program
97 @type interface: str
98 @ivar commands: how to run this selection (will contain more than one item if runners are used)
99 @type commands: [{L{Command}}]
100 @ivar selections: the selected implementations
101 @type selections: {str: L{Selection}}
103 __slots__ = ['interface', 'selections', 'commands']
105 def __init__(self, source):
106 """Constructor.
107 @param source: a map of implementations, policy or selections document
108 @type source: {str: L{Selection}} | L{Policy} | L{Element}
110 self.selections = {}
112 if source is None:
113 self.commands = []
114 # (Solver will fill everything in)
115 elif isinstance(source, Policy):
116 self._init_from_policy(source)
117 elif isinstance(source, Element):
118 self._init_from_qdom(source)
119 else:
120 raise Exception(_("Source not a Policy or qdom.Element!"))
122 def _init_from_policy(self, policy):
123 """Set the selections from a policy.
124 @deprecated: use Solver.selections instead
125 @param policy: the policy giving the selected implementations."""
126 self.interface = policy.root
127 self.selections = policy.solver.selections.selections
128 self.commands = policy.solver.selections.commands
130 def _init_from_qdom(self, root):
131 """Parse and load a selections document.
132 @param root: a saved set of selections."""
133 self.interface = root.getAttribute('interface')
134 assert self.interface
135 self.commands = []
137 for selection in root.childNodes:
138 if selection.uri != XMLNS_IFACE:
139 continue
140 if selection.name != 'selection':
141 if selection.name == 'command':
142 self.commands.append(Command(selection, None))
143 continue
145 requires = []
146 bindings = []
147 digests = []
148 for dep_elem in selection.childNodes:
149 if dep_elem.uri != XMLNS_IFACE:
150 continue
151 if dep_elem.name in binding_names:
152 bindings.append(process_binding(dep_elem))
153 elif dep_elem.name == 'requires':
154 dep = process_depends(dep_elem, None)
155 requires.append(dep)
156 elif dep_elem.name == 'manifest-digest':
157 for aname, avalue in dep_elem.attrs.iteritems():
158 digests.append('%s=%s' % (aname, avalue))
160 # For backwards compatibility, allow getting the digest from the ID
161 sel_id = selection.attrs['id']
162 local_path = selection.attrs.get("local-path", None)
163 if (not digests and not local_path) and '=' in sel_id:
164 alg = sel_id.split('=', 1)[0]
165 if alg in ('sha1', 'sha1new', 'sha256'):
166 digests.append(sel_id)
168 iface_uri = selection.attrs['interface']
170 s = XMLSelection(requires, bindings, selection.attrs, digests)
171 self.selections[iface_uri] = s
173 if not self.commands:
174 # Old-style selections document; use the main attribute
175 if iface_uri == self.interface:
176 root_sel = self.selections[self.interface]
177 main = root_sel.attrs.get('main', None)
178 if main is not None:
179 self.commands = [Command(Element(XMLNS_IFACE, 'command', {'path': main}), None)]
181 def toDOM(self):
182 """Create a DOM document for the selected implementations.
183 The document gives the URI of the root, plus each selected implementation.
184 For each selected implementation, we record the ID, the version, the URI and
185 (if different) the feed URL. We also record all the bindings needed.
186 @return: a new DOM Document"""
187 from xml.dom import minidom, XMLNS_NAMESPACE
189 assert self.interface
191 impl = minidom.getDOMImplementation()
193 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
195 root = doc.documentElement
196 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
198 root.setAttributeNS(None, 'interface', self.interface)
200 prefixes = Prefixes()
202 for iface, selection in sorted(self.selections.items()):
203 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
204 selection_elem.setAttributeNS(None, 'interface', selection.interface)
205 root.appendChild(selection_elem)
207 for name, value in selection.attrs.iteritems():
208 if ' ' in name:
209 ns, localName = name.split(' ', 1)
210 selection_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, value)
211 elif name == 'from-feed':
212 # Don't bother writing from-feed attr if it's the same as the interface
213 if value != selection.attrs['interface']:
214 selection_elem.setAttributeNS(None, name, value)
215 elif name not in ('main', 'self-test'): # (replaced by <command>)
216 selection_elem.setAttributeNS(None, name, value)
218 if selection.digests:
219 manifest_digest = doc.createElementNS(XMLNS_IFACE, 'manifest-digest')
220 for digest in selection.digests:
221 aname, avalue = digest.split('=', 1)
222 assert ':' not in aname
223 manifest_digest.setAttribute(aname, avalue)
224 selection_elem.appendChild(manifest_digest)
226 for b in selection.bindings:
227 selection_elem.appendChild(b._toxml(doc))
229 for dep in selection.dependencies:
230 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
231 dep_elem.setAttributeNS(None, 'interface', dep.interface)
232 selection_elem.appendChild(dep_elem)
234 for m in dep.metadata:
235 parts = m.split(' ', 1)
236 if len(parts) == 1:
237 ns = None
238 localName = parts[0]
239 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
240 else:
241 ns, localName = parts
242 dep_elem.setAttributeNS(ns, prefixes.get(ns) + ':' + localName, dep.metadata[m])
244 for b in dep.bindings:
245 dep_elem.appendChild(b._toxml(doc))
247 for command in self.commands:
248 root.appendChild(command._toxml(doc, prefixes))
250 for ns, prefix in prefixes.prefixes.items():
251 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
253 return doc
255 def __repr__(self):
256 return "Selections for " + self.interface
258 def download_missing(self, config, _old = None):
259 """Check all selected implementations are available.
260 Download any that are not present.
261 Note: package implementations (distribution packages) are ignored.
262 @param config: used to get iface_cache, stores and fetcher
263 @return: a L{tasks.Blocker} or None"""
264 from zeroinstall.zerostore import NotStored
266 if _old:
267 config = get_deprecated_singleton_config()
269 iface_cache = config.iface_cache
270 stores = config.stores
272 # Check that every required selection is cached
273 needed_downloads = []
274 for sel in self.selections.values():
275 if (not sel.local_path) and (not sel.id.startswith('package:')):
276 try:
277 stores.lookup_any(sel.digests)
278 except NotStored:
279 needed_downloads.append(sel)
280 if not needed_downloads:
281 return
283 if config.network_use == model.network_offline:
284 from zeroinstall import NeedDownload
285 raise NeedDownload(', '.join([str(x) for x in needed_downloads]))
287 @tasks.async
288 def download():
289 # We're missing some. For each one, get the feed it came from
290 # and find the corresponding <implementation> in that. This will
291 # tell us where to get it from.
292 # Note: we look for an implementation with the same ID. Maybe we
293 # should check it has the same digest(s) too?
294 needed_impls = []
295 for sel in needed_downloads:
296 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
297 feed = iface_cache.get_feed(feed_url)
298 if feed is None or sel.id not in feed.implementations:
299 fetch_feed = config.fetcher.download_and_import_feed(feed_url, iface_cache)
300 yield fetch_feed
301 tasks.check(fetch_feed)
303 feed = iface_cache.get_feed(feed_url)
304 assert feed, "Failed to get feed for %s" % feed_url
305 impl = feed.implementations[sel.id]
306 needed_impls.append(impl)
308 fetch_impls = config.fetcher.download_impls(needed_impls, stores)
309 yield fetch_impls
310 tasks.check(fetch_impls)
311 return download()
313 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
315 def __getitem__(self, key):
316 # Deprecated
317 if isinstance(key, basestring):
318 return self.selections[key]
319 sel = self.selections[key.uri]
320 return sel and sel.impl
322 def iteritems(self):
323 # Deprecated
324 iface_cache = get_deprecated_singleton_config().iface_cache
325 for (uri, sel) in self.selections.iteritems():
326 yield (iface_cache.get_interface(uri), sel and sel.impl)
328 def values(self):
329 # Deprecated
330 for (uri, sel) in self.selections.iteritems():
331 yield sel and sel.impl
333 def __iter__(self):
334 # Deprecated
335 iface_cache = get_deprecated_singleton_config().iface_cache
336 for (uri, sel) in self.selections.iteritems():
337 yield iface_cache.get_interface(uri)
339 def get(self, iface, if_missing):
340 # Deprecated
341 sel = self.selections.get(iface.uri, None)
342 if sel:
343 return sel.impl
344 return if_missing
346 def copy(self):
347 # Deprecated
348 s = Selections(None)
349 s.interface = self.interface
350 s.selections = self.selections.copy()
351 return s
353 def items(self):
354 # Deprecated
355 return list(self.iteritems())