Added sha256new algorithm
[zeroinstall/solver.git] / zeroinstall / injector / selections.py
blob3327f2f82e1da909b443e6931a2ef6880f763d40
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 import os
10 from zeroinstall import _, zerostore
11 from zeroinstall.injector import model
12 from zeroinstall.injector.policy import get_deprecated_singleton_config
13 from zeroinstall.injector.model import process_binding, process_depends, binding_names, Command
14 from zeroinstall.injector.namespaces import XMLNS_IFACE
15 from zeroinstall.injector.qdom import Element, Prefixes
16 from zeroinstall.support import tasks, basestring
18 class Selection(object):
19 """A single selected implementation in a L{Selections} set.
20 @ivar dependencies: list of dependencies
21 @type dependencies: [L{model.Dependency}]
22 @ivar attrs: XML attributes map (name is in the format "{namespace} {localName}")
23 @type attrs: {str: 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 def is_available(self, stores):
46 """Is this implementation available locally?
47 (a local implementation or a cached ZeroInstallImplementation)
48 @rtype: bool
49 @since: 0.53"""
50 path = self.local_path
51 if path is not None:
52 return os.path.exists(path)
53 path = stores.lookup_maybe(self.digests)
54 return path is not None
56 def get_path(self, stores, missing_ok = False):
57 """Return the root directory of this implementation.
58 For local implementations, this is L{local_path}.
59 For cached implementations, this is the directory in the cache.
60 @param stores: stores to search
61 @type stores: L{zerostore.Stores}
62 @param missing_ok: return None for uncached implementations
63 @type missing_ok: bool
64 @return: the path of the directory
65 @rtype: str | None
66 @since: 1.8"""
67 if self.local_path is not None:
68 return self.local_path
69 if not self.digests:
70 # (for now, we assume this is always an error, even for missing_ok)
71 raise model.SafeException("No digests for {feed} {version}".format(feed = self.feed, version = self.version))
72 if missing_ok:
73 return stores.lookup_maybe(self.digests)
74 else:
75 return stores.lookup_any(self.digests)
77 class ImplSelection(Selection):
78 """A Selection created from an Implementation"""
80 __slots__ = ['impl', 'dependencies', 'attrs', '_used_commands']
82 def __init__(self, iface_uri, impl, dependencies):
83 assert impl
84 self.impl = impl
85 self.dependencies = dependencies
86 self._used_commands = set()
88 attrs = impl.metadata.copy()
89 attrs['id'] = impl.id
90 attrs['version'] = impl.get_version()
91 attrs['interface'] = iface_uri
92 attrs['from-feed'] = impl.feed.url
93 if impl.local_path:
94 attrs['local-path'] = impl.local_path
95 self.attrs = attrs
97 @property
98 def bindings(self): return self.impl.bindings
100 @property
101 def digests(self): return self.impl.digests
103 def get_command(self, name):
104 assert name in self._used_commands, "internal error: '{command}' not in my commands list".format(command = name)
105 return self.impl.commands[name]
107 def get_commands(self):
108 commands = {}
109 for c in self._used_commands:
110 commands[c] = self.impl.commands[c]
111 return commands
113 class XMLSelection(Selection):
114 """A Selection created by reading an XML selections document.
115 @ivar digests: a list of manifest digests
116 @type digests: [str]
118 __slots__ = ['bindings', 'dependencies', 'attrs', 'digests', 'commands']
120 def __init__(self, dependencies, bindings = None, attrs = None, digests = None, commands = None):
121 if bindings is None: bindings = []
122 if digests is None: digests = []
123 self.dependencies = dependencies
124 self.bindings = bindings
125 self.attrs = attrs
126 self.digests = digests
127 self.commands = commands
129 assert self.interface
130 assert self.id
131 assert self.version
132 assert self.feed
134 def get_command(self, name):
135 if name not in self.commands:
136 raise model.SafeException("Command '{name}' not present in selections for {iface}".format(name = name, iface = self.interface))
137 return self.commands[name]
139 def get_commands(self):
140 return self.commands
142 class Selections(object):
144 A selected set of components which will make up a complete program.
145 @ivar interface: the interface of the program
146 @type interface: str
147 @ivar command: the command to run on 'interface'
148 @type command: str
149 @ivar selections: the selected implementations
150 @type selections: {str: L{Selection}}
152 __slots__ = ['interface', 'selections', 'command']
154 def __init__(self, source):
155 """Constructor.
156 @param source: a map of implementations, policy or selections document
157 @type source: L{Element}
159 self.selections = {}
160 self.command = None
162 if source is None:
163 # (Solver will fill everything in)
164 pass
165 elif isinstance(source, Element):
166 self._init_from_qdom(source)
167 else:
168 raise Exception(_("Source not a qdom.Element!"))
170 def _init_from_qdom(self, root):
171 """Parse and load a selections document.
172 @param root: a saved set of selections."""
173 self.interface = root.getAttribute('interface')
174 self.command = root.getAttribute('command')
175 assert self.interface
176 old_commands = []
178 for selection in root.childNodes:
179 if selection.uri != XMLNS_IFACE:
180 continue
181 if selection.name != 'selection':
182 if selection.name == 'command':
183 old_commands.append(Command(selection, None))
184 continue
186 requires = []
187 bindings = []
188 digests = []
189 commands = {}
190 for elem in selection.childNodes:
191 if elem.uri != XMLNS_IFACE:
192 continue
193 if elem.name in binding_names:
194 bindings.append(process_binding(elem))
195 elif elem.name == 'requires':
196 dep = process_depends(elem, None)
197 requires.append(dep)
198 elif elem.name == 'manifest-digest':
199 for aname, avalue in elem.attrs.items():
200 digests.append(zerostore.format_algorithm_digest_pair(aname, avalue))
201 elif elem.name == 'command':
202 name = elem.getAttribute('name')
203 assert name, "Missing name attribute on <command>"
204 commands[name] = Command(elem, None)
206 # For backwards compatibility, allow getting the digest from the ID
207 sel_id = selection.attrs['id']
208 local_path = selection.attrs.get("local-path", None)
209 if (not digests and not local_path) and '=' in sel_id:
210 alg = sel_id.split('=', 1)[0]
211 if alg in ('sha1', 'sha1new', 'sha256'):
212 digests.append(sel_id)
214 iface_uri = selection.attrs['interface']
216 s = XMLSelection(requires, bindings, selection.attrs, digests, commands)
217 self.selections[iface_uri] = s
219 if self.command is None:
220 # Old style selections document
221 if old_commands:
222 # 0launch 0.52 to 1.1
223 self.command = 'run'
224 iface = self.interface
226 for command in old_commands:
227 command.qdom.attrs['name'] = 'run'
228 self.selections[iface].commands['run'] = command
229 runner = command.get_runner()
230 if runner:
231 iface = runner.interface
232 else:
233 iface = None
234 else:
235 # 0launch < 0.51
236 root_sel = self.selections[self.interface]
237 main = root_sel.attrs.get('main', None)
238 if main is not None:
239 root_sel.commands['run'] = Command(Element(XMLNS_IFACE, 'command', {'path': main, 'name': 'run'}), None)
240 self.command = 'run'
242 elif self.command == '':
243 # New style, but no command requested
244 self.command = None
245 assert not old_commands, "<command> list in new-style selections document!"
247 def toDOM(self):
248 """Create a DOM document for the selected implementations.
249 The document gives the URI of the root, plus each selected implementation.
250 For each selected implementation, we record the ID, the version, the URI and
251 (if different) the feed URL. We also record all the bindings needed.
252 @return: a new DOM Document"""
253 from xml.dom import minidom, XMLNS_NAMESPACE
255 assert self.interface
257 impl = minidom.getDOMImplementation()
259 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
261 root = doc.documentElement
262 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
264 root.setAttributeNS(None, 'interface', self.interface)
266 root.setAttributeNS(None, 'command', self.command or "")
268 prefixes = Prefixes(XMLNS_IFACE)
270 for iface, selection in sorted(self.selections.items()):
271 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
272 selection_elem.setAttributeNS(None, 'interface', selection.interface)
273 root.appendChild(selection_elem)
275 for name, value in selection.attrs.items():
276 if ' ' in name:
277 ns, localName = name.split(' ', 1)
278 prefixes.setAttributeNS(selection_elem, ns, localName, value)
279 elif name == 'stability':
280 pass
281 elif name == 'from-feed':
282 # Don't bother writing from-feed attr if it's the same as the interface
283 if value != selection.attrs['interface']:
284 selection_elem.setAttributeNS(None, name, value)
285 elif name not in ('main', 'self-test'): # (replaced by <command>)
286 selection_elem.setAttributeNS(None, name, value)
288 if selection.digests:
289 manifest_digest = doc.createElementNS(XMLNS_IFACE, 'manifest-digest')
290 for digest in selection.digests:
291 aname, avalue = zerostore.parse_algorithm_digest_pair(digest)
292 assert ':' not in aname
293 manifest_digest.setAttribute(aname, avalue)
294 selection_elem.appendChild(manifest_digest)
296 for b in selection.bindings:
297 selection_elem.appendChild(b._toxml(doc, prefixes))
299 for dep in selection.dependencies:
300 if not isinstance(dep, model.InterfaceDependency): continue
302 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
303 dep_elem.setAttributeNS(None, 'interface', dep.interface)
304 selection_elem.appendChild(dep_elem)
306 for m in dep.metadata:
307 parts = m.split(' ', 1)
308 if len(parts) == 1:
309 ns = None
310 localName = parts[0]
311 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
312 else:
313 ns, localName = parts
314 prefixes.setAttributeNS(dep_elem, ns, localName, dep.metadata[m])
316 for b in dep.bindings:
317 dep_elem.appendChild(b._toxml(doc, prefixes))
319 for command in selection.get_commands().values():
320 selection_elem.appendChild(command._toxml(doc, prefixes))
322 for ns, prefix in prefixes.prefixes.items():
323 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
325 return doc
327 def __repr__(self):
328 return "Selections for " + self.interface
330 def download_missing(self, config, _old = None, include_packages = False):
331 """Check all selected implementations are available.
332 Download any that are not present. Since native distribution packages are usually
333 only available in a single version, which is unlikely to be the one in the
334 selections document, we ignore them by default.
335 Note: package implementations (distribution packages) are ignored.
336 @param config: used to get iface_cache, stores and fetcher
337 @param include_packages: also try to install native packages (since 1.5)
338 @return: a L{tasks.Blocker} or None"""
339 if _old:
340 config = get_deprecated_singleton_config()
342 iface_cache = config.iface_cache
343 stores = config.stores
345 # Check that every required selection is cached
346 def needs_download(sel):
347 if sel.id.startswith('package:'):
348 if not include_packages: return False
349 feed = iface_cache.get_feed(sel.feed)
350 if not feed: return False
351 impl = feed.implementations.get(sel.id, None)
352 return impl is None or not impl.installed
353 elif sel.local_path:
354 return False
355 else:
356 return sel.get_path(stores, missing_ok = True) is None
358 needed_downloads = list(filter(needs_download, self.selections.values()))
359 if not needed_downloads:
360 return
362 if config.network_use == model.network_offline:
363 from zeroinstall import NeedDownload
364 raise NeedDownload(', '.join([str(x) for x in needed_downloads]))
366 @tasks.async
367 def download():
368 # We're missing some. For each one, get the feed it came from
369 # and find the corresponding <implementation> in that. This will
370 # tell us where to get it from.
371 # Note: we look for an implementation with the same ID. Maybe we
372 # should check it has the same digest(s) too?
373 needed_impls = []
374 for sel in needed_downloads:
375 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
376 feed = iface_cache.get_feed(feed_url)
377 if feed is None or sel.id not in feed.implementations:
378 fetch_feed = config.fetcher.download_and_import_feed(feed_url, iface_cache)
379 yield fetch_feed
380 tasks.check(fetch_feed)
382 feed = iface_cache.get_feed(feed_url)
383 assert feed, "Failed to get feed for %s" % feed_url
384 impl = feed.implementations[sel.id]
385 needed_impls.append(impl)
387 fetch_impls = config.fetcher.download_impls(needed_impls, stores)
388 yield fetch_impls
389 tasks.check(fetch_impls)
390 return download()
392 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
394 def __getitem__(self, key):
395 # Deprecated
396 if isinstance(key, basestring):
397 return self.selections[key]
398 sel = self.selections[key.uri]
399 return sel and sel.impl
401 def iteritems(self):
402 # Deprecated
403 iface_cache = get_deprecated_singleton_config().iface_cache
404 for (uri, sel) in self.selections.items():
405 yield (iface_cache.get_interface(uri), sel and sel.impl)
407 def values(self):
408 # Deprecated
409 for (uri, sel) in self.selections.items():
410 yield sel and sel.impl
412 def __iter__(self):
413 # Deprecated
414 iface_cache = get_deprecated_singleton_config().iface_cache
415 for (uri, sel) in self.selections.items():
416 yield iface_cache.get_interface(uri)
418 def get(self, iface, if_missing):
419 # Deprecated
420 sel = self.selections.get(iface.uri, None)
421 if sel:
422 return sel.impl
423 return if_missing
425 def copy(self):
426 # Deprecated
427 s = Selections(None)
428 s.interface = self.interface
429 s.selections = self.selections.copy()
430 return s
432 def items(self):
433 # Deprecated
434 return list(self.iteritems())
436 @property
437 def commands(self):
438 i = self.interface
439 c = self.command
440 commands = []
441 while c is not None:
442 sel = self.selections[i]
443 command = sel.get_command(c)
445 commands.append(command)
447 runner = command.get_runner()
448 if not runner:
449 break
451 i = runner.metadata['interface']
452 c = runner.qdom.attrs.get('command', 'run')
454 return commands