Read min-version and max-version attributes on <requires> elements.
[zeroinstall.git] / zeroinstall / injector / reader.py
blob0c17eb25c9f80cf2194e513b9a640c172c61ea4c
1 # Copyright (C) 2006, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import os
5 import sys
6 import shutil
7 import time
8 from logging import debug, warn
9 from os.path import dirname
11 from zeroinstall import version
12 from zeroinstall.injector import basedir, qdom
13 from zeroinstall.injector.namespaces import *
14 from zeroinstall.injector.model import *
16 class InvalidInterface(SafeException):
17 def __init__(self, message, ex = None):
18 if ex:
19 message += "\n\n(exact error: %s)" % ex
20 SafeException.__init__(self, message)
22 def get_singleton_text(parent, ns, localName):
23 elem = None
24 for x in parent.childNodes:
25 if x.uri == ns and x.name == localName:
26 if elem:
27 raise InvalidInterface('Multiple <%s> elements in <%s>' % (localName, parent.name))
28 elem = x
29 if elem:
30 return elem.content
31 raise InvalidInterface('No <%s> element in <%s>' % (localName, parent.name))
33 class Attrs(object):
34 __slots__ = ['version', 'released', 'arch', 'stability', 'main']
35 def __init__(self, **kwargs):
36 for x in self.__slots__:
37 setattr(self, x, kwargs.get(x, None))
39 def merge(self, item):
40 new = Attrs()
41 for x in self.__slots__:
42 value = item.attrs.get(x, None)
43 if value is None:
44 value = getattr(self, x)
45 setattr(new, x, value)
46 return new
48 def parse_version(version_string):
49 if version_string is None: return None
50 try:
51 return map(int, version_string.split('.'))
52 except ValueError, ex:
53 raise InvalidInterface("Invalid version format in '%s': %s" % (version_string, ex))
55 def process_depends(dependency, item):
56 dependency.min_version = parse_version(item.getAttribute('min-version'))
57 dependency.max_version = parse_version(item.getAttribute('max-version'))
58 for e in item.childNodes:
59 if e.uri == XMLNS_IFACE and e.name == 'environment':
60 binding = EnvironmentBinding(e.getAttribute('name'),
61 insert = e.getAttribute('insert'),
62 default = e.getAttribute('default'))
63 dependency.bindings.append(binding)
65 def update_from_cache(interface):
66 """True if cached version and user overrides loaded OK.
67 False if upstream not cached. Local interfaces (starting with /) are
68 always considered to be cached, although they are not stored there."""
69 interface.reset()
71 if interface.uri.startswith('/'):
72 debug("Loading local interface file '%s'", interface.uri)
73 update(interface, interface.uri, local = True)
74 interface.last_modified = os.stat(interface.uri).st_mtime
75 cached = True
76 else:
77 cached = basedir.load_first_cache(config_site, 'interfaces', escape(interface.uri))
78 if cached:
79 debug("Loading cached information for %s from %s", interface, cached)
80 update(interface, cached)
82 update_user_overrides(interface)
84 # Special case: add our fall-back local copy of the injector as a feed
85 if interface.uri == injector_gui_uri:
86 local_gui = os.path.join(os.path.abspath(dirname(dirname(__file__))), '0launch-gui', 'injector-gui.xml')
87 interface.feeds.append(Feed(local_gui, None, False))
89 return bool(cached)
91 def update_user_overrides(interface):
92 user = basedir.load_first_config(config_site, config_prog,
93 'user_overrides', escape(interface.uri))
94 if not user:
95 return
97 root = qdom.parse(file(user))
99 last_checked = root.getAttribute('last-checked')
100 if last_checked:
101 interface.last_checked = int(last_checked)
103 stability_policy = root.getAttribute('stability-policy')
104 if stability_policy:
105 interface.set_stability_policy(stability_levels[str(stability_policy)])
107 for item in root.childNodes:
108 if item.uri != XMLNS_IFACE: continue
109 if item.name == 'implementation':
110 id = item.getAttribute('id')
111 assert id is not None
112 if id.startswith('/'):
113 impl = interface.get_impl(id)
114 else:
115 assert '=' in id
116 impl = interface.get_impl(id)
118 user_stability = item.getAttribute('user-stability')
119 if user_stability:
120 impl.user_stability = stability_levels[str(user_stability)]
121 elif item.name == 'feed':
122 feed_src = item.getAttribute('src')
123 if not feed_src:
124 raise InvalidInterface('Missing "src" attribute in <feed>')
125 interface.feeds.append(Feed(feed_src, item.getAttribute('arch'), True))
127 def check_readable(interface_uri, source):
128 """Returns the modified time in 'source'. If syntax is incorrect,
129 throws an exception."""
130 tmp = Interface(interface_uri)
131 try:
132 update(tmp, source)
133 except InvalidInterface, ex:
134 raise InvalidInterface("Error loading interface:\n"
135 "Interface URI: %s\n"
136 "Local file: %s\n%s" %
137 (interface_uri, source, ex))
138 return tmp.last_modified
140 def parse_time(t):
141 try:
142 return long(t)
143 except Exception, ex:
144 raise InvalidInterface("Date '%s' not in correct format (should be integer number "
145 "of seconds since Unix epoch)\n%s" % (t, ex))
147 def _check_canonical_name(interface, source, root):
148 "Ensure the uri= attribute in the interface file matches the interface we are trying to load"
149 canonical_name = root.getAttribute('uri')
150 if not canonical_name:
151 raise InvalidInterface("<interface> uri attribute missing in " + source)
152 if canonical_name != interface.uri:
153 raise InvalidInterface("<interface> uri attribute is '%s', but accessed as '%s'\n(%s)" %
154 (canonical_name, interface.uri, source))
156 def update(interface, source, local = False):
157 """local - use file mtime for last-modified, and uri attribute is ignored"""
158 assert isinstance(interface, Interface)
160 try:
161 root = qdom.parse(file(source))
162 except Exception, ex:
163 raise InvalidInterface("Invalid XML", ex)
165 if not local:
166 _check_canonical_name(interface, source, root)
167 time_str = root.getAttribute('last-modified')
168 if not time_str:
169 raise InvalidInterface("Missing last-modified attribute on root element.")
170 interface.last_modified = parse_time(time_str)
171 main = root.getAttribute('main')
172 if main:
173 interface.main = main
175 min_injector_version = root.getAttribute('min-injector-version')
176 if min_injector_version:
177 try:
178 min_ints = map(int, min_injector_version.split('.'))
179 except ValueError, ex:
180 raise InvalidInterface("Bad version number '%s'" % min_injector_version)
181 injector_version = map(int, version.split('.'))
182 if min_ints > injector_version:
183 raise InvalidInterface("This interface requires version %s or later of "
184 "the Zero Install injector, but I am only version %s. "
185 "You can get a newer version from http://0install.net" %
186 (min_injector_version, version))
188 if local:
189 iface_dir = os.path.dirname(source)
190 else:
191 iface_dir = None # Can't have relative paths
193 for x in root.childNodes:
194 if x.uri != XMLNS_IFACE:
195 interface.add_metadata(x)
196 continue
197 if x.name == 'name':
198 interface.name = interface.name or x.content
199 elif x.name == 'description':
200 interface.description = interface.description or x.content
201 elif x.name == 'summary':
202 interface.summary = interface.summary or x.content
203 elif x.name == 'feed-for':
204 feed_iface = x.getAttribute('interface')
205 if not feed_iface:
206 raise InvalidInterface('Missing "interface" attribute in <feed-for>')
207 interface.feed_for[feed_iface] = True
208 elif x.name == 'feed':
209 feed_src = x.getAttribute('src')
210 if not feed_src:
211 raise InvalidInterface('Missing "src" attribute in <feed>')
212 if feed_src.startswith('http:') or local:
213 interface.feeds.append(Feed(feed_src, x.getAttribute('arch'), False))
214 else:
215 raise InvalidInterface("Invalid feed URL '%s'" % feed_src)
216 else:
217 interface.add_metadata(x)
219 def process_group(group, group_attrs, base_depends):
220 for item in group.childNodes:
221 if item.uri != XMLNS_IFACE: continue
223 depends = base_depends.copy()
225 item_attrs = group_attrs.merge(item)
227 for child in item.childNodes:
228 if child.uri != XMLNS_IFACE: continue
229 if child.name == 'requires':
230 dep_iface = child.getAttribute('interface')
231 if dep_iface is None:
232 raise InvalidInterface("Missing 'interface' on <requires>")
233 dep = Dependency(dep_iface)
234 process_depends(dep, child)
235 depends[dep.interface] = dep
237 if item.name == 'group':
238 process_group(item, item_attrs, depends)
239 elif item.name == 'implementation':
240 process_impl(item, item_attrs, depends)
242 def process_impl(item, item_attrs, depends):
243 id = item.getAttribute('id')
244 if id is None:
245 raise InvalidInterface("Missing 'id' attribute on %s" % item)
246 if local and (id.startswith('/') or id.startswith('.')):
247 impl = interface.get_impl(os.path.abspath(os.path.join(iface_dir, id)))
248 else:
249 if '=' not in id:
250 raise InvalidInterface('Invalid "id"; form is "alg=value" (got "%s")' % id)
251 alg, sha1 = id.split('=')
252 try:
253 long(sha1, 16)
254 except Exception, ex:
255 raise InvalidInterface('Bad SHA1 attribute: %s' % ex)
256 impl = interface.get_impl(id)
258 version = item_attrs.version
259 if not version:
260 raise InvalidInterface("Missing version attribute")
261 impl.version = map(int, version.split('.'))
263 impl.main = item_attrs.main
265 if item_attrs.released:
266 impl.released = item_attrs.released
268 size = item.getAttribute('size')
269 if size:
270 impl.size = long(size)
271 impl.arch = item_attrs.arch
272 try:
273 stability = stability_levels[str(item_attrs.stability)]
274 except KeyError:
275 stab = str(item_attrs.stability)
276 if stab != stab.lower():
277 raise InvalidInterface('Stability "%s" invalid - use lower case!' % item_attrs.stability)
278 raise InvalidInterface('Stability "%s" invalid' % item_attrs.stability)
279 if stability >= preferred:
280 raise InvalidInterface("Upstream can't set stability to preferred!")
281 impl.upstream_stability = stability
283 impl.dependencies.update(depends)
285 for elem in item.childNodes:
286 if elem.uri == XMLNS_IFACE and elem.name == 'archive':
287 url = elem.getAttribute('href')
288 if not url:
289 raise InvalidInterface("Missing href attribute on <archive>")
290 size = elem.getAttribute('size')
291 if not size:
292 raise InvalidInterface("Missing size attribute on <archive>")
293 impl.add_download_source(url = url, size = long(size),
294 extract = elem.getAttribute('extract'))
296 process_group(root,
297 Attrs(stability = testing,
298 main = root.getAttribute('main') or None),