When we start checking for updates on a feed, record the time. In the GUI, if this...
[zeroinstall.git] / zeroinstall / injector / reader.py
blobebe13c1609b68d1cd777cd02e25c46774d924314
1 """
2 Parses an XML interface into a Python representation.
3 """
5 # Copyright (C) 2006, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
8 import os
9 import sys
10 import shutil
11 import time
12 from logging import debug, warn, info
13 from os.path import dirname
15 from zeroinstall.injector import basedir, qdom
16 from zeroinstall.injector.namespaces import *
17 from zeroinstall.injector.model import *
18 from zeroinstall import version, SafeException
20 class InvalidInterface(SafeException):
21 """Raised when parsing an invalid interface."""
22 def __init__(self, message, ex = None):
23 if ex:
24 message += "\n\n(exact error: %s)" % ex
25 SafeException.__init__(self, message)
27 def _process_depends(dependency, item):
28 for e in item.childNodes:
29 if e.uri != XMLNS_IFACE: continue
30 if e.name == 'environment':
31 binding = EnvironmentBinding(e.getAttribute('name'),
32 insert = e.getAttribute('insert'),
33 default = e.getAttribute('default'))
34 dependency.bindings.append(binding)
35 elif e.name == 'version':
36 dependency.restrictions.append(
37 Restriction(not_before = parse_version(e.getAttribute('not-before')),
38 before = parse_version(e.getAttribute('before'))))
40 def update_from_cache(interface):
41 """Read a cached interface and any user overrides.
42 @param interface: the interface object to update
43 @type interface: L{model.Interface}
44 @return: True if cached version and user overrides loaded OK.
45 False if upstream not cached. Local interfaces (starting with /) are
46 always considered to be cached, although they are not actually stored in the cache.
47 @rtype: bool"""
48 interface.reset()
50 if interface.uri.startswith('/'):
51 debug("Loading local interface file '%s'", interface.uri)
52 update(interface, interface.uri, local = True)
53 interface.last_modified = int(os.stat(interface.uri).st_mtime)
54 cached = True
55 else:
56 cached = basedir.load_first_cache(config_site, 'interfaces', escape(interface.uri))
57 if cached:
58 debug("Loading cached information for %s from %s", interface, cached)
59 update(interface, cached)
61 update_user_overrides(interface)
63 # Special case: add our fall-back local copy of the injector as a feed
64 if interface.uri == injector_gui_uri:
65 local_gui = os.path.join(os.path.abspath(dirname(dirname(__file__))), '0launch-gui', 'ZeroInstall-GUI.xml')
66 interface.feeds.append(Feed(local_gui, None, False))
68 return bool(cached)
70 def update_user_overrides(interface):
71 """Update an interface with user-supplied information.
72 @param interface: the interface object to update
73 @type interface: L{model.Interface}"""
74 user = basedir.load_first_config(config_site, config_prog,
75 'user_overrides', escape(interface.uri))
76 if not user:
77 return
79 root = qdom.parse(file(user))
81 last_checked = root.getAttribute('last-checked')
82 if last_checked:
83 interface.last_checked = int(last_checked)
85 last_check_attempt = root.getAttribute('last-check-attempt')
86 if last_check_attempt:
87 interface.last_check_attempt = int(last_check_attempt)
89 stability_policy = root.getAttribute('stability-policy')
90 if stability_policy:
91 interface.set_stability_policy(stability_levels[str(stability_policy)])
93 for item in root.childNodes:
94 if item.uri != XMLNS_IFACE: continue
95 if item.name == 'implementation':
96 id = item.getAttribute('id')
97 assert id is not None
98 if not (id.startswith('/') or id.startswith('.')):
99 assert '=' in id
100 impl = interface.implementations.get(id, None)
101 if not impl:
102 debug("Ignoring user-override for unknown implementation %s in %s", id, interface)
103 continue
105 user_stability = item.getAttribute('user-stability')
106 if user_stability:
107 impl.user_stability = stability_levels[str(user_stability)]
108 elif item.name == 'feed':
109 feed_src = item.getAttribute('src')
110 if not feed_src:
111 raise InvalidInterface('Missing "src" attribute in <feed>')
112 interface.feeds.append(Feed(feed_src, item.getAttribute('arch'), True))
114 def check_readable(interface_uri, source):
115 """Test whether an interface file is valid.
116 @param interface_uri: the interface's URI
117 @type interface_uri: str
118 @param source: the name of the file to test
119 @type source: str
120 @return: the modification time in src (usually just the mtime of the file)
121 @rtype: int
122 @raise InvalidInterface: If the source's syntax is incorrect,
124 tmp = Interface(interface_uri)
125 try:
126 update(tmp, source)
127 except InvalidInterface, ex:
128 raise InvalidInterface("Error loading interface:\n"
129 "Interface URI: %s\n"
130 "Local file: %s\n%s" %
131 (interface_uri, source, ex))
132 return tmp.last_modified
134 def _parse_time(t):
135 try:
136 return long(t)
137 except Exception, ex:
138 raise InvalidInterface("Date '%s' not in correct format (should be integer number "
139 "of seconds since Unix epoch)\n%s" % (t, ex))
141 def _check_canonical_name(interface, source, root):
142 "Ensure the uri= attribute in the interface file matches the interface we are trying to load"
143 canonical_name = root.getAttribute('uri')
144 if not canonical_name:
145 raise InvalidInterface("<interface> uri attribute missing in " + source)
146 if canonical_name != interface.uri:
147 raise InvalidInterface("<interface> uri attribute is '%s', but accessed as '%s'\n(%s)" %
148 (canonical_name, interface.uri, source))
150 def _get_long(elem, attr_name):
151 val = elem.getAttribute(attr_name)
152 if val is not None:
153 try:
154 val = long(val)
155 except ValueError, ex:
156 raise SafeException("Invalid value for integer attribute '%s': %s" % (attr_name, val))
157 return val
159 def _merge_attrs(attrs, item):
160 """Add each attribute of item to a copy of attrs and return the copy.
161 @type attrs: {str: str}
162 @type item: L{qdom.Element}
163 @rtype: {str: str}
165 new = attrs.copy()
166 for a in item.attrs:
167 new[str(a)] = item.attrs[a]
168 return new
170 def update(interface, source, local = False):
171 """Read in information about an interface.
172 @param interface: the interface object to update
173 @type interface: L{model.Interface}
174 @param source: the name of the file to read
175 @type source: str
176 @param local: use file's mtime for last-modified, and uri attribute is ignored
177 @raise InvalidInterface: if the source's syntax is incorrect
178 @see: L{update_from_cache}, which calls this"""
179 assert isinstance(interface, Interface)
181 try:
182 root = qdom.parse(file(source))
183 except Exception, ex:
184 raise InvalidInterface("Invalid XML", ex)
186 if not local:
187 _check_canonical_name(interface, source, root)
188 time_str = root.getAttribute('last-modified')
189 if time_str:
190 # Old style cached items use an attribute
191 interface.last_modified = _parse_time(time_str)
192 else:
193 # New style items have the mtime in the signature,
194 # but for quick access we use the mtime of the file
195 interface.last_modified = int(os.stat(source).st_mtime)
196 main = root.getAttribute('main')
197 if main:
198 interface.main = main
200 min_injector_version = root.getAttribute('min-injector-version')
201 if min_injector_version:
202 try:
203 min_ints = map(int, min_injector_version.split('.'))
204 except ValueError, ex:
205 raise InvalidInterface("Bad version number '%s'" % min_injector_version)
206 injector_version = map(int, version.split('.'))
207 if min_ints > injector_version:
208 raise InvalidInterface("This interface requires version %s or later of "
209 "the Zero Install injector, but I am only version %s. "
210 "You can get a newer version from http://0install.net" %
211 (min_injector_version, version))
213 if local:
214 iface_dir = os.path.dirname(source)
215 else:
216 iface_dir = None # Can't have relative paths
218 for x in root.childNodes:
219 if x.uri != XMLNS_IFACE:
220 interface.add_metadata(x)
221 continue
222 if x.name == 'name':
223 interface.name = interface.name or x.content
224 elif x.name == 'description':
225 interface.description = interface.description or x.content
226 elif x.name == 'summary':
227 interface.summary = interface.summary or x.content
228 elif x.name == 'feed-for':
229 feed_iface = x.getAttribute('interface')
230 if not feed_iface:
231 raise InvalidInterface('Missing "interface" attribute in <feed-for>')
232 interface.feed_for[feed_iface] = True
233 # Bug report from a Debian/stable user that --feed gets the wrong value.
234 # Can't reproduce (even in a Debian/stable chroot), but add some logging here
235 # in case it happens again.
236 debug("Is feed-for %s", feed_iface)
237 elif x.name == 'feed':
238 feed_src = x.getAttribute('src')
239 if not feed_src:
240 raise InvalidInterface('Missing "src" attribute in <feed>')
241 if feed_src.startswith('http:') or local:
242 interface.feeds.append(Feed(feed_src, x.getAttribute('arch'), False))
243 else:
244 raise InvalidInterface("Invalid feed URL '%s'" % feed_src)
245 else:
246 interface.add_metadata(x)
248 def process_group(group, group_attrs, base_depends):
249 for item in group.childNodes:
250 if item.uri != XMLNS_IFACE: continue
252 depends = base_depends.copy()
254 item_attrs = _merge_attrs(group_attrs, item)
256 for child in item.childNodes:
257 if child.uri != XMLNS_IFACE: continue
258 if child.name == 'requires':
259 dep_iface = child.getAttribute('interface')
260 if dep_iface is None:
261 raise InvalidInterface("Missing 'interface' on <requires>")
262 dep = Dependency(dep_iface, metadata = child.attrs)
263 _process_depends(dep, child)
264 depends[dep.interface] = dep
266 if item.name == 'group':
267 process_group(item, item_attrs, depends)
268 elif item.name == 'implementation':
269 process_impl(item, item_attrs, depends)
271 def process_impl(item, item_attrs, depends):
272 id = item.getAttribute('id')
273 if id is None:
274 raise InvalidInterface("Missing 'id' attribute on %s" % item)
275 if local and (id.startswith('/') or id.startswith('.')):
276 impl = interface.get_impl(os.path.abspath(os.path.join(iface_dir, id)))
277 else:
278 if '=' not in id:
279 raise InvalidInterface('Invalid "id"; form is "alg=value" (got "%s")' % id)
280 alg, sha1 = id.split('=')
281 try:
282 long(sha1, 16)
283 except Exception, ex:
284 raise InvalidInterface('Bad SHA1 attribute: %s' % ex)
285 impl = interface.get_impl(id)
287 impl.metadata = item_attrs
288 try:
289 version = item_attrs['version']
290 version_mod = item_attrs.get('version-modifier', None)
291 if version_mod: version += version_mod
292 except KeyError:
293 raise InvalidInterface("Missing version attribute")
294 impl.version = parse_version(version)
296 item_main = item_attrs.get('main', None)
297 if item_main and item_main.startswith('/'):
298 raise InvalidInterface("'main' attribute must be relative, but '%s' starts with '/'!" %
299 item_main)
300 impl.main = item_main
302 impl.released = item_attrs.get('released', None)
304 size = item.getAttribute('size')
305 if size:
306 impl.size = long(size)
307 impl.arch = item_attrs.get('arch', None)
308 try:
309 stability = stability_levels[str(item_attrs['stability'])]
310 except KeyError:
311 stab = str(item_attrs['stability'])
312 if stab != stab.lower():
313 raise InvalidInterface('Stability "%s" invalid - use lower case!' % item_attrs.stability)
314 raise InvalidInterface('Stability "%s" invalid' % item_attrs['stability'])
315 if stability >= preferred:
316 raise InvalidInterface("Upstream can't set stability to preferred!")
317 impl.upstream_stability = stability
319 impl.dependencies.update(depends)
321 for elem in item.childNodes:
322 if elem.uri != XMLNS_IFACE: continue
323 if elem.name == 'archive':
324 url = elem.getAttribute('href')
325 if not url:
326 raise InvalidInterface("Missing href attribute on <archive>")
327 size = elem.getAttribute('size')
328 if not size:
329 raise InvalidInterface("Missing size attribute on <archive>")
330 impl.add_download_source(url = url, size = long(size),
331 extract = elem.getAttribute('extract'),
332 start_offset = _get_long(elem, 'start-offset'),
333 type = elem.getAttribute('type'))
334 elif elem.name == 'recipe':
335 recipe = Recipe()
336 for recipe_step in elem.childNodes:
337 if recipe_step.uri == XMLNS_IFACE and recipe_step.name == 'archive':
338 url = recipe_step.getAttribute('href')
339 if not url:
340 raise InvalidInterface("Missing href attribute on <archive>")
341 size = recipe_step.getAttribute('size')
342 if not size:
343 raise InvalidInterface("Missing size attribute on <archive>")
344 recipe.steps.append(DownloadSource(None, url = url, size = long(size),
345 extract = recipe_step.getAttribute('extract'),
346 start_offset = _get_long(recipe_step, 'start-offset'),
347 type = recipe_step.getAttribute('type')))
348 else:
349 info("Unknown step '%s' in recipe; skipping recipe", recipe_step.name)
350 break
351 else:
352 impl.download_sources.append(recipe)
354 process_group(root,
355 {'stability': 'testing',
356 'main' : root.getAttribute('main') or None,