blob: 0e6421f102be2a89f8808b65b6f79e7b6dcda561 [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001#
2# Copyright (C) 2008 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import os
17import sys
18import xml.dom.minidom
19
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070020from git_config import GitConfig, IsId
Shawn O. Pearced1f70d92009-05-19 14:58:02 -070021from project import RemoteSpec, Project, MetaProject, R_HEADS, HEAD
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070022from error import ManifestParseError
23
24MANIFEST_FILE_NAME = 'manifest.xml'
Shawn O. Pearce5cc66792008-10-23 16:19:27 -070025LOCAL_MANIFEST_NAME = 'local_manifest.xml'
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070026
27class _Default(object):
28 """Project defaults within the manifest."""
29
Shawn O. Pearce3c8dea12009-05-29 18:38:17 -070030 revisionExpr = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070031 remote = None
32
Shawn O. Pearced1f70d92009-05-19 14:58:02 -070033class _XmlRemote(object):
34 def __init__(self,
35 name,
36 fetch=None,
37 review=None):
38 self.name = name
39 self.fetchUrl = fetch
40 self.reviewUrl = review
41
42 def ToRemoteSpec(self, projectName):
43 url = self.fetchUrl
44 while url.endswith('/'):
45 url = url[:-1]
46 url += '/%s.git' % projectName
47 return RemoteSpec(self.name, url, self.reviewUrl)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070048
Shawn O. Pearcec8a300f2009-05-18 13:19:57 -070049class XmlManifest(object):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070050 """manages the repo configuration file"""
51
52 def __init__(self, repodir):
53 self.repodir = os.path.abspath(repodir)
54 self.topdir = os.path.dirname(self.repodir)
55 self.manifestFile = os.path.join(self.repodir, MANIFEST_FILE_NAME)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070056 self.globalConfig = GitConfig.ForUser()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070057
58 self.repoProject = MetaProject(self, 'repo',
59 gitdir = os.path.join(repodir, 'repo/.git'),
60 worktree = os.path.join(repodir, 'repo'))
61
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070062 self.manifestProject = MetaProject(self, 'manifests',
Shawn O. Pearcef5c25a62008-11-04 08:11:53 -080063 gitdir = os.path.join(repodir, 'manifests.git'),
64 worktree = os.path.join(repodir, 'manifests'))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070065
66 self._Unload()
67
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -070068 def Override(self, name):
69 """Use a different manifest, just for the current instantiation.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070070 """
71 path = os.path.join(self.manifestProject.worktree, name)
72 if not os.path.isfile(path):
73 raise ManifestParseError('manifest %s not found' % name)
74
75 old = self.manifestFile
76 try:
77 self.manifestFile = path
78 self._Unload()
79 self._Load()
80 finally:
81 self.manifestFile = old
82
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -070083 def Link(self, name):
84 """Update the repo metadata to use a different manifest.
85 """
86 self.Override(name)
87
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070088 try:
89 if os.path.exists(self.manifestFile):
90 os.remove(self.manifestFile)
91 os.symlink('manifests/%s' % name, self.manifestFile)
92 except OSError, e:
93 raise ManifestParseError('cannot link manifest %s' % name)
94
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -080095 def _RemoteToXml(self, r, doc, root):
96 e = doc.createElement('remote')
97 root.appendChild(e)
98 e.setAttribute('name', r.name)
99 e.setAttribute('fetch', r.fetchUrl)
100 if r.reviewUrl is not None:
101 e.setAttribute('review', r.reviewUrl)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800102
103 def Save(self, fd, peg_rev=False):
104 """Write the current manifest out to the given file descriptor.
105 """
106 doc = xml.dom.minidom.Document()
107 root = doc.createElement('manifest')
108 doc.appendChild(root)
109
Doug Anderson2b8db3c2010-11-01 15:08:06 -0700110 # Save out the notice. There's a little bit of work here to give it the
111 # right whitespace, which assumes that the notice is automatically indented
112 # by 4 by minidom.
113 if self.notice:
114 notice_element = root.appendChild(doc.createElement('notice'))
115 notice_lines = self.notice.splitlines()
116 indented_notice = ('\n'.join(" "*4 + line for line in notice_lines))[4:]
117 notice_element.appendChild(doc.createTextNode(indented_notice))
118
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800119 d = self.default
120 sort_remotes = list(self.remotes.keys())
121 sort_remotes.sort()
122
123 for r in sort_remotes:
124 self._RemoteToXml(self.remotes[r], doc, root)
125 if self.remotes:
126 root.appendChild(doc.createTextNode(''))
127
128 have_default = False
129 e = doc.createElement('default')
130 if d.remote:
131 have_default = True
132 e.setAttribute('remote', d.remote.name)
Shawn O. Pearce3c8dea12009-05-29 18:38:17 -0700133 if d.revisionExpr:
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800134 have_default = True
Shawn O. Pearce3c8dea12009-05-29 18:38:17 -0700135 e.setAttribute('revision', d.revisionExpr)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800136 if have_default:
137 root.appendChild(e)
138 root.appendChild(doc.createTextNode(''))
139
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700140 if self._manifest_server:
141 e = doc.createElement('manifest-server')
142 e.setAttribute('url', self._manifest_server)
143 root.appendChild(e)
144 root.appendChild(doc.createTextNode(''))
145
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800146 sort_projects = list(self.projects.keys())
147 sort_projects.sort()
148
149 for p in sort_projects:
150 p = self.projects[p]
151 e = doc.createElement('project')
152 root.appendChild(e)
153 e.setAttribute('name', p.name)
154 if p.relpath != p.name:
155 e.setAttribute('path', p.relpath)
156 if not d.remote or p.remote.name != d.remote.name:
157 e.setAttribute('remote', p.remote.name)
158 if peg_rev:
159 if self.IsMirror:
160 e.setAttribute('revision',
Shawn O. Pearce3c8dea12009-05-29 18:38:17 -0700161 p.bare_git.rev_parse(p.revisionExpr + '^0'))
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800162 else:
163 e.setAttribute('revision',
164 p.work_git.rev_parse(HEAD + '^0'))
Shawn O. Pearce3c8dea12009-05-29 18:38:17 -0700165 elif not d.revisionExpr or p.revisionExpr != d.revisionExpr:
166 e.setAttribute('revision', p.revisionExpr)
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800167
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800168 for c in p.copyfiles:
169 ce = doc.createElement('copyfile')
170 ce.setAttribute('src', c.src)
171 ce.setAttribute('dest', c.dest)
172 e.appendChild(ce)
173
Doug Anderson37282b42011-03-04 11:54:18 -0800174 if self._repo_hooks_project:
175 root.appendChild(doc.createTextNode(''))
176 e = doc.createElement('repo-hooks')
177 e.setAttribute('in-project', self._repo_hooks_project.name)
178 e.setAttribute('enabled-list',
179 ' '.join(self._repo_hooks_project.enabled_repo_hooks))
180 root.appendChild(e)
181
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800182 doc.writexml(fd, '', ' ', '\n', 'UTF-8')
183
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700184 @property
185 def projects(self):
186 self._Load()
187 return self._projects
188
189 @property
190 def remotes(self):
191 self._Load()
192 return self._remotes
193
194 @property
195 def default(self):
196 self._Load()
197 return self._default
198
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800199 @property
Doug Anderson37282b42011-03-04 11:54:18 -0800200 def repo_hooks_project(self):
201 self._Load()
202 return self._repo_hooks_project
203
204 @property
Doug Anderson2b8db3c2010-11-01 15:08:06 -0700205 def notice(self):
206 self._Load()
207 return self._notice
208
209 @property
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700210 def manifest_server(self):
211 self._Load()
212 return self._manifest_server
213
214 @property
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800215 def IsMirror(self):
216 return self.manifestProject.config.GetBoolean('repo.mirror')
217
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700218 def _Unload(self):
219 self._loaded = False
220 self._projects = {}
221 self._remotes = {}
222 self._default = None
Doug Anderson37282b42011-03-04 11:54:18 -0800223 self._repo_hooks_project = None
Doug Anderson2b8db3c2010-11-01 15:08:06 -0700224 self._notice = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700225 self.branch = None
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700226 self._manifest_server = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700227
228 def _Load(self):
229 if not self._loaded:
Shawn O. Pearce2450a292008-11-04 08:22:07 -0800230 m = self.manifestProject
231 b = m.GetBranch(m.CurrentBranch).merge
Shawn O. Pearce21c5c342009-06-25 16:47:30 -0700232 if b is not None and b.startswith(R_HEADS):
Shawn O. Pearce2450a292008-11-04 08:22:07 -0800233 b = b[len(R_HEADS):]
234 self.branch = b
235
Shawn O. Pearce5cc66792008-10-23 16:19:27 -0700236 self._ParseManifest(True)
237
238 local = os.path.join(self.repodir, LOCAL_MANIFEST_NAME)
239 if os.path.exists(local):
240 try:
241 real = self.manifestFile
242 self.manifestFile = local
243 self._ParseManifest(False)
244 finally:
245 self.manifestFile = real
246
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800247 if self.IsMirror:
248 self._AddMetaProjectMirror(self.repoProject)
249 self._AddMetaProjectMirror(self.manifestProject)
250
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700251 self._loaded = True
252
Shawn O. Pearce5cc66792008-10-23 16:19:27 -0700253 def _ParseManifest(self, is_root_file):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700254 root = xml.dom.minidom.parse(self.manifestFile)
255 if not root or not root.childNodes:
Doug Anderson37282b42011-03-04 11:54:18 -0800256 raise ManifestParseError(
257 "no root node in %s" %
258 self.manifestFile)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700259
260 config = root.childNodes[0]
261 if config.nodeName != 'manifest':
Doug Anderson37282b42011-03-04 11:54:18 -0800262 raise ManifestParseError(
263 "no <manifest> in %s" %
264 self.manifestFile)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700265
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700266 for node in config.childNodes:
Shawn O. Pearce03eaf072008-11-20 11:42:22 -0800267 if node.nodeName == 'remove-project':
268 name = self._reqatt(node, 'name')
269 try:
270 del self._projects[name]
271 except KeyError:
Doug Anderson37282b42011-03-04 11:54:18 -0800272 raise ManifestParseError(
273 'project %s not found' %
274 (name))
275
276 # If the manifest removes the hooks project, treat it as if it deleted
277 # the repo-hooks element too.
278 if self._repo_hooks_project and (self._repo_hooks_project.name == name):
279 self._repo_hooks_project = None
Shawn O. Pearce03eaf072008-11-20 11:42:22 -0800280
281 for node in config.childNodes:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700282 if node.nodeName == 'remote':
283 remote = self._ParseRemote(node)
284 if self._remotes.get(remote.name):
Doug Anderson37282b42011-03-04 11:54:18 -0800285 raise ManifestParseError(
286 'duplicate remote %s in %s' %
287 (remote.name, self.manifestFile))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700288 self._remotes[remote.name] = remote
289
290 for node in config.childNodes:
291 if node.nodeName == 'default':
292 if self._default is not None:
Doug Anderson37282b42011-03-04 11:54:18 -0800293 raise ManifestParseError(
294 'duplicate default in %s' %
295 (self.manifestFile))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700296 self._default = self._ParseDefault(node)
297 if self._default is None:
298 self._default = _Default()
299
300 for node in config.childNodes:
Doug Anderson2b8db3c2010-11-01 15:08:06 -0700301 if node.nodeName == 'notice':
302 if self._notice is not None:
Doug Anderson37282b42011-03-04 11:54:18 -0800303 raise ManifestParseError(
304 'duplicate notice in %s' %
305 (self.manifestFile))
Doug Anderson2b8db3c2010-11-01 15:08:06 -0700306 self._notice = self._ParseNotice(node)
307
308 for node in config.childNodes:
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700309 if node.nodeName == 'manifest-server':
310 url = self._reqatt(node, 'url')
311 if self._manifest_server is not None:
Doug Anderson37282b42011-03-04 11:54:18 -0800312 raise ManifestParseError(
313 'duplicate manifest-server in %s' %
314 (self.manifestFile))
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700315 self._manifest_server = url
316
317 for node in config.childNodes:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700318 if node.nodeName == 'project':
319 project = self._ParseProject(node)
320 if self._projects.get(project.name):
Doug Anderson37282b42011-03-04 11:54:18 -0800321 raise ManifestParseError(
322 'duplicate project %s in %s' %
323 (project.name, self.manifestFile))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700324 self._projects[project.name] = project
325
Doug Anderson37282b42011-03-04 11:54:18 -0800326 for node in config.childNodes:
327 if node.nodeName == 'repo-hooks':
328 # Get the name of the project and the (space-separated) list of enabled.
329 repo_hooks_project = self._reqatt(node, 'in-project')
330 enabled_repo_hooks = self._reqatt(node, 'enabled-list').split()
331
332 # Only one project can be the hooks project
333 if self._repo_hooks_project is not None:
334 raise ManifestParseError(
335 'duplicate repo-hooks in %s' %
336 (self.manifestFile))
337
338 # Store a reference to the Project.
339 try:
340 self._repo_hooks_project = self._projects[repo_hooks_project]
341 except KeyError:
342 raise ManifestParseError(
343 'project %s not found for repo-hooks' %
344 (repo_hooks_project))
345
346 # Store the enabled hooks in the Project object.
347 self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
348
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800349 def _AddMetaProjectMirror(self, m):
350 name = None
351 m_url = m.GetRemote(m.remote.name).url
352 if m_url.endswith('/.git'):
353 raise ManifestParseError, 'refusing to mirror %s' % m_url
354
355 if self._default and self._default.remote:
356 url = self._default.remote.fetchUrl
357 if not url.endswith('/'):
358 url += '/'
359 if m_url.startswith(url):
360 remote = self._default.remote
361 name = m_url[len(url):]
362
363 if name is None:
364 s = m_url.rindex('/') + 1
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700365 remote = _XmlRemote('origin', m_url[:s])
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800366 name = m_url[s:]
367
368 if name.endswith('.git'):
369 name = name[:-4]
370
371 if name not in self._projects:
372 m.PreSync()
373 gitdir = os.path.join(self.topdir, '%s.git' % name)
374 project = Project(manifest = self,
375 name = name,
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700376 remote = remote.ToRemoteSpec(name),
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800377 gitdir = gitdir,
378 worktree = None,
379 relpath = None,
Shawn O. Pearce3c8dea12009-05-29 18:38:17 -0700380 revisionExpr = m.revisionExpr,
381 revisionId = None)
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800382 self._projects[project.name] = project
383
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700384 def _ParseRemote(self, node):
385 """
386 reads a <remote> element from the manifest file
387 """
388 name = self._reqatt(node, 'name')
389 fetch = self._reqatt(node, 'fetch')
390 review = node.getAttribute('review')
Shawn O. Pearceae6e0942008-11-06 10:25:35 -0800391 if review == '':
392 review = None
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700393 return _XmlRemote(name, fetch, review)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700394
395 def _ParseDefault(self, node):
396 """
397 reads a <default> element from the manifest file
398 """
399 d = _Default()
400 d.remote = self._get_remote(node)
Shawn O. Pearce3c8dea12009-05-29 18:38:17 -0700401 d.revisionExpr = node.getAttribute('revision')
402 if d.revisionExpr == '':
403 d.revisionExpr = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700404 return d
405
Doug Anderson2b8db3c2010-11-01 15:08:06 -0700406 def _ParseNotice(self, node):
407 """
408 reads a <notice> element from the manifest file
409
410 The <notice> element is distinct from other tags in the XML in that the
411 data is conveyed between the start and end tag (it's not an empty-element
412 tag).
413
414 The white space (carriage returns, indentation) for the notice element is
415 relevant and is parsed in a way that is based on how python docstrings work.
416 In fact, the code is remarkably similar to here:
417 http://www.python.org/dev/peps/pep-0257/
418 """
419 # Get the data out of the node...
420 notice = node.childNodes[0].data
421
422 # Figure out minimum indentation, skipping the first line (the same line
423 # as the <notice> tag)...
424 minIndent = sys.maxint
425 lines = notice.splitlines()
426 for line in lines[1:]:
427 lstrippedLine = line.lstrip()
428 if lstrippedLine:
429 indent = len(line) - len(lstrippedLine)
430 minIndent = min(indent, minIndent)
431
432 # Strip leading / trailing blank lines and also indentation.
433 cleanLines = [lines[0].strip()]
434 for line in lines[1:]:
435 cleanLines.append(line[minIndent:].rstrip())
436
437 # Clear completely blank lines from front and back...
438 while cleanLines and not cleanLines[0]:
439 del cleanLines[0]
440 while cleanLines and not cleanLines[-1]:
441 del cleanLines[-1]
442
443 return '\n'.join(cleanLines)
444
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700445 def _ParseProject(self, node):
446 """
447 reads a <project> element from the manifest file
Nico Sallembiena1bfd2c2010-04-06 10:40:01 -0700448 """
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700449 name = self._reqatt(node, 'name')
450
451 remote = self._get_remote(node)
452 if remote is None:
453 remote = self._default.remote
454 if remote is None:
455 raise ManifestParseError, \
456 "no remote for project %s within %s" % \
457 (name, self.manifestFile)
458
Shawn O. Pearce3c8dea12009-05-29 18:38:17 -0700459 revisionExpr = node.getAttribute('revision')
460 if not revisionExpr:
461 revisionExpr = self._default.revisionExpr
462 if not revisionExpr:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700463 raise ManifestParseError, \
464 "no revision for project %s within %s" % \
465 (name, self.manifestFile)
466
467 path = node.getAttribute('path')
468 if not path:
469 path = name
470 if path.startswith('/'):
471 raise ManifestParseError, \
472 "project %s path cannot be absolute in %s" % \
473 (name, self.manifestFile)
474
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800475 if self.IsMirror:
476 relpath = None
477 worktree = None
478 gitdir = os.path.join(self.topdir, '%s.git' % name)
479 else:
Anthony Newnamdf14a702011-01-09 17:31:57 -0800480 worktree = os.path.join(self.topdir, path).replace('\\', '/')
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800481 gitdir = os.path.join(self.repodir, 'projects/%s.git' % path)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700482
483 project = Project(manifest = self,
484 name = name,
Shawn O. Pearced1f70d92009-05-19 14:58:02 -0700485 remote = remote.ToRemoteSpec(name),
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700486 gitdir = gitdir,
487 worktree = worktree,
488 relpath = path,
Shawn O. Pearce3c8dea12009-05-29 18:38:17 -0700489 revisionExpr = revisionExpr,
490 revisionId = None)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700491
492 for n in node.childNodes:
Shawn O. Pearce242b5262009-05-19 13:00:29 -0700493 if n.nodeName == 'copyfile':
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700494 self._ParseCopyFile(project, n)
495
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700496 return project
497
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700498 def _ParseCopyFile(self, project, node):
499 src = self._reqatt(node, 'src')
500 dest = self._reqatt(node, 'dest')
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800501 if not self.IsMirror:
502 # src is project relative;
503 # dest is relative to the top of the tree
Shawn O. Pearcec7a4eef2009-03-05 10:32:38 -0800504 project.AddCopyFile(src, dest, os.path.join(self.topdir, dest))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700505
506 def _get_remote(self, node):
507 name = node.getAttribute('remote')
508 if not name:
509 return None
510
511 v = self._remotes.get(name)
512 if not v:
513 raise ManifestParseError, \
514 "remote %s not defined in %s" % \
515 (name, self.manifestFile)
516 return v
517
518 def _reqatt(self, node, attname):
519 """
520 reads a required attribute from the node.
521 """
522 v = node.getAttribute(attname)
523 if not v:
524 raise ManifestParseError, \
525 "no %s in <%s> within %s" % \
526 (attname, node.nodeName, self.manifestFile)
527 return v