Update year to 2009 in various places
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / injector / model.py
blob9d62f8f33eebda700b0205d4a69f6d9f34d9d95d
1 """In-memory representation of interfaces and other data structures.
3 The objects in this module are used to build a representation of an XML interface
4 file in memory.
6 @see: L{reader} constructs these data-structures
7 @see: U{http://0install.net/interface-spec.html} description of the domain model
9 @var defaults: Default values for the 'default' attribute for <environment> bindings of
10 well-known variables.
11 """
13 # Copyright (C) 2009, Thomas Leonard
14 # See the README file for details, or visit http://0install.net.
16 import os, re
17 from logging import info, debug
18 from zeroinstall import SafeException, version
19 from zeroinstall.injector.namespaces import XMLNS_IFACE
21 # Element names for bindings in feed files
22 binding_names = frozenset(['environment', 'overlay'])
24 network_offline = 'off-line'
25 network_minimal = 'minimal'
26 network_full = 'full'
27 network_levels = (network_offline, network_minimal, network_full)
29 stability_levels = {} # Name -> Stability
31 defaults = {
32 'PATH': '/bin:/usr/bin',
33 'XDG_CONFIG_DIRS': '/etc/xdg',
34 'XDG_DATA_DIRS': '/usr/local/share:/usr/share',
37 class InvalidInterface(SafeException):
38 """Raised when parsing an invalid feed."""
39 def __init__(self, message, ex = None):
40 if ex:
41 message += "\n\n(exact error: %s)" % ex
42 SafeException.__init__(self, message)
44 def _split_arch(arch):
45 """Split an arch into an (os, machine) tuple. Either or both parts may be None."""
46 if not arch:
47 return None, None
48 elif '-' not in arch:
49 raise SafeException("Malformed arch '%s'" % arch)
50 else:
51 osys, machine = arch.split('-', 1)
52 if osys == '*': osys = None
53 if machine == '*': machine = None
54 return osys, machine
56 def _join_arch(osys, machine):
57 if osys == machine == None: return None
58 return "%s-%s" % (osys or '*', machine or '*')
60 class Stability(object):
61 """A stability rating. Each implementation has an upstream stability rating and,
62 optionally, a user-set rating."""
63 __slots__ = ['level', 'name', 'description']
64 def __init__(self, level, name, description):
65 self.level = level
66 self.name = name
67 self.description = description
68 assert name not in stability_levels
69 stability_levels[name] = self
71 def __cmp__(self, other):
72 return cmp(self.level, other.level)
74 def __str__(self):
75 return self.name
77 def __repr__(self):
78 return "<Stability: " + self.description + ">"
80 def process_binding(e):
81 """Internal"""
82 if e.name == 'environment':
83 mode = {
84 None: EnvironmentBinding.PREPEND,
85 'prepend': EnvironmentBinding.PREPEND,
86 'append': EnvironmentBinding.APPEND,
87 'replace': EnvironmentBinding.REPLACE,
88 }[e.getAttribute('mode')]
90 binding = EnvironmentBinding(e.getAttribute('name'),
91 insert = e.getAttribute('insert'),
92 default = e.getAttribute('default'),
93 mode = mode)
94 if not binding.name: raise InvalidInterface("Missing 'name' in binding")
95 if binding.insert is None: raise InvalidInterface("Missing 'insert' in binding")
96 return binding
97 elif e.name == 'overlay':
98 return OverlayBinding(e.getAttribute('src'), e.getAttribute('mount-point'))
99 else:
100 raise Exception("Unknown binding type '%s'" % e.name)
102 def process_depends(item):
103 """Internal"""
104 # Note: also called from selections
105 dep_iface = item.getAttribute('interface')
106 if not dep_iface:
107 raise InvalidInterface("Missing 'interface' on <requires>")
108 dependency = InterfaceDependency(dep_iface, metadata = item.attrs)
110 for e in item.childNodes:
111 if e.uri != XMLNS_IFACE: continue
112 if e.name in binding_names:
113 dependency.bindings.append(process_binding(e))
114 elif e.name == 'version':
115 dependency.restrictions.append(
116 VersionRangeRestriction(not_before = parse_version(e.getAttribute('not-before')),
117 before = parse_version(e.getAttribute('before'))))
118 return dependency
121 insecure = Stability(0, 'insecure', 'This is a security risk')
122 buggy = Stability(5, 'buggy', 'Known to have serious bugs')
123 developer = Stability(10, 'developer', 'Work-in-progress - bugs likely')
124 testing = Stability(20, 'testing', 'Stability unknown - please test!')
125 stable = Stability(30, 'stable', 'Tested - no serious problems found')
126 packaged = Stability(35, 'packaged', 'Supplied by the local package manager')
127 preferred = Stability(40, 'preferred', 'Best of all - must be set manually')
129 class Restriction(object):
130 """A Restriction limits the allowed implementations of an Interface."""
131 __slots__ = []
133 def meets_restriction(self, impl):
134 raise NotImplementedError("Abstract")
136 class VersionRangeRestriction(Restriction):
137 """Only versions within the given range are acceptable"""
138 __slots__ = ['before', 'not_before']
139 def __init__(self, before, not_before):
140 self.before = before
141 self.not_before = not_before
143 def meets_restriction(self, impl):
144 if self.not_before and impl.version < self.not_before:
145 return False
146 if self.before and impl.version >= self.before:
147 return False
148 return True
150 def __str__(self):
151 if self.not_before is not None or self.before is not None:
152 range = ''
153 if self.not_before is not None:
154 range += format_version(self.not_before) + ' <= '
155 range += 'version'
156 if self.before is not None:
157 range += ' < ' + format_version(self.before)
158 else:
159 range = 'none'
160 return "(restriction: %s)" % range
162 class Binding(object):
163 """Information about how the choice of a Dependency is made known
164 to the application being run."""
166 class EnvironmentBinding(Binding):
167 """Indicate the chosen implementation using an environment variable."""
168 __slots__ = ['name', 'insert', 'default', 'mode']
170 PREPEND = 'prepend'
171 APPEND = 'append'
172 REPLACE = 'replace'
174 def __init__(self, name, insert, default = None, mode = PREPEND):
175 """mode argument added in version 0.28"""
176 self.name = name
177 self.insert = insert
178 self.default = default
179 self.mode = mode
181 def __str__(self):
182 return "<environ %s %s %s>" % (self.name, self.mode, self.insert)
184 __repr__ = __str__
186 def get_value(self, path, old_value):
187 """Calculate the new value of the environment variable after applying this binding.
188 @param path: the path to the selected implementation
189 @param old_value: the current value of the environment variable
190 @return: the new value for the environment variable"""
191 extra = os.path.join(path, self.insert)
193 if self.mode == EnvironmentBinding.REPLACE:
194 return extra
196 if old_value is None:
197 old_value = self.default or defaults.get(self.name, None)
198 if old_value is None:
199 return extra
200 if self.mode == EnvironmentBinding.PREPEND:
201 return extra + ':' + old_value
202 else:
203 return old_value + ':' + extra
205 def _toxml(self, doc):
206 """Create a DOM element for this binding.
207 @param doc: document to use to create the element
208 @return: the new element
210 env_elem = doc.createElementNS(XMLNS_IFACE, 'environment')
211 env_elem.setAttributeNS(None, 'name', self.name)
212 env_elem.setAttributeNS(None, 'insert', self.insert)
213 if self.default:
214 env_elem.setAttributeNS(None, 'default', self.default)
215 return env_elem
217 class OverlayBinding(Binding):
218 """Make the chosen implementation available by overlaying it onto another part of the file-system.
219 This is to support legacy programs which use hard-coded paths."""
220 __slots__ = ['src', 'mount_point']
222 def __init__(self, src, mount_point):
223 self.src = src
224 self.mount_point = mount_point
226 def __str__(self):
227 return "<overlay %s on %s>" % (self.src or '.', self.mount_point or '/')
229 __repr__ = __str__
231 def _toxml(self, doc):
232 """Create a DOM element for this binding.
233 @param doc: document to use to create the element
234 @return: the new element
236 env_elem = doc.createElementNS(XMLNS_IFACE, 'overlay')
237 if self.src is not None:
238 env_elem.setAttributeNS(None, 'src', self.src)
239 if self.mount_point is not None:
240 env_elem.setAttributeNS(None, 'mount-point', self.mount_point)
241 return env_elem
243 class Feed(object):
244 """An interface's feeds are other interfaces whose implementations can also be
245 used as implementations of this interface."""
246 __slots__ = ['uri', 'os', 'machine', 'user_override', 'langs']
247 def __init__(self, uri, arch, user_override, langs = None):
248 self.uri = uri
249 # This indicates whether the feed comes from the user's overrides
250 # file. If true, writer.py will write it when saving.
251 self.user_override = user_override
252 self.os, self.machine = _split_arch(arch)
253 self.langs = langs
255 def __str__(self):
256 return "<Feed from %s>" % self.uri
257 __repr__ = __str__
259 arch = property(lambda self: _join_arch(self.os, self.machine))
261 class Dependency(object):
262 """A Dependency indicates that an Implementation requires some additional
263 code to function. This is an abstract base class.
264 @ivar metadata: any extra attributes from the XML element
265 @type metadata: {str: str}
267 __slots__ = ['metadata']
269 def __init__(self, metadata):
270 if metadata is None:
271 metadata = {}
272 else:
273 assert not isinstance(metadata, basestring) # Use InterfaceDependency instead!
274 self.metadata = metadata
276 class InterfaceDependency(Dependency):
277 """A Dependency on a Zero Install interface.
278 @ivar interface: the interface required by this dependency
279 @type interface: str
280 @ivar restrictions: a list of constraints on acceptable implementations
281 @type restrictions: [L{Restriction}]
282 @ivar bindings: how to make the choice of implementation known
283 @type bindings: [L{Binding}]
284 @since: 0.28
286 __slots__ = ['interface', 'restrictions', 'bindings', 'metadata']
288 def __init__(self, interface, restrictions = None, metadata = None):
289 Dependency.__init__(self, metadata)
290 assert isinstance(interface, (str, unicode))
291 assert interface
292 self.interface = interface
293 if restrictions is None:
294 self.restrictions = []
295 else:
296 self.restrictions = restrictions
297 self.bindings = []
299 def __str__(self):
300 return "<Dependency on %s; bindings: %s%s>" % (self.interface, self.bindings, self.restrictions)
302 class RetrievalMethod(object):
303 """A RetrievalMethod provides a way to fetch an implementation."""
304 __slots__ = []
306 class DownloadSource(RetrievalMethod):
307 """A DownloadSource provides a way to fetch an implementation."""
308 __slots__ = ['implementation', 'url', 'size', 'extract', 'start_offset', 'type']
310 def __init__(self, implementation, url, size, extract, start_offset = 0, type = None):
311 self.implementation = implementation
312 self.url = url
313 self.size = size
314 self.extract = extract
315 self.start_offset = start_offset
316 self.type = type # MIME type - see unpack.py
318 class Recipe(RetrievalMethod):
319 """Get an implementation by following a series of steps.
320 @ivar size: the combined download sizes from all the steps
321 @type size: int
322 @ivar steps: the sequence of steps which must be performed
323 @type steps: [L{RetrievalMethod}]"""
324 __slots__ = ['steps']
326 def __init__(self):
327 self.steps = []
329 size = property(lambda self: sum([x.size for x in self.steps]))
331 class Implementation(object):
332 """An Implementation is a package which implements an Interface.
333 @ivar download_sources: list of methods of getting this implementation
334 @type download_sources: [L{RetrievalMethod}]
335 @ivar feed: the feed owning this implementation (since 0.32)
336 @type feed: [L{ZeroInstallFeed}]
337 @ivar bindings: how to tell this component where it itself is located (since 0.31)
338 @type bindings: [Binding]
339 @ivar upstream_stability: the stability reported by the packager
340 @type upstream_stability: [insecure | buggy | developer | testing | stable | packaged]
341 @ivar user_stability: the stability as set by the user
342 @type upstream_stability: [insecure | buggy | developer | testing | stable | packaged | preferred]
343 @ivar langs: natural languages supported by this package
344 @ivar requires: interfaces this package depends on
345 @ivar main: the default file to execute when running as a program
346 @ivar metadata: extra metadata from the feed
347 @type metadata: {"[URI ]localName": str}
348 @ivar id: a unique identifier for this Implementation
349 @ivar version: a parsed version number
350 @ivar released: release date
353 # Note: user_stability shouldn't really be here
355 __slots__ = ['upstream_stability', 'user_stability', 'langs',
356 'requires', 'main', 'metadata', 'download_sources',
357 'id', 'feed', 'version', 'released', 'bindings', 'machine']
359 def __init__(self, feed, id):
360 assert id
361 self.feed = feed
362 self.id = id
363 self.main = None
364 self.user_stability = None
365 self.upstream_stability = None
366 self.metadata = {} # [URI + " "] + localName -> value
367 self.requires = []
368 self.version = None
369 self.released = None
370 self.download_sources = []
371 self.langs = None
372 self.machine = None
373 self.bindings = []
375 def get_stability(self):
376 return self.user_stability or self.upstream_stability or testing
378 def __str__(self):
379 return self.id
381 def __repr__(self):
382 return "v%s (%s)" % (self.get_version(), self.id)
384 def __cmp__(self, other):
385 """Newer versions come first"""
386 return cmp(other.version, self.version)
388 def get_version(self):
389 """Return the version as a string.
390 @see: L{format_version}
392 return format_version(self.version)
394 arch = property(lambda self: _join_arch(self.os, self.machine))
396 os = None
398 class DistributionImplementation(Implementation):
399 """An implementation provided by the distribution. Information such as the version
400 comes from the package manager.
401 @since: 0.28"""
402 __slots__ = ['installed']
404 def __init__(self, feed, id):
405 assert id.startswith('package:')
406 Implementation.__init__(self, feed, id)
407 self.installed = True
409 class ZeroInstallImplementation(Implementation):
410 """An implementation where all the information comes from Zero Install.
411 @since: 0.28"""
412 __slots__ = ['os', 'size']
414 def __init__(self, feed, id):
415 """id can be a local path (string starting with /) or a manifest hash (eg "sha1=XXX")"""
416 Implementation.__init__(self, feed, id)
417 self.size = None
418 self.os = None
420 # Deprecated
421 dependencies = property(lambda self: dict([(x.interface, x) for x in self.requires
422 if isinstance(x, InterfaceDependency)]))
424 def add_download_source(self, url, size, extract, start_offset = 0, type = None):
425 """Add a download source."""
426 self.download_sources.append(DownloadSource(self, url, size, extract, start_offset, type))
428 def set_arch(self, arch):
429 self.os, self.machine = _split_arch(arch)
430 arch = property(lambda self: _join_arch(self.os, self.machine), set_arch)
432 class Interface(object):
433 """An Interface represents some contract of behaviour.
434 @ivar uri: the URI for this interface.
435 @ivar stability_policy: user's configured policy.
436 Implementations at this level or higher are preferred.
437 Lower levels are used only if there is no other choice.
439 __slots__ = ['uri', 'stability_policy', '_main_feed', 'extra_feeds']
441 implementations = property(lambda self: self._main_feed.implementations)
442 name = property(lambda self: self._main_feed.name)
443 description = property(lambda self: self._main_feed.description)
444 summary = property(lambda self: self._main_feed.summary)
445 last_modified = property(lambda self: self._main_feed.last_modified)
446 feeds = property(lambda self: self.extra_feeds + self._main_feed.feeds)
447 metadata = property(lambda self: self._main_feed.metadata)
449 last_checked = property(lambda self: self._main_feed.last_checked)
451 def __init__(self, uri):
452 assert uri
453 if uri.startswith('http:') or uri.startswith('/'):
454 self.uri = uri
455 else:
456 raise SafeException("Interface name '%s' doesn't start "
457 "with 'http:'" % uri)
458 self.reset()
460 def _get_feed_for(self):
461 retval = {}
462 for key in self._main_feed.feed_for:
463 retval[key] = True
464 return retval
465 feed_for = property(_get_feed_for) # Deprecated (used by 0publish)
467 def reset(self):
468 self.extra_feeds = []
469 self._main_feed = _dummy_feed
470 self.stability_policy = None
472 def get_name(self):
473 if self._main_feed is not _dummy_feed:
474 return self._main_feed.get_name()
475 return '(' + os.path.basename(self.uri) + ')'
477 def __repr__(self):
478 return "<Interface %s>" % self.uri
480 def set_stability_policy(self, new):
481 assert new is None or isinstance(new, Stability)
482 self.stability_policy = new
484 def get_feed(self, url):
485 for x in self.extra_feeds:
486 if x.uri == url:
487 return x
488 return self._main_feed.get_feed(url)
490 def get_metadata(self, uri, name):
491 return self._main_feed.get_metadata(uri, name)
493 def _merge_attrs(attrs, item):
494 """Add each attribute of item to a copy of attrs and return the copy.
495 @type attrs: {str: str}
496 @type item: L{qdom.Element}
497 @rtype: {str: str}
499 new = attrs.copy()
500 for a in item.attrs:
501 new[str(a)] = item.attrs[a]
502 return new
504 def _get_long(elem, attr_name):
505 val = elem.getAttribute(attr_name)
506 if val is not None:
507 try:
508 val = long(val)
509 except ValueError, ex:
510 raise SafeException("Invalid value for integer attribute '%s': %s" % (attr_name, val))
511 return val
513 class ZeroInstallFeed(object):
514 """A feed lists available implementations of an interface.
515 @ivar url: the URL for this feed
516 @ivar implementations: Implementations in this feed, indexed by ID
517 @type implementations: {str: L{Implementation}}
518 @ivar name: human-friendly name
519 @ivar summary: short textual description
520 @ivar description: long textual description
521 @ivar last_modified: timestamp on signature
522 @ivar last_checked: time feed was last successfully downloaded and updated
523 @ivar feeds: list of <feed> elements in this feed
524 @type feeds: [L{Feed}]
525 @ivar feed_for: interfaces for which this could be a feed
526 @type feed_for: set(str)
527 @ivar metadata: extra elements we didn't understand
529 # _main is deprecated
530 __slots__ = ['url', 'implementations', 'name', 'description', 'summary',
531 'last_checked', 'last_modified', 'feeds', 'feed_for', 'metadata']
533 def __init__(self, feed_element, local_path = None, distro = None):
534 """Create a feed object from a DOM.
535 @param feed_element: the root element of a feed file
536 @type feed_element: L{qdom.Element}
537 @param local_path: the pathname of this local feed, or None for remote feeds
538 @param distro: used to resolve distribution package references
539 @type distro: L{distro.Distribution} or None"""
540 assert feed_element
541 self.implementations = {}
542 self.name = None
543 self.summary = None
544 self.description = ""
545 self.last_modified = None
546 self.feeds = []
547 self.feed_for = set()
548 self.metadata = []
549 self.last_checked = None
551 assert feed_element.name in ('interface', 'feed'), "Root element should be <interface>, not %s" % feed_element
552 assert feed_element.uri == XMLNS_IFACE, "Wrong namespace on root element: %s" % feed_element.uri
554 main = feed_element.getAttribute('main')
555 #if main: warn("Setting 'main' on the root element is deprecated. Put it on a <group> instead")
557 if local_path:
558 self.url = local_path
559 local_dir = os.path.dirname(local_path)
560 else:
561 self.url = feed_element.getAttribute('uri')
562 if not self.url:
563 raise InvalidInterface("<interface> uri attribute missing")
564 local_dir = None # Can't have relative paths
566 min_injector_version = feed_element.getAttribute('min-injector-version')
567 if min_injector_version:
568 if parse_version(min_injector_version) > parse_version(version):
569 raise InvalidInterface("This feed requires version %s or later of "
570 "Zero Install, but I am only version %s. "
571 "You can get a newer version from http://0install.net" %
572 (min_injector_version, version))
574 for x in feed_element.childNodes:
575 if x.uri != XMLNS_IFACE:
576 self.metadata.append(x)
577 continue
578 if x.name == 'name':
579 self.name = x.content
580 elif x.name == 'description':
581 self.description = x.content
582 elif x.name == 'summary':
583 self.summary = x.content
584 elif x.name == 'feed-for':
585 feed_iface = x.getAttribute('interface')
586 if not feed_iface:
587 raise InvalidInterface('Missing "interface" attribute in <feed-for>')
588 self.feed_for.add(feed_iface)
589 # Bug report from a Debian/stable user that --feed gets the wrong value.
590 # Can't reproduce (even in a Debian/stable chroot), but add some logging here
591 # in case it happens again.
592 debug("Is feed-for %s", feed_iface)
593 elif x.name == 'feed':
594 feed_src = x.getAttribute('src')
595 if not feed_src:
596 raise InvalidInterface('Missing "src" attribute in <feed>')
597 if feed_src.startswith('http:') or local_path:
598 self.feeds.append(Feed(feed_src, x.getAttribute('arch'), False, langs = x.getAttribute('langs')))
599 else:
600 raise InvalidInterface("Invalid feed URL '%s'" % feed_src)
601 else:
602 self.metadata.append(x)
604 if not self.name:
605 raise InvalidInterface("Missing <name> in feed")
606 if not self.summary:
607 raise InvalidInterface("Missing <summary> in feed")
609 def process_group(group, group_attrs, base_depends, base_bindings):
610 for item in group.childNodes:
611 if item.uri != XMLNS_IFACE: continue
613 if item.name not in ('group', 'implementation', 'package-implementation'):
614 continue
616 depends = base_depends[:]
617 bindings = base_bindings[:]
619 item_attrs = _merge_attrs(group_attrs, item)
621 # We've found a group or implementation. Scan for dependencies
622 # and bindings. Doing this here means that:
623 # - We can share the code for groups and implementations here.
624 # - The order doesn't matter, because these get processed first.
625 # A side-effect is that the document root cannot contain
626 # these.
627 for child in item.childNodes:
628 if child.uri != XMLNS_IFACE: continue
629 if child.name == 'requires':
630 dep = process_depends(child)
631 depends.append(dep)
632 elif child.name in binding_names:
633 bindings.append(process_binding(child))
635 if item.name == 'group':
636 process_group(item, item_attrs, depends, bindings)
637 elif item.name == 'implementation':
638 process_impl(item, item_attrs, depends, bindings)
639 elif item.name == 'package-implementation':
640 process_native_impl(item, item_attrs, depends)
641 else:
642 assert 0
644 def process_impl(item, item_attrs, depends, bindings):
645 id = item.getAttribute('id')
646 if id is None:
647 raise InvalidInterface("Missing 'id' attribute on %s" % item)
648 if local_dir and (id.startswith('/') or id.startswith('.')):
649 impl = self._get_impl(os.path.abspath(os.path.join(local_dir, id)))
650 else:
651 if '=' not in id:
652 raise InvalidInterface('Invalid "id"; form is "alg=value" (got "%s")' % id)
653 alg, sha1 = id.split('=')
654 try:
655 long(sha1, 16)
656 except Exception, ex:
657 raise InvalidInterface('Bad SHA1 attribute: %s' % ex)
658 impl = self._get_impl(id)
660 impl.metadata = item_attrs
661 try:
662 version_mod = item_attrs.get('version-modifier', None)
663 if version_mod:
664 item_attrs['version'] += version_mod
665 del item_attrs['version-modifier']
666 version = item_attrs['version']
667 except KeyError:
668 raise InvalidInterface("Missing version attribute")
669 impl.version = parse_version(version)
671 item_main = item_attrs.get('main', None)
672 if item_main and item_main.startswith('/'):
673 raise InvalidInterface("'main' attribute must be relative, but '%s' starts with '/'!" %
674 item_main)
675 impl.main = item_main
677 impl.released = item_attrs.get('released', None)
678 impl.langs = item_attrs.get('langs', None)
680 size = item.getAttribute('size')
681 if size:
682 impl.size = long(size)
683 impl.arch = item_attrs.get('arch', None)
684 try:
685 stability = stability_levels[str(item_attrs['stability'])]
686 except KeyError:
687 stab = str(item_attrs['stability'])
688 if stab != stab.lower():
689 raise InvalidInterface('Stability "%s" invalid - use lower case!' % item_attrs.stability)
690 raise InvalidInterface('Stability "%s" invalid' % item_attrs['stability'])
691 if stability >= preferred:
692 raise InvalidInterface("Upstream can't set stability to preferred!")
693 impl.upstream_stability = stability
695 impl.bindings = bindings
696 impl.requires = depends
698 for elem in item.childNodes:
699 if elem.uri != XMLNS_IFACE: continue
700 if elem.name == 'archive':
701 url = elem.getAttribute('href')
702 if not url:
703 raise InvalidInterface("Missing href attribute on <archive>")
704 size = elem.getAttribute('size')
705 if not size:
706 raise InvalidInterface("Missing size attribute on <archive>")
707 impl.add_download_source(url = url, size = long(size),
708 extract = elem.getAttribute('extract'),
709 start_offset = _get_long(elem, 'start-offset'),
710 type = elem.getAttribute('type'))
711 elif elem.name == 'recipe':
712 recipe = Recipe()
713 for recipe_step in elem.childNodes:
714 if recipe_step.uri == XMLNS_IFACE and recipe_step.name == 'archive':
715 url = recipe_step.getAttribute('href')
716 if not url:
717 raise InvalidInterface("Missing href attribute on <archive>")
718 size = recipe_step.getAttribute('size')
719 if not size:
720 raise InvalidInterface("Missing size attribute on <archive>")
721 recipe.steps.append(DownloadSource(None, url = url, size = long(size),
722 extract = recipe_step.getAttribute('extract'),
723 start_offset = _get_long(recipe_step, 'start-offset'),
724 type = recipe_step.getAttribute('type')))
725 else:
726 info("Unknown step '%s' in recipe; skipping recipe", recipe_step.name)
727 break
728 else:
729 impl.download_sources.append(recipe)
731 def process_native_impl(item, item_attrs, depends):
732 package = item_attrs.get('package', None)
733 if package is None:
734 raise InvalidInterface("Missing 'package' attribute on %s" % item)
736 def factory(id):
737 assert id.startswith('package:')
738 impl = self._get_impl(id)
740 impl.metadata = item_attrs
742 item_main = item_attrs.get('main', None)
743 if item_main and not item_main.startswith('/'):
744 raise InvalidInterface("'main' attribute must be absolute, but '%s' doesn't start with '/'!" %
745 item_main)
746 impl.main = item_main
747 impl.upstream_stability = packaged
748 impl.requires = depends
750 return impl
752 distro.get_package_info(package, factory)
754 root_attrs = {'stability': 'testing'}
755 if main:
756 root_attrs['main'] = main
757 process_group(feed_element, root_attrs, [], [])
759 def get_name(self):
760 return self.name or '(' + os.path.basename(self.url) + ')'
762 def __repr__(self):
763 return "<Feed %s>" % self.url
765 def _get_impl(self, id):
766 if id not in self.implementations:
767 if id.startswith('package:'):
768 impl = DistributionImplementation(self, id)
769 else:
770 impl = ZeroInstallImplementation(self, id)
771 self.implementations[id] = impl
772 return self.implementations[id]
774 def set_stability_policy(self, new):
775 assert new is None or isinstance(new, Stability)
776 self.stability_policy = new
778 def get_feed(self, url):
779 for x in self.feeds:
780 if x.uri == url:
781 return x
782 return None
784 def add_metadata(self, elem):
785 self.metadata.append(elem)
787 def get_metadata(self, uri, name):
788 """Return a list of interface metadata elements with this name and namespace URI."""
789 return [m for m in self.metadata if m.name == name and m.uri == uri]
791 class DummyFeed(object):
792 """Temporary class used during API transition."""
793 last_modified = None
794 name = '-'
795 last_checked = property(lambda self: None)
796 implementations = property(lambda self: {})
797 feeds = property(lambda self: [])
798 summary = property(lambda self: '-')
799 description = property(lambda self: '')
800 def get_name(self): return self.name
801 def get_feed(self, url): return None
802 def get_metadata(self, uri, name): return []
803 _dummy_feed = DummyFeed()
805 def unescape(uri):
806 """Convert each %20 to a space, etc.
807 @rtype: str"""
808 uri = uri.replace('#', '/')
809 if '%' not in uri: return uri
810 return re.sub('%[0-9a-fA-F][0-9a-fA-F]',
811 lambda match: chr(int(match.group(0)[1:], 16)),
812 uri).decode('utf-8')
814 def escape(uri):
815 """Convert each space to %20, etc
816 @rtype: str"""
817 return re.sub('[^-_.a-zA-Z0-9]',
818 lambda match: '%%%02x' % ord(match.group(0)),
819 uri.encode('utf-8'))
821 def _pretty_escape(uri):
822 """Convert each space to %20, etc
823 : is preserved and / becomes #. This makes for nicer strings,
824 and may replace L{escape} everywhere in future.
825 @rtype: str"""
826 return re.sub('[^-_.a-zA-Z0-9:/]',
827 lambda match: '%%%02x' % ord(match.group(0)),
828 uri.encode('utf-8')).replace('/', '#')
830 def canonical_iface_uri(uri):
831 """If uri is a relative path, convert to an absolute one.
832 A "file:///foo" URI is converted to "/foo".
833 Otherwise, return it unmodified.
834 @rtype: str
835 @raise SafeException: if uri isn't valid
837 if uri.startswith('http://'):
838 if uri.find("/", 7) == -1:
839 raise SafeException("Missing / after hostname in URI '%s'" % uri)
840 return uri
841 elif uri.startswith('file:///'):
842 return uri[7:]
843 else:
844 iface_uri = os.path.realpath(uri)
845 if os.path.isfile(iface_uri):
846 return iface_uri
847 raise SafeException("Bad interface name '%s'.\n"
848 "(doesn't start with 'http:', and "
849 "doesn't exist as a local file '%s' either)" %
850 (uri, iface_uri))
852 _version_mod_to_value = {
853 'pre': -2,
854 'rc': -1,
855 '': 0,
856 'post': 1,
859 # Reverse mapping
860 _version_value_to_mod = {}
861 for x in _version_mod_to_value: _version_value_to_mod[_version_mod_to_value[x]] = x
862 del x
864 _version_re = re.compile('-([a-z]*)')
866 def parse_version(version_string):
867 """Convert a version string to an internal representation.
868 The parsed format can be compared quickly using the standard Python functions.
869 - Version := DottedList ("-" Mod DottedList?)*
870 - DottedList := (Integer ("." Integer)*)
871 @rtype: tuple (opaque)
872 @raise SafeException: if the string isn't a valid version
873 @since: 0.24 (moved from L{reader}, from where it is still available):"""
874 if version_string is None: return None
875 parts = _version_re.split(version_string)
876 if parts[-1] == '':
877 del parts[-1] # Ends with a modifier
878 else:
879 parts.append('')
880 if not parts:
881 raise SafeException("Empty version string!")
882 l = len(parts)
883 try:
884 for x in range(0, l, 2):
885 part = parts[x]
886 if part:
887 parts[x] = map(int, parts[x].split('.'))
888 else:
889 parts[x] = [] # (because ''.split('.') == [''], not [])
890 for x in range(1, l, 2):
891 parts[x] = _version_mod_to_value[parts[x]]
892 return parts
893 except ValueError, ex:
894 raise SafeException("Invalid version format in '%s': %s" % (version_string, ex))
895 except KeyError, ex:
896 raise SafeException("Invalid version modifier in '%s': %s" % (version_string, ex))
898 def format_version(version):
899 """Format a parsed version for display. Undoes the effect of L{parse_version}.
900 @see: L{Implementation.get_version}
901 @rtype: str
902 @since: 0.24"""
903 version = version[:]
904 l = len(version)
905 for x in range(0, l, 2):
906 version[x] = '.'.join(map(str, version[x]))
907 for x in range(1, l, 2):
908 version[x] = '-' + _version_value_to_mod[version[x]]
909 if version[-1] == '-': del version[-1]
910 return ''.join(version)