More Python 3 support
[zeroinstall/solver.git] / zeroinstall / injector / selections.py
blobd4c5782f389519da88d084394aa83b6c06a77e56
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 _
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('%s=%s' % (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 = digest.split('=', 1)
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 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
301 dep_elem.setAttributeNS(None, 'interface', dep.interface)
302 selection_elem.appendChild(dep_elem)
304 for m in dep.metadata:
305 parts = m.split(' ', 1)
306 if len(parts) == 1:
307 ns = None
308 localName = parts[0]
309 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
310 else:
311 ns, localName = parts
312 prefixes.setAttributeNS(dep_elem, ns, localName, dep.metadata[m])
314 for b in dep.bindings:
315 dep_elem.appendChild(b._toxml(doc, prefixes))
317 for command in selection.get_commands().values():
318 selection_elem.appendChild(command._toxml(doc, prefixes))
320 for ns, prefix in prefixes.prefixes.items():
321 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
323 return doc
325 def __repr__(self):
326 return "Selections for " + self.interface
328 def download_missing(self, config, _old = None, include_packages = False):
329 """Check all selected implementations are available.
330 Download any that are not present. Since native distribution packages are usually
331 only available in a single version, which is unlikely to be the one in the
332 selections document, we ignore them by default.
333 Note: package implementations (distribution packages) are ignored.
334 @param config: used to get iface_cache, stores and fetcher
335 @param include_packages: also try to install native packages (since 1.5)
336 @return: a L{tasks.Blocker} or None"""
337 if _old:
338 config = get_deprecated_singleton_config()
340 iface_cache = config.iface_cache
341 stores = config.stores
343 # Check that every required selection is cached
344 def needs_download(sel):
345 if sel.id.startswith('package:'):
346 if not include_packages: return False
347 feed = iface_cache.get_feed(sel.feed)
348 if not feed: return False
349 impl = feed.implementations.get(sel.id, None)
350 return impl is None or not impl.installed
351 elif sel.local_path:
352 return False
353 else:
354 return sel.get_path(stores, missing_ok = True) is None
356 needed_downloads = list(filter(needs_download, self.selections.values()))
357 if not needed_downloads:
358 return
360 if config.network_use == model.network_offline:
361 from zeroinstall import NeedDownload
362 raise NeedDownload(', '.join([str(x) for x in needed_downloads]))
364 @tasks.async
365 def download():
366 # We're missing some. For each one, get the feed it came from
367 # and find the corresponding <implementation> in that. This will
368 # tell us where to get it from.
369 # Note: we look for an implementation with the same ID. Maybe we
370 # should check it has the same digest(s) too?
371 needed_impls = []
372 for sel in needed_downloads:
373 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
374 feed = iface_cache.get_feed(feed_url)
375 if feed is None or sel.id not in feed.implementations:
376 fetch_feed = config.fetcher.download_and_import_feed(feed_url, iface_cache)
377 yield fetch_feed
378 tasks.check(fetch_feed)
380 feed = iface_cache.get_feed(feed_url)
381 assert feed, "Failed to get feed for %s" % feed_url
382 impl = feed.implementations[sel.id]
383 needed_impls.append(impl)
385 fetch_impls = config.fetcher.download_impls(needed_impls, stores)
386 yield fetch_impls
387 tasks.check(fetch_impls)
388 return download()
390 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
392 def __getitem__(self, key):
393 # Deprecated
394 if isinstance(key, basestring):
395 return self.selections[key]
396 sel = self.selections[key.uri]
397 return sel and sel.impl
399 def iteritems(self):
400 # Deprecated
401 iface_cache = get_deprecated_singleton_config().iface_cache
402 for (uri, sel) in self.selections.items():
403 yield (iface_cache.get_interface(uri), sel and sel.impl)
405 def values(self):
406 # Deprecated
407 for (uri, sel) in self.selections.items():
408 yield sel and sel.impl
410 def __iter__(self):
411 # Deprecated
412 iface_cache = get_deprecated_singleton_config().iface_cache
413 for (uri, sel) in self.selections.items():
414 yield iface_cache.get_interface(uri)
416 def get(self, iface, if_missing):
417 # Deprecated
418 sel = self.selections.get(iface.uri, None)
419 if sel:
420 return sel.impl
421 return if_missing
423 def copy(self):
424 # Deprecated
425 s = Selections(None)
426 s.interface = self.interface
427 s.selections = self.selections.copy()
428 return s
430 def items(self):
431 # Deprecated
432 return list(self.iteritems())
434 @property
435 def commands(self):
436 i = self.interface
437 c = self.command
438 commands = []
439 while c is not None:
440 sel = self.selections[i]
441 command = sel.get_command(c)
443 commands.append(command)
445 runner = command.get_runner()
446 if not runner:
447 break
449 i = runner.metadata['interface']
450 c = runner.qdom.attrs.get('command', 'run')
452 return commands