Clarified copyrights.
[zeroinstall.git] / zeroinstall / injector / reader.py
blob149cd517b4098897d9ab63e3077d22d059092904
1 # Copyright (C) 2006, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 from xml.dom import Node, minidom
5 import sys
6 import shutil
7 import time
8 from logging import debug, warn
10 import basedir
11 from namespaces import *
12 from model import *
14 class InvalidInterface(SafeException):
15 def __init__(self, message, ex = None):
16 if ex:
17 message += "\n\n(exact error: %s)" % ex
18 SafeException.__init__(self, message)
20 def get_singleton_text(parent, ns, localName):
21 names = parent.getElementsByTagNameNS(ns, localName)
22 if not names:
23 raise InvalidInterface('No <%s> element in <%s>' % (localName, parent.localName))
24 if len(names) > 1:
25 raise InvalidInterface('Multiple <%s> elements in <%s>' % (localName, parent.localName))
26 text = ''
27 for x in names[0].childNodes:
28 if x.nodeType == Node.TEXT_NODE:
29 text += x.data
30 return text.strip()
32 class Attrs(object):
33 __slots__ = ['version', 'released', 'arch', 'stability', 'main']
34 def __init__(self, **kwargs):
35 for x in self.__slots__:
36 setattr(self, x, kwargs.get(x, None))
38 def merge(self, item):
39 new = Attrs()
40 for x in self.__slots__:
41 if item.hasAttribute(x):
42 value = item.getAttribute(x)
43 else:
44 value = getattr(self, x)
45 setattr(new, x, value)
46 return new
48 def process_depends(dependency, item):
49 for e in item.getElementsByTagNameNS(XMLNS_IFACE, 'environment'):
50 binding = EnvironmentBinding(e.getAttribute('name'),
51 insert = e.getAttribute('insert'),
52 default = e.getAttribute('default'))
53 dependency.bindings.append(binding)
55 def update_from_cache(interface):
56 """True if cached version and user overrides loaded OK.
57 False if upstream not cached. Local interfaces (starting with /) are
58 always considered to be cached, although they are not stored there."""
59 interface.reset()
61 if interface.uri.startswith('/'):
62 debug("Loading local interface file '%s'", interface.uri)
63 update(interface, interface.uri, local = True)
64 interface.last_modified = os.stat(interface.uri).st_mtime
65 cached = True
66 else:
67 cached = basedir.load_first_cache(config_site, 'interfaces', escape(interface.uri))
68 if cached:
69 debug("Loading cached information for %s from %s", interface, cached)
70 update(interface, cached)
72 update_user_overrides(interface)
74 return bool(cached)
76 def update_user_overrides(interface):
77 user = basedir.load_first_config(config_site, config_prog,
78 'user_overrides', escape(interface.uri))
79 if not user:
80 return
82 doc = minidom.parse(user)
83 root = doc.documentElement
85 last_checked = root.getAttribute('last-checked')
86 if last_checked:
87 interface.last_checked = int(last_checked)
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.getElementsByTagNameNS(XMLNS_IFACE, 'implementation'):
94 id = item.getAttribute('id')
95 assert id is not None
96 if id.startswith('/'):
97 impl = interface.get_impl(id)
98 else:
99 assert '=' in id
100 impl = interface.get_impl(id)
102 user_stability = item.getAttribute('user-stability')
103 if user_stability:
104 impl.user_stability = stability_levels[str(user_stability)]
106 for feed in root.getElementsByTagNameNS(XMLNS_IFACE, 'feed'):
107 feed_src = feed.getAttribute('src')
108 if not feed_src:
109 raise InvalidInterface('Missing "src" attribute in <feed>')
110 interface.feeds.append(Feed(feed_src, feed.getAttribute('arch'), True))
112 def check_readable(interface_uri, source):
113 """Returns the modified time in 'source'. If syntax is incorrect,
114 throws an exception."""
115 tmp = Interface(interface_uri)
116 try:
117 update(tmp, source)
118 except InvalidInterface, ex:
119 raise InvalidInterface("Error loading interface:\n"
120 "Interface URI: %s\n"
121 "Local file: %s\n%s" %
122 (interface_uri, source, ex))
123 return tmp.last_modified
125 def parse_time(t):
126 try:
127 return long(t)
128 except Exception, ex:
129 raise InvalidInterface("Date '%s' not in correct format (should be integer number "
130 "of seconds since Unix epoch)\n%s" % (t, ex))
132 def _check_canonical_name(interface, source, root):
133 "Ensure the uri= attribute in the interface file matches the interface we are trying to load"
134 canonical_name = root.getAttribute('uri')
135 if not canonical_name:
136 raise InvalidInterface("<interface> uri attribute missing in " + source)
137 if canonical_name != interface.uri:
138 raise InvalidInterface("<interface> uri attribute is '%s', but accessed as '%s'\n(%s)" %
139 (canonical_name, interface.uri, source))
141 def update(interface, source, local = False):
142 """local - use file mtime for last-modified, and uri attribute is ignored"""
143 assert isinstance(interface, Interface)
145 try:
146 doc = minidom.parse(source)
147 except Exception, ex:
148 raise InvalidInterface("Invalid XML", ex)
150 root = doc.documentElement
152 interface.name = interface.name or get_singleton_text(root, XMLNS_IFACE, 'name')
153 interface.description = interface.description or get_singleton_text(root, XMLNS_IFACE, 'description')
154 interface.summary = interface.summary or get_singleton_text(root, XMLNS_IFACE, 'summary')
156 if not local:
157 _check_canonical_name(interface, source, root)
158 time_str = root.getAttribute('last-modified')
159 if not time_str:
160 raise InvalidInterface("Missing last-modified attribute on root element.")
161 interface.last_modified = parse_time(time_str)
162 main = root.getAttribute('main')
163 if main:
164 interface.main = main
166 if local:
167 iface_dir = os.path.dirname(source)
168 else:
169 iface_dir = None # Can't have relative paths
171 for source in root.getElementsByTagNameNS(XMLNS_IFACE, 'source'):
172 source_interface = source.getAttribute('interface')
173 if source_interface:
174 interface.sources.append(Source(source_interface))
175 else:
176 raise InvalidInterface("Missing interface attribute on <source>")
178 for feed in root.getElementsByTagNameNS(XMLNS_IFACE, 'feed-for'):
179 feed_iface = feed.getAttribute('interface')
180 if not feed_iface:
181 raise InvalidInterface('Missing "interface" attribute in <feed-for>')
182 interface.feed_for[feed_iface] = True
184 for feed in root.getElementsByTagNameNS(XMLNS_IFACE, 'feed'):
185 feed_src = feed.getAttribute('src')
186 if not feed_src:
187 raise InvalidInterface('Missing "src" attribute in <feed>')
188 if feed_src.startswith('http:') or local:
189 interface.feeds.append(Feed(feed_src, feed.getAttribute('arch'), False))
190 else:
191 raise InvalidInterface("Invalid feed URL '%s'" % feed_src)
193 def process_group(group, group_attrs, base_depends):
194 for item in group.childNodes:
195 depends = base_depends.copy()
196 if item.namespaceURI != XMLNS_IFACE:
197 continue
199 item_attrs = group_attrs.merge(item)
201 for dep_elem in item.getElementsByTagNameNS(XMLNS_IFACE, 'requires'):
202 dep = Dependency(dep_elem.getAttribute('interface'))
203 process_depends(dep, dep_elem)
204 depends[dep.interface] = dep
206 if item.localName == 'group':
207 process_group(item, item_attrs, depends)
208 elif item.localName == 'implementation':
209 process_impl(item, item_attrs, depends)
211 def process_impl(item, item_attrs, depends):
212 id = item.getAttribute('id')
213 if id is None:
214 raise InvalidInterface("Missing 'id' attribute on %s" % item)
215 if local and (id.startswith('/') or id.startswith('.')):
216 impl = interface.get_impl(os.path.abspath(os.path.join(iface_dir, id)))
217 else:
218 if '=' not in id:
219 raise InvalidInterface('Invalid "id"; form is "alg=value" (got "%s")' % id)
220 alg, sha1 = id.split('=')
221 try:
222 long(sha1, 16)
223 except Exception, ex:
224 raise InvalidInterface('Bad SHA1 attribute: %s' % ex)
225 impl = interface.get_impl(id)
227 version = item_attrs.version
228 if not version:
229 raise InvalidInterface("Missing version attribute")
230 impl.version = map(int, version.split('.'))
232 impl.main = item_attrs.main
234 if item_attrs.released:
235 impl.released = item_attrs.released
237 size = item.getAttribute('size')
238 if size:
239 impl.size = long(size)
240 impl.arch = item_attrs.arch
241 try:
242 stability = stability_levels[str(item_attrs.stability)]
243 except KeyError:
244 stab = str(item_attrs.stability)
245 if stab != stab.lower():
246 raise InvalidInterface('Stability "%s" invalid - use lower case!' % item_attrs.stability)
247 raise InvalidInterface('Stability "%s" invalid' % item_attrs.stability)
248 if stability >= preferred:
249 raise InvalidInterface("Upstream can't set stability to preferred!")
250 impl.upstream_stability = stability
252 impl.dependencies.update(depends)
254 for elem in item.getElementsByTagNameNS(XMLNS_IFACE, 'archive'):
255 url = elem.getAttribute('href')
256 if not url:
257 raise InvalidInterface("Missing href attribute on <archive>")
258 size = elem.getAttribute('size')
259 if not size:
260 raise InvalidInterface("Missing size attribute on <archive>")
261 impl.add_download_source(url = url, size = long(size),
262 extract = elem.getAttribute('extract'))
264 process_group(root,
265 Attrs(stability = testing,
266 main = root.getAttribute('main') or None),