Added 'mode' argument to <environment> bindings (requested by Lionel Tricon
[zeroinstall.git] / zeroinstall / injector / reader.py
blobb5ae3a13a253820d431acb00ab98b8a081e27efc
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, distro
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(item):
28 # Note: also called from selections
29 dep_iface = item.getAttribute('interface')
30 if dep_iface is None:
31 raise InvalidInterface("Missing 'interface' on <requires>")
32 dependency = InterfaceDependency(dep_iface, metadata = item.attrs)
34 for e in item.childNodes:
35 if e.uri != XMLNS_IFACE: continue
36 if e.name == 'environment':
37 mode = {
38 None: EnvironmentBinding.PREPEND,
39 'prepend': EnvironmentBinding.PREPEND,
40 'append': EnvironmentBinding.APPEND,
41 'replace': EnvironmentBinding.REPLACE,
42 }[e.getAttribute('mode')]
44 binding = EnvironmentBinding(e.getAttribute('name'),
45 insert = e.getAttribute('insert'),
46 default = e.getAttribute('default'),
47 mode = mode)
48 if not binding.name: raise InvalidInterface("Missing 'name' in binding")
49 if binding.insert is None: raise InvalidInterface("Missing 'insert' in binding")
50 dependency.bindings.append(binding)
51 elif e.name == 'version':
52 dependency.restrictions.append(
53 Restriction(not_before = parse_version(e.getAttribute('not-before')),
54 before = parse_version(e.getAttribute('before'))))
55 return dependency
57 def update_from_cache(interface):
58 """Read a cached interface and any local feeds or user overrides.
59 @param interface: the interface object to update
60 @type interface: L{model.Interface}
61 @return: True if cached version and user overrides loaded OK.
62 False if upstream not cached. Local interfaces (starting with /) are
63 always considered to be cached, although they are not actually stored in the cache.
64 @rtype: bool"""
65 interface.reset()
67 if interface.uri.startswith('/'):
68 debug("Loading local interface file '%s'", interface.uri)
69 update(interface, interface.uri, local = True)
70 interface.last_modified = int(os.stat(interface.uri).st_mtime)
71 cached = True
72 else:
73 cached = basedir.load_first_cache(config_site, 'interfaces', escape(interface.uri))
74 if cached:
75 debug("Loading cached information for %s from %s", interface, cached)
76 update(interface, cached)
78 # Add the distribution package manager's version, if any
79 path = basedir.load_first_data(config_site, 'native_feeds', escape(interface.uri))
80 if path:
81 # Resolve any symlinks
82 info("Adding native packager feed '%s'", path)
83 interface.feeds.append(Feed(os.path.realpath(path), None, False))
85 update_user_overrides(interface)
87 # Special case: add our fall-back local copy of the injector as a feed
88 if interface.uri == injector_gui_uri:
89 local_gui = os.path.join(os.path.abspath(dirname(dirname(__file__))), '0launch-gui', 'ZeroInstall-GUI.xml')
90 interface.feeds.append(Feed(local_gui, None, False))
92 return bool(cached)
94 def update_user_overrides(interface):
95 """Update an interface with user-supplied information.
96 @param interface: the interface object to update
97 @type interface: L{model.Interface}"""
98 user = basedir.load_first_config(config_site, config_prog,
99 'user_overrides', escape(interface.uri))
100 if not user:
101 return
103 root = qdom.parse(file(user))
105 last_checked = root.getAttribute('last-checked')
106 if last_checked:
107 interface.last_checked = int(last_checked)
109 last_check_attempt = root.getAttribute('last-check-attempt')
110 if last_check_attempt:
111 interface.last_check_attempt = int(last_check_attempt)
113 stability_policy = root.getAttribute('stability-policy')
114 if stability_policy:
115 interface.set_stability_policy(stability_levels[str(stability_policy)])
117 for item in root.childNodes:
118 if item.uri != XMLNS_IFACE: continue
119 if item.name == 'implementation':
120 id = item.getAttribute('id')
121 assert id is not None
122 if not (id.startswith('/') or id.startswith('.')):
123 assert '=' in id
124 impl = interface.implementations.get(id, None)
125 if not impl:
126 debug("Ignoring user-override for unknown implementation %s in %s", id, interface)
127 continue
129 user_stability = item.getAttribute('user-stability')
130 if user_stability:
131 impl.user_stability = stability_levels[str(user_stability)]
132 elif item.name == 'feed':
133 feed_src = item.getAttribute('src')
134 if not feed_src:
135 raise InvalidInterface('Missing "src" attribute in <feed>')
136 interface.feeds.append(Feed(feed_src, item.getAttribute('arch'), True))
138 def check_readable(interface_uri, source):
139 """Test whether an interface file is valid.
140 @param interface_uri: the interface's URI
141 @type interface_uri: str
142 @param source: the name of the file to test
143 @type source: str
144 @return: the modification time in src (usually just the mtime of the file)
145 @rtype: int
146 @raise InvalidInterface: If the source's syntax is incorrect,
148 tmp = Interface(interface_uri)
149 try:
150 update(tmp, source)
151 except InvalidInterface, ex:
152 info("Error loading interface:\n"
153 "Interface URI: %s\n"
154 "Local file: %s\n%s" %
155 (interface_uri, source, ex))
156 raise InvalidInterface("Error loading feed '%s':\n\n%s" % (interface_uri, ex))
157 return tmp.last_modified
159 def _parse_time(t):
160 try:
161 return long(t)
162 except Exception, ex:
163 raise InvalidInterface("Date '%s' not in correct format (should be integer number "
164 "of seconds since Unix epoch)\n%s" % (t, ex))
166 def _check_canonical_name(interface, root):
167 "Ensure the uri= attribute in the interface file matches the interface we are trying to load"
168 canonical_name = root.getAttribute('uri')
169 if not canonical_name:
170 raise InvalidInterface("<interface> uri attribute missing")
171 if canonical_name != interface.uri:
172 raise InvalidInterface("Incorrect URL used for feed.\n\n"
173 "%s is given in the feed, but\n"
174 "%s was requested" %
175 (canonical_name, interface.uri))
177 def _get_long(elem, attr_name):
178 val = elem.getAttribute(attr_name)
179 if val is not None:
180 try:
181 val = long(val)
182 except ValueError, ex:
183 raise SafeException("Invalid value for integer attribute '%s': %s" % (attr_name, val))
184 return val
186 def _merge_attrs(attrs, item):
187 """Add each attribute of item to a copy of attrs and return the copy.
188 @type attrs: {str: str}
189 @type item: L{qdom.Element}
190 @rtype: {str: str}
192 new = attrs.copy()
193 for a in item.attrs:
194 new[str(a)] = item.attrs[a]
195 return new
197 def update(interface, source, local = False):
198 """Read in information about an interface.
199 @param interface: the interface object to update
200 @type interface: L{model.Interface}
201 @param source: the name of the file to read
202 @type source: str
203 @param local: use file's mtime for last-modified, and uri attribute is ignored
204 @raise InvalidInterface: if the source's syntax is incorrect
205 @see: L{update_from_cache}, which calls this"""
206 assert isinstance(interface, Interface)
208 try:
209 root = qdom.parse(file(source))
210 except Exception, ex:
211 raise InvalidInterface("Invalid XML", ex)
213 if not local:
214 _check_canonical_name(interface, root)
215 time_str = root.getAttribute('last-modified')
216 if time_str:
217 # Old style cached items use an attribute
218 interface.last_modified = _parse_time(time_str)
219 else:
220 # New style items have the mtime in the signature,
221 # but for quick access we use the mtime of the file
222 interface.last_modified = int(os.stat(source).st_mtime)
223 main = root.getAttribute('main')
224 if main:
225 interface.main = main
227 min_injector_version = root.getAttribute('min-injector-version')
228 if min_injector_version:
229 try:
230 min_ints = map(int, min_injector_version.split('.'))
231 except ValueError, ex:
232 raise InvalidInterface("Bad version number '%s'" % min_injector_version)
233 injector_version = map(int, version.split('.'))
234 if min_ints > injector_version:
235 raise InvalidInterface("This interface requires version %s or later of "
236 "the Zero Install injector, but I am only version %s. "
237 "You can get a newer version from http://0install.net" %
238 (min_injector_version, version))
240 if local:
241 iface_dir = os.path.dirname(source)
242 else:
243 iface_dir = None # Can't have relative paths
245 for x in root.childNodes:
246 if x.uri != XMLNS_IFACE:
247 interface.add_metadata(x)
248 continue
249 if x.name == 'name':
250 interface.name = interface.name or x.content
251 elif x.name == 'description':
252 interface.description = interface.description or x.content
253 elif x.name == 'summary':
254 interface.summary = interface.summary or x.content
255 elif x.name == 'feed-for':
256 feed_iface = x.getAttribute('interface')
257 if not feed_iface:
258 raise InvalidInterface('Missing "interface" attribute in <feed-for>')
259 interface.feed_for[feed_iface] = True
260 # Bug report from a Debian/stable user that --feed gets the wrong value.
261 # Can't reproduce (even in a Debian/stable chroot), but add some logging here
262 # in case it happens again.
263 debug("Is feed-for %s", feed_iface)
264 elif x.name == 'feed':
265 feed_src = x.getAttribute('src')
266 if not feed_src:
267 raise InvalidInterface('Missing "src" attribute in <feed>')
268 if feed_src.startswith('http:') or local:
269 interface.feeds.append(Feed(feed_src, x.getAttribute('arch'), False))
270 else:
271 raise InvalidInterface("Invalid feed URL '%s'" % feed_src)
272 else:
273 interface.add_metadata(x)
275 def process_group(group, group_attrs, base_depends):
276 for item in group.childNodes:
277 if item.uri != XMLNS_IFACE: continue
279 depends = base_depends[:]
281 item_attrs = _merge_attrs(group_attrs, item)
283 for child in item.childNodes:
284 if child.uri != XMLNS_IFACE: continue
285 if child.name == 'requires':
286 dep = _process_depends(child)
287 depends.append(dep)
289 if item.name == 'group':
290 process_group(item, item_attrs, depends)
291 elif item.name == 'implementation':
292 process_impl(item, item_attrs, depends)
293 elif item.name == 'package-implementation':
294 process_native_impl(item, item_attrs, depends)
296 def process_impl(item, item_attrs, depends):
297 id = item.getAttribute('id')
298 if id is None:
299 raise InvalidInterface("Missing 'id' attribute on %s" % item)
300 if local and (id.startswith('/') or id.startswith('.')):
301 impl = interface.get_impl(os.path.abspath(os.path.join(iface_dir, id)))
302 else:
303 if '=' not in id:
304 raise InvalidInterface('Invalid "id"; form is "alg=value" (got "%s")' % id)
305 alg, sha1 = id.split('=')
306 try:
307 long(sha1, 16)
308 except Exception, ex:
309 raise InvalidInterface('Bad SHA1 attribute: %s' % ex)
310 impl = interface.get_impl(id)
312 impl.metadata = item_attrs
313 try:
314 version = item_attrs['version']
315 version_mod = item_attrs.get('version-modifier', None)
316 if version_mod: version += version_mod
317 except KeyError:
318 raise InvalidInterface("Missing version attribute")
319 impl.version = parse_version(version)
321 item_main = item_attrs.get('main', None)
322 if item_main and item_main.startswith('/'):
323 raise InvalidInterface("'main' attribute must be relative, but '%s' starts with '/'!" %
324 item_main)
325 impl.main = item_main
327 impl.released = item_attrs.get('released', None)
329 size = item.getAttribute('size')
330 if size:
331 impl.size = long(size)
332 impl.arch = item_attrs.get('arch', None)
333 try:
334 stability = stability_levels[str(item_attrs['stability'])]
335 except KeyError:
336 stab = str(item_attrs['stability'])
337 if stab != stab.lower():
338 raise InvalidInterface('Stability "%s" invalid - use lower case!' % item_attrs.stability)
339 raise InvalidInterface('Stability "%s" invalid' % item_attrs['stability'])
340 if stability >= preferred:
341 raise InvalidInterface("Upstream can't set stability to preferred!")
342 impl.upstream_stability = stability
344 impl.requires = depends
346 for elem in item.childNodes:
347 if elem.uri != XMLNS_IFACE: continue
348 if elem.name == 'archive':
349 url = elem.getAttribute('href')
350 if not url:
351 raise InvalidInterface("Missing href attribute on <archive>")
352 size = elem.getAttribute('size')
353 if not size:
354 raise InvalidInterface("Missing size attribute on <archive>")
355 impl.add_download_source(url = url, size = long(size),
356 extract = elem.getAttribute('extract'),
357 start_offset = _get_long(elem, 'start-offset'),
358 type = elem.getAttribute('type'))
359 elif elem.name == 'recipe':
360 recipe = Recipe()
361 for recipe_step in elem.childNodes:
362 if recipe_step.uri == XMLNS_IFACE and recipe_step.name == 'archive':
363 url = recipe_step.getAttribute('href')
364 if not url:
365 raise InvalidInterface("Missing href attribute on <archive>")
366 size = recipe_step.getAttribute('size')
367 if not size:
368 raise InvalidInterface("Missing size attribute on <archive>")
369 recipe.steps.append(DownloadSource(None, url = url, size = long(size),
370 extract = recipe_step.getAttribute('extract'),
371 start_offset = _get_long(recipe_step, 'start-offset'),
372 type = recipe_step.getAttribute('type')))
373 else:
374 info("Unknown step '%s' in recipe; skipping recipe", recipe_step.name)
375 break
376 else:
377 impl.download_sources.append(recipe)
379 def process_native_impl(item, item_attrs, depends):
380 package = item_attrs.get('package', None)
381 if package is None:
382 raise InvalidInterface("Missing 'package' attribute on %s" % item)
383 distribution = item_attrs.get('distribution', None)
384 if distribution is None:
385 raise InvalidInterface("Missing 'distribution' attribute on %s" % item)
387 def factory(id):
388 assert id.startswith('package:')
389 impl = interface.get_impl(id)
391 impl.metadata = item_attrs
393 item_main = item_attrs.get('main', None)
394 if item_main and not item_main.startswith('/'):
395 raise InvalidInterface("'main' attribute must be absolute, but '%s' doesn't start with '/'!" %
396 item_main)
397 impl.main = item_main
398 impl.upstream_stability = packaged
399 impl.requires = depends
401 return impl
403 distro.host_distribution.get_package_info(distribution, package, factory)
406 process_group(root,
407 {'stability': 'testing',
408 'main' : root.getAttribute('main') or None,