1 """In-memory representation of interfaces and other data structures.
2
3 The objects in this module are used to build a representation of an XML interface
4 file in memory.
5
6 @see: L{reader} constructs these data-structures
7 @see: U{http://0install.net/interface-spec.html} description of the domain model
8
9 @var defaults: Default values for the 'default' attribute for <environment> bindings of
10 well-known variables.
11 """
12
13
14
15
16 from zeroinstall import _, logger
17 import os, re, locale, sys
18 from zeroinstall import SafeException, version
19 from zeroinstall.injector.namespaces import XMLNS_IFACE
20 from zeroinstall.injector.versions import parse_version, format_version
21 from zeroinstall.injector import qdom, versions
22 from zeroinstall import support, zerostore
23 from zeroinstall.support import escaping
24
25
26 binding_names = frozenset(['environment', 'overlay', 'executable-in-path', 'executable-in-var'])
27
28 _dependency_names = frozenset(['requires', 'restricts'])
29
30 network_offline = 'off-line'
31 network_minimal = 'minimal'
32 network_full = 'full'
33 network_levels = (network_offline, network_minimal, network_full)
34
35 stability_levels = {}
36
37 defaults = {
38 'PATH': '/bin:/usr/bin',
39 'XDG_CONFIG_DIRS': '/etc/xdg',
40 'XDG_DATA_DIRS': '/usr/local/share:/usr/share',
41 }
44 """Raised when parsing an invalid feed."""
45 feed_url = None
46
48 """@type message: str"""
49 if ex:
50 try:
51 message += "\n\n(exact error: %s)" % ex
52 except:
53
54
55
56 import codecs
57 decoder = codecs.lookup('utf-8')
58 decex = decoder.decode(str(ex), errors = 'replace')[0]
59 message += "\n\n(exact error: %s)" % decex
60
61 SafeException.__init__(self, message)
62
72
74 """Split an arch into an (os, machine) tuple. Either or both parts may be None.
75 @type arch: str"""
76 if not arch:
77 return None, None
78 elif '-' not in arch:
79 raise SafeException(_("Malformed arch '%s'") % arch)
80 else:
81 osys, machine = arch.split('-', 1)
82 if osys == '*': osys = None
83 if machine == '*': machine = None
84 return osys, machine
85
87 """@type osys: str
88 @type machine: str
89 @rtype: str"""
90 if osys == machine == None: return None
91 return "%s-%s" % (osys or '*', machine or '*')
92
94 """@type options: {str: str}
95 @rtype: str"""
96 (language, encoding) = locale.getlocale()
97
98 if language:
99
100 language = language.replace('_', '-')
101 else:
102 language = 'en-US'
103
104 return (options.get(language, None) or
105 options.get(language.split('-', 1)[0], None) or
106 options.get('en', None))
107
109 """A stability rating. Each implementation has an upstream stability rating and,
110 optionally, a user-set rating."""
111 __slots__ = ['level', 'name', 'description']
112 - def __init__(self, level, name, description):
121
123 """@type other: L{Stability}
124 @rtype: int"""
125 return cmp(self.level, other.level)
126
128 """@type other: L{Stability}
129 @rtype: bool"""
130 if isinstance(other, Stability):
131 return self.level < other.level
132 else:
133 return NotImplemented
134
136 """@type other: L{Stability}
137 @rtype: bool"""
138 if isinstance(other, Stability):
139 return self.level == other.level
140 else:
141 return NotImplemented
142
144 """@rtype: str"""
145 return self.name
146
149
151 """Internal
152 @type e: L{zeroinstall.injector.qdom.Element}
153 @rtype: L{Binding}"""
154 if e.name == 'environment':
155 mode = {
156 None: EnvironmentBinding.PREPEND,
157 'prepend': EnvironmentBinding.PREPEND,
158 'append': EnvironmentBinding.APPEND,
159 'replace': EnvironmentBinding.REPLACE,
160 }[e.getAttribute('mode')]
161
162 binding = EnvironmentBinding(e.getAttribute('name'),
163 insert = e.getAttribute('insert'),
164 default = e.getAttribute('default'),
165 value = e.getAttribute('value'),
166 mode = mode,
167 separator = e.getAttribute('separator'))
168 if not binding.name: raise InvalidInterface(_("Missing 'name' in binding"))
169 if binding.insert is None and binding.value is None:
170 raise InvalidInterface(_("Missing 'insert' or 'value' in binding"))
171 if binding.insert is not None and binding.value is not None:
172 raise InvalidInterface(_("Binding contains both 'insert' and 'value'"))
173 return binding
174 elif e.name == 'executable-in-path':
175 return ExecutableBinding(e, in_path = True)
176 elif e.name == 'executable-in-var':
177 return ExecutableBinding(e, in_path = False)
178 elif e.name == 'overlay':
179 return OverlayBinding(e.getAttribute('src'), e.getAttribute('mount-point'))
180 else:
181 raise Exception(_("Unknown binding type '%s'") % e.name)
182
230
231 -def N_(message): return message
232
233 insecure = Stability(0, N_('insecure'), _('This is a security risk'))
234 buggy = Stability(5, N_('buggy'), _('Known to have serious bugs'))
235 developer = Stability(10, N_('developer'), _('Work-in-progress - bugs likely'))
236 testing = Stability(20, N_('testing'), _('Stability unknown - please test!'))
237 stable = Stability(30, N_('stable'), _('Tested - no serious problems found'))
238 packaged = Stability(35, N_('packaged'), _('Supplied by the local package manager'))
239 preferred = Stability(40, N_('preferred'), _('Best of all - must be set manually'))
240
241 del N_
244 """A Restriction limits the allowed implementations of an Interface."""
245 __slots__ = []
246
247 reason = _("Incompatible with user-specified requirements")
248
250 """Called by the L{solver.Solver} to check whether a particular implementation is acceptable.
251 @return: False if this implementation is not a possibility
252 @rtype: bool"""
253 raise NotImplementedError(_("Abstract"))
254
256 return "missing __str__ on %s" % type(self)
257
259 """@rtype: str"""
260 return "<restriction: %s>" % self
261
263 """Only select implementations with a particular version number.
264 @since: 0.40"""
265
267 """@param version: the required version number
268 @see: L{parse_version}; use this to pre-process the version number"""
269 assert not isinstance(version, str), "Not parsed: " + version
270 self.version = version
271
273 """@type impl: L{ZeroInstallImplementation}
274 @rtype: bool"""
275 return impl.version == self.version
276
279
281 """Only versions within the given range are acceptable"""
282 __slots__ = ['before', 'not_before']
283
284 - def __init__(self, before, not_before):
285 """@param before: chosen versions must be earlier than this
286 @param not_before: versions must be at least this high
287 @see: L{parse_version}; use this to pre-process the versions"""
288 self.before = before
289 self.not_before = not_before
290
292 """@type impl: L{Implementation}
293 @rtype: bool"""
294 if self.not_before and impl.version < self.not_before:
295 return False
296 if self.before and impl.version >= self.before:
297 return False
298 return True
299
301 """@rtype: str"""
302 if self.not_before is not None or self.before is not None:
303 range = ''
304 if self.not_before is not None:
305 range += format_version(self.not_before) + ' <= '
306 range += 'version'
307 if self.before is not None:
308 range += ' < ' + format_version(self.before)
309 else:
310 range = 'none'
311 return range
312
314 """Only versions for which the expression is true are acceptable.
315 @since: 1.13"""
316 __slots__ = ['expr', '_test_fn']
317
319 """Constructor.
320 @param expr: the expression, in the form "2.6..!3 | 3.2.2.."
321 @type expr: str"""
322 self.expr = expr
323 self._test_fn = versions.parse_version_expression(expr)
324
326 """@type impl: L{Implementation}
327 @rtype: bool"""
328 return self._test_fn(impl.version)
329
331 """@rtype: str"""
332 return "version " + self.expr
333
335 """A restriction that can never be met.
336 This is used when we can't understand some other restriction.
337 @since: 1.13"""
338
342
344 """@type impl: L{Implementation}
345 @rtype: bool"""
346 return False
347
349 """@rtype: str"""
350 return "<impossible: %s>" % self.reason
351
353 """A restriction that can only be satisfied by an implementation
354 from the given distribution.
355 For example, a MacPorts Python library requires us to select the MacPorts
356 version of Python too.
357 @since: 1.15"""
358 distros = None
359
361 """@type distros: str"""
362 self.distros = frozenset(distros.split(' '))
363
365 """@type impl: L{Implementation}
366 @rtype: bool"""
367 return impl.distro_name in self.distros
368
370 """@rtype: str"""
371 return "distro " + '|'.join(sorted(self.distros))
372
374 """Information about how the choice of a Dependency is made known
375 to the application being run."""
376
377 @property
379 """"Returns the name of the specific command needed by this binding, if any.
380 @since: 1.2"""
381 return None
382
384 """Indicate the chosen implementation using an environment variable."""
385 __slots__ = ['name', 'insert', 'default', 'mode', 'value']
386
387 PREPEND = 'prepend'
388 APPEND = 'append'
389 REPLACE = 'replace'
390
391 - def __init__(self, name, insert, default = None, mode = PREPEND, value=None, separator=None):
392 """
393 mode argument added in version 0.28
394 value argument added in version 0.52
395 """
396 self.name = name
397 self.insert = insert
398 self.default = default
399 self.mode = mode
400 self.value = value
401 if separator is None:
402 self.separator = os.pathsep
403 else:
404 self.separator = separator
405
406
408 return _("<environ %(name)s %(mode)s %(insert)s %(value)s>") % \
409 {'name': self.name, 'mode': self.mode, 'insert': self.insert, 'value': self.value}
410
411 __repr__ = __str__
412
414 """Calculate the new value of the environment variable after applying this binding.
415 @param path: the path to the selected implementation
416 @param old_value: the current value of the environment variable
417 @return: the new value for the environment variable"""
418
419 if self.insert is not None:
420 extra = os.path.join(path, self.insert)
421 else:
422 assert self.value is not None
423 extra = self.value
424
425 if self.mode == EnvironmentBinding.REPLACE:
426 return extra
427
428 if old_value is None:
429 old_value = self.default or defaults.get(self.name, None)
430 if old_value is None:
431 return extra
432 if self.mode == EnvironmentBinding.PREPEND:
433 return extra + self.separator + old_value
434 else:
435 return old_value + self.separator + extra
436
437 - def _toxml(self, doc, prefixes):
438 """Create a DOM element for this binding.
439 @param doc: document to use to create the element
440 @return: the new element
441 """
442 env_elem = doc.createElementNS(XMLNS_IFACE, 'environment')
443 env_elem.setAttributeNS(None, 'name', self.name)
444 if self.mode is not None:
445 env_elem.setAttributeNS(None, 'mode', self.mode)
446 if self.insert is not None:
447 env_elem.setAttributeNS(None, 'insert', self.insert)
448 else:
449 env_elem.setAttributeNS(None, 'value', self.value)
450 if self.default:
451 env_elem.setAttributeNS(None, 'default', self.default)
452 if self.separator:
453 env_elem.setAttributeNS(None, 'separator', self.separator)
454 return env_elem
455
457 """Make the chosen command available in $PATH.
458 @ivar in_path: True to add the named command to $PATH, False to store in named variable
459 @type in_path: bool
460 """
461 __slots__ = ['qdom']
462
464 self.qdom = qdom
465 self.in_path = in_path
466
469
470 __repr__ = __str__
471
472 - def _toxml(self, doc, prefixes):
474
475 @property
478
479 @property
482
484 """Make the chosen implementation available by overlaying it onto another part of the file-system.
485 This is to support legacy programs which use hard-coded paths."""
486 __slots__ = ['src', 'mount_point']
487
489 self.src = src
490 self.mount_point = mount_point
491
493 return _("<overlay %(src)s on %(mount_point)s>") % {'src': self.src or '.', 'mount_point': self.mount_point or '/'}
494
495 __repr__ = __str__
496
497 - def _toxml(self, doc, prefixes):
498 """Create a DOM element for this binding.
499 @param doc: document to use to create the element
500 @return: the new element
501 """
502 env_elem = doc.createElementNS(XMLNS_IFACE, 'overlay')
503 if self.src is not None:
504 env_elem.setAttributeNS(None, 'src', self.src)
505 if self.mount_point is not None:
506 env_elem.setAttributeNS(None, 'mount-point', self.mount_point)
507 return env_elem
508
510 """An interface's feeds are other interfaces whose implementations can also be
511 used as implementations of this interface."""
512 __slots__ = ['uri', 'os', 'machine', 'user_override', 'langs', 'site_package']
513 - def __init__(self, uri, arch, user_override, langs = None, site_package = False):
514 self.uri = uri
515
516
517 self.user_override = user_override
518 self.os, self.machine = _split_arch(arch)
519 self.langs = langs
520 self.site_package = site_package
521
523 return "<Feed from %s>" % self.uri
524 __repr__ = __str__
525
526 arch = property(lambda self: _join_arch(self.os, self.machine))
527
529 """A Dependency indicates that an Implementation requires some additional
530 code to function. This is an abstract base class.
531 @ivar qdom: the XML element for this Dependency (since 0launch 0.51)
532 @type qdom: L{qdom.Element}
533 @ivar metadata: any extra attributes from the XML element
534 @type metadata: {str: str}
535 """
536 __slots__ = ['qdom']
537
538 Essential = "essential"
539 Recommended = "recommended"
540 Restricts = "restricts"
541
543 """@type element: L{zeroinstall.injector.qdom.Element}"""
544 assert isinstance(element, qdom.Element), type(element)
545 self.qdom = element
546
547 @property
550
552 """Return a list of command names needed by this dependency"""
553 return []
554
556 """A Dependency that restricts the possible choices of a Zero Install interface.
557 @ivar interface: the interface required by this dependency
558 @type interface: str
559 @ivar restrictions: a list of constraints on acceptable implementations
560 @type restrictions: [L{Restriction}]
561 @since: 1.10
562 """
563 __slots__ = ['interface', 'restrictions']
564
565 - def __init__(self, interface, restrictions = None, element = None):
566 """@type interface: str
567 @type element: L{zeroinstall.injector.qdom.Element} | None"""
568 Dependency.__init__(self, element)
569 assert isinstance(interface, (str, support.unicode))
570 assert interface
571 self.interface = interface
572 if restrictions is None:
573 self.restrictions = []
574 else:
575 self.restrictions = restrictions
576
577 importance = Dependency.Restricts
578 bindings = ()
579
581 return _("<Restriction on %(interface)s; %(restrictions)s>") % {'interface': self.interface, 'restrictions': self.restrictions}
582
584 """A Dependency on a Zero Install interface.
585 @ivar interface: the interface required by this dependency
586 @type interface: str
587 @ivar restrictions: a list of constraints on acceptable implementations
588 @type restrictions: [L{Restriction}]
589 @ivar bindings: how to make the choice of implementation known
590 @type bindings: [L{Binding}]
591 @since: 0.28
592 """
593 __slots__ = ['bindings']
594
595 - def __init__(self, interface, restrictions = None, element = None):
600
602 """@rtype: str"""
603 return _("<Dependency on %(interface)s; bindings: %(bindings)s%(restrictions)s>") % {'interface': self.interface, 'bindings': self.bindings, 'restrictions': self.restrictions}
604
605 @property
608
620
621 @property
626
628 """A RetrievalMethod provides a way to fetch an implementation."""
629 __slots__ = []
630
632 """A DownloadSource provides a way to fetch an implementation."""
633 __slots__ = ['implementation', 'url', 'size', 'extract', 'start_offset', 'type', 'dest']
634
635 - def __init__(self, implementation, url, size, extract, start_offset = 0, type = None, dest = None):
636 """@type implementation: L{ZeroInstallImplementation}
637 @type url: str
638 @type size: int
639 @type extract: str
640 @type start_offset: int
641 @type type: str | None
642 @type dest: str | None"""
643 self.implementation = implementation
644 self.url = url
645 self.size = size
646 self.extract = extract
647 self.dest = dest
648 self.start_offset = start_offset
649 self.type = type
650
652 """A Rename provides a way to rename / move a file within an implementation."""
653 __slots__ = ['source', 'dest']
654
656 """@type source: str
657 @type dest: str"""
658 self.source = source
659 self.dest = dest
660
662 """A FileSource provides a way to fetch a single file."""
663 __slots__ = ['url', 'dest', 'size']
664
666 """@type url: str
667 @type dest: str
668 @type size: int"""
669 self.url = url
670 self.dest = dest
671 self.size = size
672
674 """A RemoveStep provides a way to delete a path within an implementation."""
675 __slots__ = ['path']
676
678 """@type path: str"""
679 self.path = path
680
681 -class Recipe(RetrievalMethod):
682 """Get an implementation by following a series of steps.
683 @ivar size: the combined download sizes from all the steps
684 @type size: int
685 @ivar steps: the sequence of steps which must be performed
686 @type steps: [L{RetrievalMethod}]"""
687 __slots__ = ['steps']
688
691
692 size = property(lambda self: sum([x.size for x in self.steps if hasattr(x, 'size')]))
693
695 """A package that is installed using the distribution's tools (including PackageKit).
696 @ivar install: a function to call to install this package
697 @type install: (L{handler.Handler}) -> L{tasks.Blocker}
698 @ivar package_id: the package name, in a form recognised by the distribution's tools
699 @type package_id: str
700 @ivar size: the download size in bytes
701 @type size: int
702 @ivar needs_confirmation: whether the user should be asked to confirm before calling install()
703 @type needs_confirmation: bool"""
704
705 __slots__ = ['package_id', 'size', 'install', 'needs_confirmation']
706
707 - def __init__(self, package_id, size, install, needs_confirmation = True):
708 """@type package_id: str
709 @type size: int
710 @type needs_confirmation: bool"""
711 RetrievalMethod.__init__(self)
712 self.package_id = package_id
713 self.size = size
714 self.install = install
715 self.needs_confirmation = needs_confirmation
716
718 """A Command is a way of running an Implementation as a program."""
719
720 __slots__ = ['qdom', '_depends', '_local_dir', '_runner', '_bindings']
721
723 """@param qdom: the <command> element
724 @type qdom: L{zeroinstall.injector.qdom.Element}
725 @param local_dir: the directory containing the feed (for relative dependencies), or None if not local"""
726 assert qdom.name == 'command', 'not <command>: %s' % qdom
727 self.qdom = qdom
728 self._local_dir = local_dir
729 self._depends = None
730 self._bindings = None
731
732 path = property(lambda self: self.qdom.attrs.get("path", None))
733
734 - def _toxml(self, doc, prefixes):
735 """@type prefixes: L{zeroinstall.injector.qdom.Prefixes}"""
736 return self.qdom.toDOM(doc, prefixes)
737
738 @property
740 if self._depends is None:
741 self._runner = None
742 depends = []
743 for child in self.qdom.childNodes:
744 if child.uri != XMLNS_IFACE: continue
745 if child.name in _dependency_names:
746 dep = process_depends(child, self._local_dir)
747 depends.append(dep)
748 elif child.name == 'runner':
749 if self._runner:
750 raise InvalidInterface(_("Multiple <runner>s in <command>!"))
751 dep = process_depends(child, self._local_dir)
752 depends.append(dep)
753 self._runner = dep
754 self._depends = depends
755 return self._depends
756
758 """@rtype: L{InterfaceDependency}"""
759 self.requires
760 return self._runner
761
764
765 @property
776
778 """An Implementation is a package which implements an Interface.
779 @ivar download_sources: list of methods of getting this implementation
780 @type download_sources: [L{RetrievalMethod}]
781 @ivar feed: the feed owning this implementation (since 0.32)
782 @type feed: [L{ZeroInstallFeed}]
783 @ivar bindings: how to tell this component where it itself is located (since 0.31)
784 @type bindings: [Binding]
785 @ivar upstream_stability: the stability reported by the packager
786 @type upstream_stability: [insecure | buggy | developer | testing | stable | packaged]
787 @ivar user_stability: the stability as set by the user
788 @type upstream_stability: [insecure | buggy | developer | testing | stable | packaged | preferred]
789 @ivar langs: natural languages supported by this package
790 @type langs: str
791 @ivar requires: interfaces this package depends on
792 @type requires: [L{Dependency}]
793 @ivar commands: ways to execute as a program
794 @type commands: {str: Command}
795 @ivar metadata: extra metadata from the feed
796 @type metadata: {"[URI ]localName": str}
797 @ivar id: a unique identifier for this Implementation
798 @ivar version: a parsed version number
799 @ivar released: release date
800 @ivar local_path: the directory containing this local implementation, or None if it isn't local (id isn't a path)
801 @type local_path: str | None
802 @ivar requires_root_install: whether the user will need admin rights to use this
803 @type requires_root_install: bool
804 """
805
806
807
808 __slots__ = ['upstream_stability', 'user_stability', 'langs',
809 'requires', 'metadata', 'download_sources', 'commands',
810 'id', 'feed', 'version', 'released', 'bindings', 'machine']
811
813 """@type feed: L{ZeroInstallFeed}
814 @type id: str"""
815 assert id
816 self.feed = feed
817 self.id = id
818 self.user_stability = None
819 self.upstream_stability = None
820 self.metadata = {}
821 self.requires = []
822 self.version = None
823 self.released = None
824 self.download_sources = []
825 self.langs = ""
826 self.machine = None
827 self.bindings = []
828 self.commands = {}
829
831 """@rtype: L{Stability}"""
832 return self.user_stability or self.upstream_stability or testing
833
835 """@rtype: str"""
836 return self.id
837
840
842 """Newer versions come first
843 @type other: L{Implementation}
844 @rtype: int"""
845 d = cmp(other.version, self.version)
846 if d: return d
847
848
849 d = cmp(other.feed.url, self.feed.url)
850 if d: return d
851 return cmp(other.id, self.id)
852
854 """@rtype: int"""
855 return self.id.__hash__()
856
858 """@type other: L{Implementation}
859 @rtype: bool"""
860 return self is other
861
873
875 """Return the version as a string.
876 @rtype: str
877 @see: L{format_version}"""
878 return format_version(self.version)
879
880 arch = property(lambda self: _join_arch(self.os, self.machine))
881
882 os = None
883 local_path = None
884 digests = None
885 requires_root_install = False
886
887 - def _get_main(self):
888 """"@deprecated: use commands["run"] instead
889 @rtype: str"""
890 main = self.commands.get("run", None)
891 if main is not None:
892 return main.path
893 return None
894 - def _set_main(self, path):
895 """"@deprecated: use commands["run"] instead"""
896 if path is None:
897 if "run" in self.commands:
898 del self.commands["run"]
899 else:
900 self.commands["run"] = Command(qdom.Element(XMLNS_IFACE, 'command', {'path': path, 'name': 'run'}), None)
901 main = property(_get_main, _set_main)
902
904 """Is this Implementation available locally?
905 (a local implementation, an installed distribution package, or a cached ZeroInstallImplementation)
906 @rtype: bool
907 @since: 0.53"""
908 raise NotImplementedError("abstract")
909
911 """An implementation provided by the distribution. Information such as the version
912 comes from the package manager.
913 @ivar package_implementation: the <package-implementation> element that generated this impl (since 1.7)
914 @type package_implementation: L{qdom.Element}
915 @since: 0.28"""
916 __slots__ = ['distro', 'installed', 'package_implementation', 'distro_name']
917
918 - def __init__(self, feed, id, distro, package_implementation = None, distro_name = None):
919 """@type feed: L{ZeroInstallFeed}
920 @type id: str
921 @type distro: L{zeroinstall.injector.distro.Distribution}
922 @type package_implementation: L{zeroinstall.injector.qdom.Element} | None
923 @type distro_name: str | None"""
924 assert id.startswith('package:')
925 Implementation.__init__(self, feed, id)
926 self.distro = distro
927 self.installed = False
928 self.package_implementation = package_implementation
929 self.distro_name = distro_name or distro.name
930
931 if package_implementation:
932 for child in package_implementation.childNodes:
933 if child.uri != XMLNS_IFACE: continue
934 if child.name == 'command':
935 command_name = child.attrs.get('name', None)
936 if not command_name:
937 raise InvalidInterface('Missing name for <command>')
938 self.commands[command_name] = Command(child, local_dir = None)
939
940 @property
942 return not self.installed
943
945 """@type stores: L{zeroinstall.zerostore.Stores}
946 @rtype: bool"""
947 return self.installed
948
950 """An implementation where all the information comes from Zero Install.
951 @ivar digests: a list of "algorith=value" or "algorith_value" strings (since 0.45)
952 @type digests: [str]
953 @since: 0.28"""
954 __slots__ = ['os', 'size', 'digests', 'local_path']
955
956 distro_name = '0install'
957
958 - def __init__(self, feed, id, local_path):
959 """id can be a local path (string starting with /) or a manifest hash (eg "sha1=XXX")
960 @type feed: L{ZeroInstallFeed}
961 @type id: str
962 @type local_path: str"""
963 assert not id.startswith('package:'), id
964 Implementation.__init__(self, feed, id)
965 self.size = None
966 self.os = None
967 self.digests = []
968 self.local_path = local_path
969
970
971 dependencies = property(lambda self: dict([(x.interface, x) for x in self.requires
972 if isinstance(x, InterfaceRestriction)]))
973
974 - def add_download_source(self, url, size, extract, start_offset = 0, type = None, dest = None):
975 """Add a download source.
976 @type url: str
977 @type size: int
978 @type extract: str
979 @type start_offset: int
980 @type type: str | None
981 @type dest: str | None"""
982 self.download_sources.append(DownloadSource(self, url, size, extract, start_offset, type, dest))
983
985 """@type arch: str"""
986 self.os, self.machine = _split_arch(arch)
987 arch = property(lambda self: _join_arch(self.os, self.machine), set_arch)
988
998
1000 """An Interface represents some contract of behaviour.
1001 @ivar uri: the URI for this interface.
1002 @ivar stability_policy: user's configured policy.
1003 Implementations at this level or higher are preferred.
1004 Lower levels are used only if there is no other choice.
1005 """
1006 __slots__ = ['uri', 'stability_policy', 'extra_feeds']
1007
1008 implementations = property(lambda self: self._main_feed.implementations)
1009 name = property(lambda self: self._main_feed.name)
1010 description = property(lambda self: self._main_feed.description)
1011 summary = property(lambda self: self._main_feed.summary)
1012 last_modified = property(lambda self: self._main_feed.last_modified)
1013 feeds = property(lambda self: self.extra_feeds + self._main_feed.feeds)
1014 metadata = property(lambda self: self._main_feed.metadata)
1015
1016 last_checked = property(lambda self: self._main_feed.last_checked)
1017
1019 """@type uri: str"""
1020 assert uri
1021 if uri.startswith('http:') or uri.startswith('https:') or os.path.isabs(uri):
1022 self.uri = uri
1023 else:
1024 raise SafeException(_("Interface name '%s' doesn't start "
1025 "with 'http:' or 'https:'") % uri)
1026 self.reset()
1027
1029 retval = {}
1030 for key in self._main_feed.feed_for:
1031 retval[key] = True
1032 return retval
1033 feed_for = property(_get_feed_for)
1034
1036 self.extra_feeds = []
1037 self.stability_policy = None
1038
1046
1048 """@rtype: str"""
1049 return _("<Interface %s>") % self.uri
1050
1052 """@type new: L{Stability}"""
1053 assert new is None or isinstance(new, Stability)
1054 self.stability_policy = new
1055
1057
1058
1059 for x in self.extra_feeds:
1060 if x.uri == url:
1061 return x
1062
1063 return None
1064
1067
1068 @property
1069 - def _main_feed(self):
1070 import warnings
1071 warnings.warn("use the feed instead", DeprecationWarning, 3)
1072 from zeroinstall.injector import policy
1073 iface_cache = policy.get_deprecated_singleton_config().iface_cache
1074 feed = iface_cache.get_feed(self.uri)
1075 if feed is None:
1076 return _dummy_feed
1077 return feed
1078
1080 """Add each attribute of item to a copy of attrs and return the copy.
1081 @type attrs: {str: str}
1082 @type item: L{qdom.Element}
1083 @rtype: {str: str}"""
1084 new = attrs.copy()
1085 for a in item.attrs:
1086 new[str(a)] = item.attrs[a]
1087 return new
1088
1090 """@type elem: L{zeroinstall.injector.qdom.Element}
1091 @type attr_name: str
1092 @rtype: int"""
1093 val = elem.getAttribute(attr_name)
1094 if val is not None:
1095 try:
1096 val = int(val)
1097 except ValueError:
1098 raise SafeException(_("Invalid value for integer attribute '%(attribute_name)s': %(value)s") % {'attribute_name': attr_name, 'value': val})
1099 return val
1100
1102 """A feed lists available implementations of an interface.
1103 @ivar url: the URL for this feed
1104 @ivar implementations: Implementations in this feed, indexed by ID
1105 @type implementations: {str: L{Implementation}}
1106 @ivar name: human-friendly name
1107 @ivar summaries: short textual description (in various languages, since 0.49)
1108 @type summaries: {str: str}
1109 @ivar descriptions: long textual description (in various languages, since 0.49)
1110 @type descriptions: {str: str}
1111 @ivar last_modified: timestamp on signature
1112 @ivar last_checked: time feed was last successfully downloaded and updated
1113 @ivar local_path: the path of this local feed, or None if remote (since 1.7)
1114 @type local_path: str | None
1115 @ivar feeds: list of <feed> elements in this feed
1116 @type feeds: [L{Feed}]
1117 @ivar feed_for: interfaces for which this could be a feed
1118 @type feed_for: set(str)
1119 @ivar metadata: extra elements we didn't understand
1120 """
1121
1122 __slots__ = ['url', 'implementations', 'name', 'descriptions', 'first_description', 'summaries', 'first_summary', '_package_implementations',
1123 'last_checked', 'last_modified', 'feeds', 'feed_for', 'metadata', 'local_path']
1124
1125 - def __init__(self, feed_element, local_path = None, distro = None):
1126 """Create a feed object from a DOM.
1127 @param feed_element: the root element of a feed file
1128 @type feed_element: L{qdom.Element}
1129 @param local_path: the pathname of this local feed, or None for remote feeds
1130 @type local_path: str | None"""
1131 self.local_path = local_path
1132 self.implementations = {}
1133 self.name = None
1134 self.summaries = {}
1135 self.first_summary = None
1136 self.descriptions = {}
1137 self.first_description = None
1138 self.last_modified = None
1139 self.feeds = []
1140 self.feed_for = set()
1141 self.metadata = []
1142 self.last_checked = None
1143 self._package_implementations = []
1144
1145 if distro is not None:
1146 import warnings
1147 warnings.warn("distro argument is now ignored", DeprecationWarning, 2)
1148
1149 if feed_element is None:
1150 return
1151
1152 if feed_element.name not in ('interface', 'feed'):
1153 raise SafeException("Root element should be <interface>, not <%s>" % feed_element.name)
1154 assert feed_element.uri == XMLNS_IFACE, "Wrong namespace on root element: %s" % feed_element.uri
1155
1156 main = feed_element.getAttribute('main')
1157
1158
1159 if local_path:
1160 self.url = local_path
1161 local_dir = os.path.dirname(local_path)
1162 else:
1163 assert local_path is None
1164 self.url = feed_element.getAttribute('uri')
1165 if not self.url:
1166 raise InvalidInterface(_("<interface> uri attribute missing"))
1167 local_dir = None
1168
1169 min_injector_version = feed_element.getAttribute('min-injector-version')
1170 if min_injector_version:
1171 if parse_version(min_injector_version) > parse_version(version):
1172 raise InvalidInterface(_("This feed requires version %(min_version)s or later of "
1173 "Zero Install, but I am only version %(version)s. "
1174 "You can get a newer version from http://0install.net") %
1175 {'min_version': min_injector_version, 'version': version})
1176
1177 for x in feed_element.childNodes:
1178 if x.uri != XMLNS_IFACE:
1179 self.metadata.append(x)
1180 continue
1181 if x.name == 'name':
1182 self.name = x.content
1183 elif x.name == 'description':
1184 if self.first_description == None:
1185 self.first_description = x.content
1186 self.descriptions[x.attrs.get("http://www.w3.org/XML/1998/namespace lang", 'en')] = x.content
1187 elif x.name == 'summary':
1188 if self.first_summary == None:
1189 self.first_summary = x.content
1190 self.summaries[x.attrs.get("http://www.w3.org/XML/1998/namespace lang", 'en')] = x.content
1191 elif x.name == 'feed-for':
1192 feed_iface = x.getAttribute('interface')
1193 if not feed_iface:
1194 raise InvalidInterface(_('Missing "interface" attribute in <feed-for>'))
1195 self.feed_for.add(feed_iface)
1196
1197
1198
1199 logger.debug(_("Is feed-for %s"), feed_iface)
1200 elif x.name == 'feed':
1201 feed_src = x.getAttribute('src')
1202 if not feed_src:
1203 raise InvalidInterface(_('Missing "src" attribute in <feed>'))
1204 if feed_src.startswith('http:') or feed_src.startswith('https:') or local_path:
1205 if feed_src.startswith('.'):
1206 feed_src = os.path.abspath(os.path.join(local_dir, feed_src))
1207
1208 langs = x.getAttribute('langs')
1209 if langs: langs = langs.replace('_', '-')
1210 self.feeds.append(Feed(feed_src, x.getAttribute('arch'), False, langs = langs))
1211 else:
1212 raise InvalidInterface(_("Invalid feed URL '%s'") % feed_src)
1213 else:
1214 self.metadata.append(x)
1215
1216 if not self.name:
1217 raise InvalidInterface(_("Missing <name> in feed"))
1218 if not self.summary:
1219 raise InvalidInterface(_("Missing <summary> in feed"))
1220
1221 def process_group(group, group_attrs, base_depends, base_bindings, base_commands):
1222 for item in group.childNodes:
1223 if item.uri != XMLNS_IFACE: continue
1224
1225 if item.name not in ('group', 'implementation', 'package-implementation'):
1226 continue
1227
1228
1229
1230
1231
1232
1233
1234
1235 depends = base_depends[:]
1236 bindings = base_bindings[:]
1237 commands = base_commands.copy()
1238
1239 for attr, command in [('main', 'run'),
1240 ('self-test', 'test')]:
1241 value = item.attrs.get(attr, None)
1242 if value is not None:
1243 commands[command] = Command(qdom.Element(XMLNS_IFACE, 'command', {'name': command, 'path': value}), None)
1244
1245 for child in item.childNodes:
1246 if child.uri != XMLNS_IFACE: continue
1247 if child.name in _dependency_names:
1248 dep = process_depends(child, local_dir)
1249 depends.append(dep)
1250 elif child.name == 'command':
1251 command_name = child.attrs.get('name', None)
1252 if not command_name:
1253 raise InvalidInterface('Missing name for <command>')
1254 commands[command_name] = Command(child, local_dir)
1255 elif child.name in binding_names:
1256 bindings.append(process_binding(child))
1257
1258 compile_command = item.attrs.get('http://zero-install.sourceforge.net/2006/namespaces/0compile command')
1259 if compile_command is not None:
1260 commands['compile'] = Command(qdom.Element(XMLNS_IFACE, 'command', {'name': 'compile', 'shell-command': compile_command}), None)
1261
1262 item_attrs = _merge_attrs(group_attrs, item)
1263
1264 if item.name == 'group':
1265 process_group(item, item_attrs, depends, bindings, commands)
1266 elif item.name == 'implementation':
1267 process_impl(item, item_attrs, depends, bindings, commands)
1268 elif item.name == 'package-implementation':
1269 self._package_implementations.append((item, item_attrs, depends))
1270 else:
1271 assert 0
1272
1273 def process_impl(item, item_attrs, depends, bindings, commands):
1274 id = item.getAttribute('id')
1275 if id is None:
1276 raise InvalidInterface(_("Missing 'id' attribute on %s") % item)
1277 local_path = item_attrs.get('local-path')
1278 if local_dir and local_path:
1279 abs_local_path = os.path.abspath(os.path.join(local_dir, local_path))
1280 impl = ZeroInstallImplementation(self, id, abs_local_path)
1281 elif local_dir and (id.startswith('/') or id.startswith('.')):
1282
1283 id = os.path.abspath(os.path.join(local_dir, id))
1284 impl = ZeroInstallImplementation(self, id, id)
1285 else:
1286 impl = ZeroInstallImplementation(self, id, None)
1287 if '=' in id:
1288
1289 impl.digests.append(id)
1290 if id in self.implementations:
1291 logger.warning(_("Duplicate ID '%(id)s' in feed '%(feed)s'"), {'id': id, 'feed': self})
1292 self.implementations[id] = impl
1293
1294 impl.metadata = item_attrs
1295 try:
1296 version_mod = item_attrs.get('version-modifier', None)
1297 if version_mod:
1298 item_attrs['version'] += version_mod
1299 del item_attrs['version-modifier']
1300 version = item_attrs['version']
1301 except KeyError:
1302 raise InvalidInterface(_("Missing version attribute"))
1303 impl.version = parse_version(version)
1304
1305 impl.commands = commands
1306
1307 impl.released = item_attrs.get('released', None)
1308 impl.langs = item_attrs.get('langs', '').replace('_', '-')
1309
1310 size = item.getAttribute('size')
1311 if size:
1312 impl.size = int(size)
1313 impl.arch = item_attrs.get('arch', None)
1314 try:
1315 stability = stability_levels[str(item_attrs['stability'])]
1316 except KeyError:
1317 stab = str(item_attrs['stability'])
1318 if stab != stab.lower():
1319 raise InvalidInterface(_('Stability "%s" invalid - use lower case!') % item_attrs.stability)
1320 raise InvalidInterface(_('Stability "%s" invalid') % item_attrs['stability'])
1321 if stability >= preferred:
1322 raise InvalidInterface(_("Upstream can't set stability to preferred!"))
1323 impl.upstream_stability = stability
1324
1325 impl.bindings = bindings
1326 impl.requires = depends
1327
1328 def extract_file_source(elem):
1329 url = elem.getAttribute('href')
1330 if not url:
1331 raise InvalidInterface(_("Missing href attribute on <file>"))
1332 dest = elem.getAttribute('dest')
1333 if not dest:
1334 raise InvalidInterface(_("Missing dest attribute on <file>"))
1335 size = elem.getAttribute('size')
1336 if not size:
1337 raise InvalidInterface(_("Missing size attribute on <file>"))
1338 return FileSource(url, dest, int(size))
1339
1340 for elem in item.childNodes:
1341 if elem.uri != XMLNS_IFACE: continue
1342 if elem.name == 'archive':
1343 url = elem.getAttribute('href')
1344 if not url:
1345 raise InvalidInterface(_("Missing href attribute on <archive>"))
1346 size = elem.getAttribute('size')
1347 if not size:
1348 raise InvalidInterface(_("Missing size attribute on <archive>"))
1349 impl.add_download_source(url = url, size = int(size),
1350 extract = elem.getAttribute('extract'),
1351 start_offset = _get_long(elem, 'start-offset'),
1352 type = elem.getAttribute('type'),
1353 dest = elem.getAttribute('dest'))
1354 elif elem.name == 'file':
1355 impl.download_sources.append(extract_file_source(elem))
1356
1357 elif elem.name == 'manifest-digest':
1358 for aname, avalue in elem.attrs.items():
1359 if ' ' not in aname:
1360 impl.digests.append(zerostore.format_algorithm_digest_pair(aname, avalue))
1361 elif elem.name == 'recipe':
1362 recipe = Recipe()
1363 for recipe_step in elem.childNodes:
1364 if recipe_step.uri == XMLNS_IFACE and recipe_step.name == 'archive':
1365 url = recipe_step.getAttribute('href')
1366 if not url:
1367 raise InvalidInterface(_("Missing href attribute on <archive>"))
1368 size = recipe_step.getAttribute('size')
1369 if not size:
1370 raise InvalidInterface(_("Missing size attribute on <archive>"))
1371 recipe.steps.append(DownloadSource(None, url = url, size = int(size),
1372 extract = recipe_step.getAttribute('extract'),
1373 start_offset = _get_long(recipe_step, 'start-offset'),
1374 type = recipe_step.getAttribute('type'),
1375 dest = recipe_step.getAttribute('dest')))
1376 elif recipe_step.name == 'file':
1377 recipe.steps.append(extract_file_source(recipe_step))
1378 elif recipe_step.uri == XMLNS_IFACE and recipe_step.name == 'rename':
1379 source = recipe_step.getAttribute('source')
1380 if not source:
1381 raise InvalidInterface(_("Missing source attribute on <rename>"))
1382 dest = recipe_step.getAttribute('dest')
1383 if not dest:
1384 raise InvalidInterface(_("Missing dest attribute on <rename>"))
1385 recipe.steps.append(RenameStep(source=source, dest=dest))
1386 elif recipe_step.uri == XMLNS_IFACE and recipe_step.name == 'remove':
1387 path = recipe_step.getAttribute('path')
1388 if not path:
1389 raise InvalidInterface(_("Missing path attribute on <remove>"))
1390 recipe.steps.append(RemoveStep(path=path))
1391 else:
1392 logger.info(_("Unknown step '%s' in recipe; skipping recipe"), recipe_step.name)
1393 break
1394 else:
1395 impl.download_sources.append(recipe)
1396
1397 root_attrs = {'stability': 'testing'}
1398 root_commands = {}
1399 if main:
1400 logger.info("Note: @main on document element is deprecated in %s", self)
1401 root_commands['run'] = Command(qdom.Element(XMLNS_IFACE, 'command', {'path': main, 'name': 'run'}), None)
1402 process_group(feed_element, root_attrs, [], [], root_commands)
1403
1405 """Does this feed contain any <pacakge-implementation> elements?
1406 i.e. is it worth asking the package manager for more information?
1407 @return: the URL of the virtual feed, or None
1408 @rtype: str
1409 @since: 0.49"""
1410 if self._package_implementations:
1411 return "distribution:" + self.url
1412 return None
1413
1415 """Find the best <pacakge-implementation> element(s) for the given distribution.
1416 @param distro: the distribution to use to rate them
1417 @type distro: L{distro.Distribution}
1418 @return: a list of tuples for the best ranked elements
1419 @rtype: [str]
1420 @since: 0.49"""
1421 best_score = 0
1422 best_impls = []
1423
1424 for item, item_attrs, depends in self._package_implementations:
1425 distro_names = item_attrs.get('distributions', '')
1426 score_this_item = max(
1427 distro.get_score(distro_name) if distro_name else 0.5
1428 for distro_name in distro_names.split(' '))
1429 if score_this_item > best_score:
1430 best_score = score_this_item
1431 best_impls = []
1432 if score_this_item == best_score:
1433 best_impls.append((item, item_attrs, depends))
1434 return best_impls
1435
1437 """@rtype: str"""
1438 return self.name or '(' + os.path.basename(self.url) + ')'
1439
1441 return _("<Feed %s>") % self.url
1442
1444 assert new is None or isinstance(new, Stability)
1445 self.stability_policy = new
1446
1448 for x in self.feeds:
1449 if x.uri == url:
1450 return x
1451 return None
1452
1455
1461
1462 @property
1464 return _best_language_match(self.summaries) or self.first_summary
1465
1466 @property
1468 return _best_language_match(self.descriptions) or self.first_description
1469
1471 """Return the URI of the interface that replaced the one with the URI of this feed's URL.
1472 This is the value of the feed's <replaced-by interface'...'/> element.
1473 @return: the new URI, or None if it hasn't been replaced
1474 @rtype: str | None
1475 @since: 1.7"""
1476 for child in self.metadata:
1477 if child.uri == XMLNS_IFACE and child.name == 'replaced-by':
1478 new_uri = child.getAttribute('interface')
1479 if new_uri and (new_uri.startswith('http:') or new_uri.startswith('https:') or self.local_path):
1480 return new_uri
1481 return None
1482
1495 _dummy_feed = DummyFeed()
1496
1497 if sys.version_info[0] > 2:
1498
1499
1500 from functools import total_ordering
1501
1502
1503
1504
1505 - def unescape(uri):
1506 """Convert each %20 to a space, etc.
1507 @type uri: str
1508 @rtype: str"""
1509 uri = uri.replace('#', '/')
1510 if '%' not in uri: return uri
1511 return re.sub(b'%[0-9a-fA-F][0-9a-fA-F]',
1512 lambda match: bytes([int(match.group(0)[1:], 16)]),
1513 uri.encode('ascii')).decode('utf-8')
1514
1516 """Convert each space to %20, etc
1517 @type uri: str
1518 @rtype: str"""
1519 return re.sub(b'[^-_.a-zA-Z0-9]',
1520 lambda match: ('%%%02x' % ord(match.group(0))).encode('ascii'),
1521 uri.encode('utf-8')).decode('ascii')
1522
1524 """Convert each space to %20, etc
1525 : is preserved and / becomes #. This makes for nicer strings,
1526 and may replace L{escape} everywhere in future.
1527 @type uri: str
1528 @rtype: str"""
1529 if os.name == "posix":
1530
1531 preserveRegex = b'[^-_.a-zA-Z0-9:/]'
1532 else:
1533
1534 preserveRegex = b'[^-_.a-zA-Z0-9/]'
1535 return re.sub(preserveRegex,
1536 lambda match: ('%%%02x' % ord(match.group(0))).encode('ascii'),
1537 uri.encode('utf-8')).decode('ascii').replace('/', '#')
1538 else:
1542 """Convert each %20 to a space, etc.
1543 @type uri: str
1544 @rtype: str"""
1545 uri = uri.replace('#', '/')
1546 if '%' not in uri: return uri
1547 return re.sub('%[0-9a-fA-F][0-9a-fA-F]',
1548 lambda match: chr(int(match.group(0)[1:], 16)),
1549 uri).decode('utf-8')
1550
1552 """Convert each space to %20, etc
1553 @type uri: str
1554 @rtype: str"""
1555 return re.sub('[^-_.a-zA-Z0-9]',
1556 lambda match: '%%%02x' % ord(match.group(0)),
1557 uri.encode('utf-8'))
1558
1560 """Convert each space to %20, etc
1561 : is preserved and / becomes #. This makes for nicer strings,
1562 and may replace L{escape} everywhere in future.
1563 @type uri: str
1564 @rtype: str"""
1565 if os.name == "posix":
1566
1567 preserveRegex = '[^-_.a-zA-Z0-9:/]'
1568 else:
1569
1570 preserveRegex = '[^-_.a-zA-Z0-9/]'
1571 return re.sub(preserveRegex,
1572 lambda match: '%%%02x' % ord(match.group(0)),
1573 uri.encode('utf-8')).replace('/', '#')
1574
1576 """Convert an interface URI to a list of path components.
1577 e.g. "http://example.com/foo.xml" becomes ["http", "example.com", "foo.xml"], while
1578 "file:///root/feed.xml" becomes ["file", "root__feed.xml"]
1579 The number of components is determined by the scheme (three for http, two for file).
1580 Uses L{support.escaping.underscore_escape} to escape each component.
1581 @type uri: str
1582 @rtype: [str]"""
1583 if uri.startswith('http://') or uri.startswith('https://'):
1584 scheme, rest = uri.split('://', 1)
1585 parts = rest.split('/', 1)
1586 else:
1587 assert os.path.isabs(uri), uri
1588 scheme = 'file'
1589 parts = [uri[1:]]
1590
1591 return [scheme] + [escaping.underscore_escape(part) for part in parts]
1592
1594 """If uri is a relative path, convert to an absolute one.
1595 A "file:///foo" URI is converted to "/foo".
1596 An "alias:prog" URI expands to the URI in the 0alias script
1597 Otherwise, return it unmodified.
1598 @type uri: str
1599 @rtype: str
1600 @raise SafeException: if uri isn't valid"""
1601 if uri.startswith('http://') or uri.startswith('https://'):
1602 if uri.count("/") < 3:
1603 raise SafeException(_("Missing / after hostname in URI '%s'") % uri)
1604 return uri
1605 elif uri.startswith('file:///'):
1606 path = uri[7:]
1607 elif uri.startswith('file:'):
1608 if uri[5] == '/':
1609 raise SafeException(_('Use file:///path for absolute paths, not {uri}').format(uri = uri))
1610 path = os.path.abspath(uri[5:])
1611 elif uri.startswith('alias:'):
1612 from zeroinstall import alias
1613 alias_prog = uri[6:]
1614 if not os.path.isabs(alias_prog):
1615 full_path = support.find_in_path(alias_prog)
1616 if not full_path:
1617 raise alias.NotAnAliasScript("Not found in $PATH: " + alias_prog)
1618 else:
1619 full_path = alias_prog
1620 return alias.parse_script(full_path).uri
1621 else:
1622 path = os.path.realpath(uri)
1623
1624 if os.path.isfile(path):
1625 return path
1626
1627 if '/' not in uri:
1628 alias_path = support.find_in_path(uri)
1629 if alias_path is not None:
1630 from zeroinstall import alias
1631 try:
1632 alias.parse_script(alias_path)
1633 except alias.NotAnAliasScript:
1634 pass
1635 else:
1636 raise SafeException(_("Bad interface name '{uri}'.\n"
1637 "(hint: try 'alias:{uri}' instead)".format(uri = uri)))
1638
1639 raise SafeException(_("Bad interface name '%(uri)s'.\n"
1640 "(doesn't start with 'http:', and "
1641 "doesn't exist as a local file '%(interface_uri)s' either)") %
1642 {'uri': uri, 'interface_uri': path})
1643