2 Load and save a set of chosen implementations.
6 # Copyright (C) 2009, Thomas Leonard
7 # See the README file for details, or visit http://0install.net.
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
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
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))
35 local_path
= self
.attrs
.get('local-path', None)
38 if self
.id.startswith('/'):
45 def is_available(self
, stores
):
46 """Is this implementation available locally?
47 (a local implementation or a cached ZeroInstallImplementation)
50 path
= self
.local_path
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
67 if self
.local_path
is not None:
68 return self
.local_path
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
))
73 return stores
.lookup_maybe(self
.digests
)
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
):
85 self
.dependencies
= dependencies
86 self
._used
_commands
= set()
88 attrs
= impl
.metadata
.copy()
90 attrs
['version'] = impl
.get_version()
91 attrs
['interface'] = iface_uri
92 attrs
['from-feed'] = impl
.feed
.url
94 attrs
['local-path'] = impl
.local_path
98 def bindings(self
): return self
.impl
.bindings
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
):
109 for c
in self
._used
_commands
:
110 commands
[c
] = self
.impl
.commands
[c
]
113 class XMLSelection(Selection
):
114 """A Selection created by reading an XML selections document.
115 @ivar digests: a list of manifest digests
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
126 self
.digests
= digests
127 self
.commands
= commands
129 assert self
.interface
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
):
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
147 @ivar command: the command to run on 'interface'
149 @ivar selections: the selected implementations
150 @type selections: {str: L{Selection}}
152 __slots__
= ['interface', 'selections', 'command']
154 def __init__(self
, source
):
156 @param source: a map of implementations, policy or selections document
157 @type source: L{Element}
163 # (Solver will fill everything in)
165 elif isinstance(source
, Element
):
166 self
._init
_from
_qdom
(source
)
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
178 for selection
in root
.childNodes
:
179 if selection
.uri
!= XMLNS_IFACE
:
181 if selection
.name
!= 'selection':
182 if selection
.name
== 'command':
183 old_commands
.append(Command(selection
, None))
190 for elem
in selection
.childNodes
:
191 if elem
.uri
!= XMLNS_IFACE
:
193 if elem
.name
in binding_names
:
194 bindings
.append(process_binding(elem
))
195 elif elem
.name
== 'requires':
196 dep
= process_depends(elem
, None)
198 elif elem
.name
== 'manifest-digest':
199 for aname
, avalue
in elem
.attrs
.iteritems():
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
222 # 0launch 0.52 to 1.1
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()
231 iface
= runner
.interface
236 root_sel
= self
.selections
[self
.interface
]
237 main
= root_sel
.attrs
.get('main', None)
239 root_sel
.commands
['run'] = Command(Element(XMLNS_IFACE
, 'command', {'path': main
, 'name': 'run'}), None)
242 elif self
.command
== '':
243 # New style, but no command requested
245 assert not old_commands
, "<command> list in new-style selections document!"
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
.iteritems():
277 ns
, localName
= name
.split(' ', 1)
278 prefixes
.setAttributeNS(selection_elem
, ns
, localName
, value
)
279 elif name
== 'stability':
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)
309 dep_elem
.setAttributeNS(None, localName
, dep
.metadata
[m
])
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
)
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"""
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
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
:
360 if config
.network_use
== model
.network_offline
:
361 from zeroinstall
import NeedDownload
362 raise NeedDownload(', '.join([str(x
) for x
in needed_downloads
]))
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?
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
)
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
)
387 tasks
.check(fetch_impls
)
390 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
392 def __getitem__(self
, key
):
394 if isinstance(key
, basestring
):
395 return self
.selections
[key
]
396 sel
= self
.selections
[key
.uri
]
397 return sel
and sel
.impl
401 iface_cache
= get_deprecated_singleton_config().iface_cache
402 for (uri
, sel
) in self
.selections
.iteritems():
403 yield (iface_cache
.get_interface(uri
), sel
and sel
.impl
)
407 for (uri
, sel
) in self
.selections
.iteritems():
408 yield sel
and sel
.impl
412 iface_cache
= get_deprecated_singleton_config().iface_cache
413 for (uri
, sel
) in self
.selections
.iteritems():
414 yield iface_cache
.get_interface(uri
)
416 def get(self
, iface
, if_missing
):
418 sel
= self
.selections
.get(iface
.uri
, None)
426 s
.interface
= self
.interface
427 s
.selections
= self
.selections
.copy()
432 return list(self
.iteritems())
440 sel
= self
.selections
[i
]
441 command
= sel
.get_command(c
)
443 commands
.append(command
)
445 runner
= command
.get_runner()
449 i
= runner
.metadata
['interface']
450 c
= runner
.qdom
.attrs
.get('command', 'run')