2 Parses an XML interface into a Python representation.
5 # Copyright (C) 2006, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
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):
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.
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
)
56 cached
= basedir
.load_first_cache(config_site
, 'interfaces', escape(interface
.uri
))
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))
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
))
79 root
= qdom
.parse(file(user
))
81 last_checked
= root
.getAttribute('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')
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')
98 if not (id.startswith('/') or id.startswith('.')):
100 impl
= interface
.implementations
.get(id, None)
102 debug("Ignoring user-override for unknown implementation %s in %s", id, interface
)
105 user_stability
= item
.getAttribute('user-stability')
107 impl
.user_stability
= stability_levels
[str(user_stability
)]
108 elif item
.name
== 'feed':
109 feed_src
= item
.getAttribute('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
120 @return: the modification time in src (usually just the mtime of the file)
122 @raise InvalidInterface: If the source's syntax is incorrect,
124 tmp
= Interface(interface_uri
)
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
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
)
155 except ValueError, ex
:
156 raise SafeException("Invalid value for integer attribute '%s': %s" % (attr_name
, 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}
167 new
[str(a
)] = item
.attrs
[a
]
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
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
)
182 root
= qdom
.parse(file(source
))
183 except Exception, ex
:
184 raise InvalidInterface("Invalid XML", ex
)
187 _check_canonical_name(interface
, source
, root
)
188 time_str
= root
.getAttribute('last-modified')
190 # Old style cached items use an attribute
191 interface
.last_modified
= _parse_time(time_str
)
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')
198 interface
.main
= main
200 min_injector_version
= root
.getAttribute('min-injector-version')
201 if min_injector_version
:
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
))
214 iface_dir
= os
.path
.dirname(source
)
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
)
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')
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')
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))
244 raise InvalidInterface("Invalid feed URL '%s'" % feed_src
)
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')
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)))
279 raise InvalidInterface('Invalid "id"; form is "alg=value" (got "%s")' % id)
280 alg
, sha1
= id.split('=')
283 except Exception, ex
:
284 raise InvalidInterface('Bad SHA1 attribute: %s' % ex
)
285 impl
= interface
.get_impl(id)
287 impl
.metadata
= item_attrs
289 version
= item_attrs
['version']
290 version_mod
= item_attrs
.get('version-modifier', None)
291 if version_mod
: version
+= version_mod
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 '/'!" %
300 impl
.main
= item_main
302 impl
.released
= item_attrs
.get('released', None)
304 size
= item
.getAttribute('size')
306 impl
.size
= long(size
)
307 impl
.arch
= item_attrs
.get('arch', None)
309 stability
= stability_levels
[str(item_attrs
['stability'])]
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')
326 raise InvalidInterface("Missing href attribute on <archive>")
327 size
= elem
.getAttribute('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':
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')
340 raise InvalidInterface("Missing href attribute on <archive>")
341 size
= recipe_step
.getAttribute('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')))
349 info("Unknown step '%s' in recipe; skipping recipe", recipe_step
.name
)
352 impl
.download_sources
.append(recipe
)
355 {'stability': 'testing',
356 'main' : root
.getAttribute('main') or None,