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 _
, 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
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
.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
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
.items():
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
= 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)
311 dep_elem
.setAttributeNS(None, localName
, dep
.metadata
[m
])
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
)
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"""
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
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
:
362 if config
.network_use
== model
.network_offline
:
363 from zeroinstall
import NeedDownload
364 raise NeedDownload(', '.join([str(x
) for x
in needed_downloads
]))
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?
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
)
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
)
389 tasks
.check(fetch_impls
)
392 # These (deprecated) methods are to make a Selections object look like the old Policy.implementation map...
394 def __getitem__(self
, key
):
396 if isinstance(key
, basestring
):
397 return self
.selections
[key
]
398 sel
= self
.selections
[key
.uri
]
399 return sel
and sel
.impl
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
)
409 for (uri
, sel
) in self
.selections
.items():
410 yield sel
and sel
.impl
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
):
420 sel
= self
.selections
.get(iface
.uri
, None)
428 s
.interface
= self
.interface
429 s
.selections
= self
.selections
.copy()
434 return list(self
.iteritems())
442 sel
= self
.selections
[i
]
443 command
= sel
.get_command(c
)
445 commands
.append(command
)
447 runner
= command
.get_runner()
451 i
= runner
.metadata
['interface']
452 c
= runner
.qdom
.attrs
.get('command', 'run')