Added <executable> binding
[zeroinstall/solver.git] / zeroinstall / injector / selections.py
blob3029ad85097837e521d3f8901fb665eb5214d977
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 Policy, 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
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 class ImplSelection(Selection):
57 """A Selection created from an Implementation"""
59 __slots__ = ['impl', 'dependencies', 'attrs', '_used_commands']
61 def __init__(self, iface_uri, impl, dependencies):
62 assert impl
63 self.impl = impl
64 self.dependencies = dependencies
65 self._used_commands = set()
67 attrs = impl.metadata.copy()
68 attrs['id'] = impl.id
69 attrs['version'] = impl.get_version()
70 attrs['interface'] = iface_uri
71 attrs['from-feed'] = impl.feed.url
72 if impl.local_path:
73 attrs['local-path'] = impl.local_path
74 self.attrs = attrs
76 @property
77 def bindings(self): return self.impl.bindings
79 @property
80 def digests(self): return self.impl.digests
82 def get_command(self, name):
83 assert name in self._used_commands
84 return self.impl.commands[name]
86 def get_commands(self):
87 commands = {}
88 for c in self._used_commands:
89 commands[c] = self.impl.commands[c]
90 return commands
92 class XMLSelection(Selection):
93 """A Selection created by reading an XML selections document.
94 @ivar digests: a list of manifest digests
95 @type digests: [str]
96 """
97 __slots__ = ['bindings', 'dependencies', 'attrs', 'digests', 'commands']
99 def __init__(self, dependencies, bindings = None, attrs = None, digests = None, commands = None):
100 if bindings is None: bindings = []
101 if digests is None: digests = []
102 self.dependencies = dependencies
103 self.bindings = bindings
104 self.attrs = attrs
105 self.digests = digests
106 self.commands = commands
108 assert self.interface
109 assert self.id
110 assert self.version
111 assert self.feed
113 def get_command(self, name):
114 return self.commands[name]
116 def get_commands(self):
117 return self.commands
119 class Selections(object):
121 A selected set of components which will make up a complete program.
122 @ivar interface: the interface of the program
123 @type interface: str
124 @ivar command: the command to run on 'interface'
125 @type command: str
126 @ivar selections: the selected implementations
127 @type selections: {str: L{Selection}}
129 __slots__ = ['interface', 'selections', 'command']
131 def __init__(self, source):
132 """Constructor.
133 @param source: a map of implementations, policy or selections document
134 @type source: L{Element}
136 self.selections = {}
137 self.command = None
139 if source is None:
140 # (Solver will fill everything in)
141 pass
142 elif isinstance(source, Policy):
143 import warnings
144 warnings.warn("Use policy.solver.selections instead", DeprecationWarning, 2)
145 self._init_from_policy(source)
146 elif isinstance(source, Element):
147 self._init_from_qdom(source)
148 else:
149 raise Exception(_("Source not a qdom.Element!"))
151 def _init_from_policy(self, policy):
152 """Set the selections from a policy.
153 @deprecated: use Solver.selections instead
154 @param policy: the policy giving the selected implementations."""
155 self.interface = policy.root
156 self.selections = policy.solver.selections.selections
157 self.commands = policy.solver.selections.commands
159 def _init_from_qdom(self, root):
160 """Parse and load a selections document.
161 @param root: a saved set of selections."""
162 self.interface = root.getAttribute('interface')
163 self.command = root.getAttribute('command')
164 assert self.interface
165 old_commands = []
167 for selection in root.childNodes:
168 if selection.uri != XMLNS_IFACE:
169 continue
170 if selection.name != 'selection':
171 if selection.name == 'command':
172 old_commands.append(Command(selection, None))
173 continue
175 requires = []
176 bindings = []
177 digests = []
178 commands = {}
179 for elem in selection.childNodes:
180 if elem.uri != XMLNS_IFACE:
181 continue
182 if elem.name in binding_names:
183 bindings.append(process_binding(elem))
184 elif elem.name == 'requires':
185 dep = process_depends(elem, None)
186 requires.append(dep)
187 elif elem.name == 'manifest-digest':
188 for aname, avalue in elem.attrs.iteritems():
189 digests.append('%s=%s' % (aname, avalue))
190 elif elem.name == 'command':
191 commands[elem.getAttribute('name')] = Command(elem, None)
193 # For backwards compatibility, allow getting the digest from the ID
194 sel_id = selection.attrs['id']
195 local_path = selection.attrs.get("local-path", None)
196 if (not digests and not local_path) and '=' in sel_id:
197 alg = sel_id.split('=', 1)[0]
198 if alg in ('sha1', 'sha1new', 'sha256'):
199 digests.append(sel_id)
201 iface_uri = selection.attrs['interface']
203 s = XMLSelection(requires, bindings, selection.attrs, digests, commands)
204 self.selections[iface_uri] = s
206 if self.command is None:
207 # Old style selections document
208 if old_commands:
209 # 0launch 0.52 to 1.1
210 self.command = 'run'
211 iface = self.interface
212 last_command = None
214 for command in old_commands:
215 command.qdom.attrs['name'] = 'run'
216 self.selections[iface].commands['run'] = command
217 runner = command.get_runner()
218 if runner:
219 iface = runner.interface
220 else:
221 iface = None
222 else:
223 # 0launch < 0.51
224 root_sel = self.selections[self.interface]
225 main = root_sel.attrs.get('main', None)
226 if main is not None:
227 root_sel.commands['run'] = Command(Element(XMLNS_IFACE, 'command', {'path': main}), None)
228 self.command = 'run'
230 elif self.command == '':
231 # New style, but no command requested
232 self.command = None
233 assert not old_commands, "<command> list in new-style selections document!"
235 def toDOM(self):
236 """Create a DOM document for the selected implementations.
237 The document gives the URI of the root, plus each selected implementation.
238 For each selected implementation, we record the ID, the version, the URI and
239 (if different) the feed URL. We also record all the bindings needed.
240 @return: a new DOM Document"""
241 from xml.dom import minidom, XMLNS_NAMESPACE
243 assert self.interface
245 impl = minidom.getDOMImplementation()
247 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
249 root = doc.documentElement
250 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
252 root.setAttributeNS(None, 'interface', self.interface)
254 root.setAttributeNS(None, 'command', self.command or "")
256 prefixes = Prefixes(XMLNS_IFACE)
258 for iface, selection in sorted(self.selections.items()):
259 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
260 selection_elem.setAttributeNS(None, 'interface', selection.interface)
261 root.appendChild(selection_elem)
263 for name, value in selection.attrs.iteritems():
264 if ' ' in name:
265 ns, localName = name.split(' ', 1)
266 prefixes.setAttributeNS(selection_elem, ns, localName, value)
267 elif name == 'stability':
268 pass
269 elif name == 'from-feed':
270 # Don't bother writing from-feed attr if it's the same as the interface
271 if value != selection.attrs['interface']:
272 selection_elem.setAttributeNS(None, name, value)
273 elif name not in ('main', 'self-test'): # (replaced by <command>)
274 selection_elem.setAttributeNS(None, name, value)
276 if selection.digests:
277 manifest_digest = doc.createElementNS(XMLNS_IFACE, 'manifest-digest')
278 for digest in selection.digests:
279 aname, avalue = digest.split('=', 1)
280 assert ':' not in aname
281 manifest_digest.setAttribute(aname, avalue)
282 selection_elem.appendChild(manifest_digest)
284 for b in selection.bindings:
285 selection_elem.appendChild(b._toxml(doc, prefixes))
287 for dep in selection.dependencies:
288 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
289 dep_elem.setAttributeNS(None, 'interface', dep.interface)
290 selection_elem.appendChild(dep_elem)
292 for m in dep.metadata:
293 parts = m.split(' ', 1)
294 if len(parts) == 1:
295 ns = None
296 localName = parts[0]
297 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
298 else:
299 ns, localName = parts
300 prefixes.setAttributeNS(dep_elem, ns, localName, dep.metadata[m])
302 for b in dep.bindings:
303 dep_elem.appendChild(b._toxml(doc, prefixes))
305 for command in selection.get_commands().values():
306 selection_elem.appendChild(command._toxml(doc, prefixes))
308 for ns, prefix in prefixes.prefixes.items():
309 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
311 return doc
313 def __repr__(self):
314 return "Selections for " + self.interface
316 def download_missing(self, config, _old = None):
317 """Check all selected implementations are available.
318 Download any that are not present.
319 Note: package implementations (distribution packages) are ignored.
320 @param config: used to get iface_cache, stores and fetcher
321 @return: a L{tasks.Blocker} or None"""
322 from zeroinstall.zerostore import NotStored
324 if _old:
325 config = get_deprecated_singleton_config()
327 iface_cache = config.iface_cache
328 stores = config.stores
330 # Check that every required selection is cached
331 needed_downloads = []
332 for sel in self.selections.values():
333 if (not sel.local_path) and (not sel.id.startswith('package:')):
334 try:
335 stores.lookup_any(sel.digests)
336 except NotStored:
337 needed_downloads.append(sel)
338 if not needed_downloads:
339 return
341 if config.network_use == model.network_offline:
342 from zeroinstall import NeedDownload
343 raise NeedDownload(', '.join([str(x) for x in needed_downloads]))
345 @tasks.async
346 def download():
347 # We're missing some. For each one, get the feed it came from
348 # and find the corresponding <implementation> in that. This will
349 # tell us where to get it from.
350 # Note: we look for an implementation with the same ID. Maybe we
351 # should check it has the same digest(s) too?
352 needed_impls = []
353 for sel in needed_downloads:
354 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
355 feed = iface_cache.get_feed(feed_url)
356 if feed is None or sel.id not in feed.implementations:
357 fetch_feed = config.fetcher.download_and_import_feed(feed_url, iface_cache)
358 yield fetch_feed
359 tasks.check(fetch_feed)
361 feed = iface_cache.get_feed(feed_url)
362 assert feed, "Failed to get feed for %s" % feed_url
363 impl = feed.implementations[sel.id]
364 needed_impls.append(impl)
366 fetch_impls = config.fetcher.download_impls(needed_impls, stores)
367 yield fetch_impls
368 tasks.check(fetch_impls)
369 return download()
371 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
373 def __getitem__(self, key):
374 # Deprecated
375 if isinstance(key, basestring):
376 return self.selections[key]
377 sel = self.selections[key.uri]
378 return sel and sel.impl
380 def iteritems(self):
381 # Deprecated
382 iface_cache = get_deprecated_singleton_config().iface_cache
383 for (uri, sel) in self.selections.iteritems():
384 yield (iface_cache.get_interface(uri), sel and sel.impl)
386 def values(self):
387 # Deprecated
388 for (uri, sel) in self.selections.iteritems():
389 yield sel and sel.impl
391 def __iter__(self):
392 # Deprecated
393 iface_cache = get_deprecated_singleton_config().iface_cache
394 for (uri, sel) in self.selections.iteritems():
395 yield iface_cache.get_interface(uri)
397 def get(self, iface, if_missing):
398 # Deprecated
399 sel = self.selections.get(iface.uri, None)
400 if sel:
401 return sel.impl
402 return if_missing
404 def copy(self):
405 # Deprecated
406 s = Selections(None)
407 s.interface = self.interface
408 s.selections = self.selections.copy()
409 return s
411 def items(self):
412 # Deprecated
413 return list(self.iteritems())
415 @property
416 def commands(self):
417 i = self.interface
418 c = self.command
419 commands = []
420 while c is not None:
421 sel = self.selections[i]
422 command = sel.get_command(c)
424 commands.append(command)
426 runner = command.get_runner()
427 if not runner:
428 break
430 i = runner.metadata['interface']
431 c = runner.qdom.attrs.get('command', 'run')
433 return commands