Fixed bug where a command depends on another command
[zeroinstall.git] / zeroinstall / injector / selections.py
blobf1d73a26c3cf83e7b898a7135aeed0d29e79ef71
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, "internal error: '{command}' not in my commands list".format(command = name)
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 if name not in self.commands:
115 raise model.SafeException("Command '{name}' not present in selections for {iface}".format(name = name, iface = self.interface))
116 return self.commands[name]
118 def get_commands(self):
119 return self.commands
121 class Selections(object):
123 A selected set of components which will make up a complete program.
124 @ivar interface: the interface of the program
125 @type interface: str
126 @ivar command: the command to run on 'interface'
127 @type command: str
128 @ivar selections: the selected implementations
129 @type selections: {str: L{Selection}}
131 __slots__ = ['interface', 'selections', 'command']
133 def __init__(self, source):
134 """Constructor.
135 @param source: a map of implementations, policy or selections document
136 @type source: L{Element}
138 self.selections = {}
139 self.command = None
141 if source is None:
142 # (Solver will fill everything in)
143 pass
144 elif isinstance(source, Policy):
145 import warnings
146 warnings.warn("Use policy.solver.selections instead", DeprecationWarning, 2)
147 self._init_from_policy(source)
148 elif isinstance(source, Element):
149 self._init_from_qdom(source)
150 else:
151 raise Exception(_("Source not a qdom.Element!"))
153 def _init_from_policy(self, policy):
154 """Set the selections from a policy.
155 @deprecated: use Solver.selections instead
156 @param policy: the policy giving the selected implementations."""
157 self.interface = policy.root
158 self.selections = policy.solver.selections.selections
159 self.commands = policy.solver.selections.commands
161 def _init_from_qdom(self, root):
162 """Parse and load a selections document.
163 @param root: a saved set of selections."""
164 self.interface = root.getAttribute('interface')
165 self.command = root.getAttribute('command')
166 assert self.interface
167 old_commands = []
169 for selection in root.childNodes:
170 if selection.uri != XMLNS_IFACE:
171 continue
172 if selection.name != 'selection':
173 if selection.name == 'command':
174 old_commands.append(Command(selection, None))
175 continue
177 requires = []
178 bindings = []
179 digests = []
180 commands = {}
181 for elem in selection.childNodes:
182 if elem.uri != XMLNS_IFACE:
183 continue
184 if elem.name in binding_names:
185 bindings.append(process_binding(elem))
186 elif elem.name == 'requires':
187 dep = process_depends(elem, None)
188 requires.append(dep)
189 elif elem.name == 'manifest-digest':
190 for aname, avalue in elem.attrs.iteritems():
191 digests.append('%s=%s' % (aname, avalue))
192 elif elem.name == 'command':
193 name = elem.getAttribute('name')
194 assert name, "Missing name attribute on <command>"
195 commands[name] = Command(elem, None)
197 # For backwards compatibility, allow getting the digest from the ID
198 sel_id = selection.attrs['id']
199 local_path = selection.attrs.get("local-path", None)
200 if (not digests and not local_path) and '=' in sel_id:
201 alg = sel_id.split('=', 1)[0]
202 if alg in ('sha1', 'sha1new', 'sha256'):
203 digests.append(sel_id)
205 iface_uri = selection.attrs['interface']
207 s = XMLSelection(requires, bindings, selection.attrs, digests, commands)
208 self.selections[iface_uri] = s
210 if self.command is None:
211 # Old style selections document
212 if old_commands:
213 # 0launch 0.52 to 1.1
214 self.command = 'run'
215 iface = self.interface
216 last_command = None
218 for command in old_commands:
219 command.qdom.attrs['name'] = 'run'
220 self.selections[iface].commands['run'] = command
221 runner = command.get_runner()
222 if runner:
223 iface = runner.interface
224 else:
225 iface = None
226 else:
227 # 0launch < 0.51
228 root_sel = self.selections[self.interface]
229 main = root_sel.attrs.get('main', None)
230 if main is not None:
231 root_sel.commands['run'] = Command(Element(XMLNS_IFACE, 'command', {'path': main, 'name': 'run'}), None)
232 self.command = 'run'
234 elif self.command == '':
235 # New style, but no command requested
236 self.command = None
237 assert not old_commands, "<command> list in new-style selections document!"
239 def toDOM(self):
240 """Create a DOM document for the selected implementations.
241 The document gives the URI of the root, plus each selected implementation.
242 For each selected implementation, we record the ID, the version, the URI and
243 (if different) the feed URL. We also record all the bindings needed.
244 @return: a new DOM Document"""
245 from xml.dom import minidom, XMLNS_NAMESPACE
247 assert self.interface
249 impl = minidom.getDOMImplementation()
251 doc = impl.createDocument(XMLNS_IFACE, "selections", None)
253 root = doc.documentElement
254 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', XMLNS_IFACE)
256 root.setAttributeNS(None, 'interface', self.interface)
258 root.setAttributeNS(None, 'command', self.command or "")
260 prefixes = Prefixes(XMLNS_IFACE)
262 for iface, selection in sorted(self.selections.items()):
263 selection_elem = doc.createElementNS(XMLNS_IFACE, 'selection')
264 selection_elem.setAttributeNS(None, 'interface', selection.interface)
265 root.appendChild(selection_elem)
267 for name, value in selection.attrs.iteritems():
268 if ' ' in name:
269 ns, localName = name.split(' ', 1)
270 prefixes.setAttributeNS(selection_elem, ns, localName, value)
271 elif name == 'stability':
272 pass
273 elif name == 'from-feed':
274 # Don't bother writing from-feed attr if it's the same as the interface
275 if value != selection.attrs['interface']:
276 selection_elem.setAttributeNS(None, name, value)
277 elif name not in ('main', 'self-test'): # (replaced by <command>)
278 selection_elem.setAttributeNS(None, name, value)
280 if selection.digests:
281 manifest_digest = doc.createElementNS(XMLNS_IFACE, 'manifest-digest')
282 for digest in selection.digests:
283 aname, avalue = digest.split('=', 1)
284 assert ':' not in aname
285 manifest_digest.setAttribute(aname, avalue)
286 selection_elem.appendChild(manifest_digest)
288 for b in selection.bindings:
289 selection_elem.appendChild(b._toxml(doc, prefixes))
291 for dep in selection.dependencies:
292 dep_elem = doc.createElementNS(XMLNS_IFACE, 'requires')
293 dep_elem.setAttributeNS(None, 'interface', dep.interface)
294 selection_elem.appendChild(dep_elem)
296 for m in dep.metadata:
297 parts = m.split(' ', 1)
298 if len(parts) == 1:
299 ns = None
300 localName = parts[0]
301 dep_elem.setAttributeNS(None, localName, dep.metadata[m])
302 else:
303 ns, localName = parts
304 prefixes.setAttributeNS(dep_elem, ns, localName, dep.metadata[m])
306 for b in dep.bindings:
307 dep_elem.appendChild(b._toxml(doc, prefixes))
309 for command in selection.get_commands().values():
310 selection_elem.appendChild(command._toxml(doc, prefixes))
312 for ns, prefix in prefixes.prefixes.items():
313 root.setAttributeNS(XMLNS_NAMESPACE, 'xmlns:' + prefix, ns)
315 return doc
317 def __repr__(self):
318 return "Selections for " + self.interface
320 def download_missing(self, config, _old = None):
321 """Check all selected implementations are available.
322 Download any that are not present.
323 Note: package implementations (distribution packages) are ignored.
324 @param config: used to get iface_cache, stores and fetcher
325 @return: a L{tasks.Blocker} or None"""
326 from zeroinstall.zerostore import NotStored
328 if _old:
329 config = get_deprecated_singleton_config()
331 iface_cache = config.iface_cache
332 stores = config.stores
334 # Check that every required selection is cached
335 needed_downloads = []
336 for sel in self.selections.values():
337 if (not sel.local_path) and (not sel.id.startswith('package:')):
338 try:
339 stores.lookup_any(sel.digests)
340 except NotStored:
341 needed_downloads.append(sel)
342 if not needed_downloads:
343 return
345 if config.network_use == model.network_offline:
346 from zeroinstall import NeedDownload
347 raise NeedDownload(', '.join([str(x) for x in needed_downloads]))
349 @tasks.async
350 def download():
351 # We're missing some. For each one, get the feed it came from
352 # and find the corresponding <implementation> in that. This will
353 # tell us where to get it from.
354 # Note: we look for an implementation with the same ID. Maybe we
355 # should check it has the same digest(s) too?
356 needed_impls = []
357 for sel in needed_downloads:
358 feed_url = sel.attrs.get('from-feed', None) or sel.attrs['interface']
359 feed = iface_cache.get_feed(feed_url)
360 if feed is None or sel.id not in feed.implementations:
361 fetch_feed = config.fetcher.download_and_import_feed(feed_url, iface_cache)
362 yield fetch_feed
363 tasks.check(fetch_feed)
365 feed = iface_cache.get_feed(feed_url)
366 assert feed, "Failed to get feed for %s" % feed_url
367 impl = feed.implementations[sel.id]
368 needed_impls.append(impl)
370 fetch_impls = config.fetcher.download_impls(needed_impls, stores)
371 yield fetch_impls
372 tasks.check(fetch_impls)
373 return download()
375 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
377 def __getitem__(self, key):
378 # Deprecated
379 if isinstance(key, basestring):
380 return self.selections[key]
381 sel = self.selections[key.uri]
382 return sel and sel.impl
384 def iteritems(self):
385 # Deprecated
386 iface_cache = get_deprecated_singleton_config().iface_cache
387 for (uri, sel) in self.selections.iteritems():
388 yield (iface_cache.get_interface(uri), sel and sel.impl)
390 def values(self):
391 # Deprecated
392 for (uri, sel) in self.selections.iteritems():
393 yield sel and sel.impl
395 def __iter__(self):
396 # Deprecated
397 iface_cache = get_deprecated_singleton_config().iface_cache
398 for (uri, sel) in self.selections.iteritems():
399 yield iface_cache.get_interface(uri)
401 def get(self, iface, if_missing):
402 # Deprecated
403 sel = self.selections.get(iface.uri, None)
404 if sel:
405 return sel.impl
406 return if_missing
408 def copy(self):
409 # Deprecated
410 s = Selections(None)
411 s.interface = self.interface
412 s.selections = self.selections.copy()
413 return s
415 def items(self):
416 # Deprecated
417 return list(self.iteritems())
419 @property
420 def commands(self):
421 i = self.interface
422 c = self.command
423 commands = []
424 while c is not None:
425 sel = self.selections[i]
426 command = sel.get_command(c)
428 commands.append(command)
430 runner = command.get_runner()
431 if not runner:
432 break
434 i = runner.metadata['interface']
435 c = runner.qdom.attrs.get('command', 'run')
437 return commands