diff options
Diffstat (limited to 'manifest_xml.py')
-rw-r--r-- | manifest_xml.py | 250 |
1 files changed, 184 insertions, 66 deletions
diff --git a/manifest_xml.py b/manifest_xml.py index dd163bed..53f33537 100644 --- a/manifest_xml.py +++ b/manifest_xml.py | |||
@@ -13,6 +13,7 @@ | |||
13 | # See the License for the specific language governing permissions and | 13 | # See the License for the specific language governing permissions and |
14 | # limitations under the License. | 14 | # limitations under the License. |
15 | 15 | ||
16 | from __future__ import print_function | ||
16 | import itertools | 17 | import itertools |
17 | import os | 18 | import os |
18 | import re | 19 | import re |
@@ -27,6 +28,7 @@ from error import ManifestParseError | |||
27 | 28 | ||
28 | MANIFEST_FILE_NAME = 'manifest.xml' | 29 | MANIFEST_FILE_NAME = 'manifest.xml' |
29 | LOCAL_MANIFEST_NAME = 'local_manifest.xml' | 30 | LOCAL_MANIFEST_NAME = 'local_manifest.xml' |
31 | LOCAL_MANIFESTS_DIR_NAME = 'local_manifests' | ||
30 | 32 | ||
31 | urlparse.uses_relative.extend(['ssh', 'git']) | 33 | urlparse.uses_relative.extend(['ssh', 'git']) |
32 | urlparse.uses_netloc.extend(['ssh', 'git']) | 34 | urlparse.uses_netloc.extend(['ssh', 'git']) |
@@ -38,6 +40,7 @@ class _Default(object): | |||
38 | remote = None | 40 | remote = None |
39 | sync_j = 1 | 41 | sync_j = 1 |
40 | sync_c = False | 42 | sync_c = False |
43 | sync_s = False | ||
41 | 44 | ||
42 | class _XmlRemote(object): | 45 | class _XmlRemote(object): |
43 | def __init__(self, | 46 | def __init__(self, |
@@ -53,15 +56,28 @@ class _XmlRemote(object): | |||
53 | self.reviewUrl = review | 56 | self.reviewUrl = review |
54 | self.resolvedFetchUrl = self._resolveFetchUrl() | 57 | self.resolvedFetchUrl = self._resolveFetchUrl() |
55 | 58 | ||
59 | def __eq__(self, other): | ||
60 | return self.__dict__ == other.__dict__ | ||
61 | |||
62 | def __ne__(self, other): | ||
63 | return self.__dict__ != other.__dict__ | ||
64 | |||
56 | def _resolveFetchUrl(self): | 65 | def _resolveFetchUrl(self): |
57 | url = self.fetchUrl.rstrip('/') | 66 | url = self.fetchUrl.rstrip('/') |
58 | manifestUrl = self.manifestUrl.rstrip('/') | 67 | manifestUrl = self.manifestUrl.rstrip('/') |
68 | p = manifestUrl.startswith('persistent-http') | ||
69 | if p: | ||
70 | manifestUrl = manifestUrl[len('persistent-'):] | ||
71 | |||
59 | # urljoin will get confused if there is no scheme in the base url | 72 | # urljoin will get confused if there is no scheme in the base url |
60 | # ie, if manifestUrl is of the form <hostname:port> | 73 | # ie, if manifestUrl is of the form <hostname:port> |
61 | if manifestUrl.find(':') != manifestUrl.find('/') - 1: | 74 | if manifestUrl.find(':') != manifestUrl.find('/') - 1: |
62 | manifestUrl = 'gopher://' + manifestUrl | 75 | manifestUrl = 'gopher://' + manifestUrl |
63 | url = urlparse.urljoin(manifestUrl, url) | 76 | url = urlparse.urljoin(manifestUrl, url) |
64 | return re.sub(r'^gopher://', '', url) | 77 | url = re.sub(r'^gopher://', '', url) |
78 | if p: | ||
79 | url = 'persistent-' + url | ||
80 | return url | ||
65 | 81 | ||
66 | def ToRemoteSpec(self, projectName): | 82 | def ToRemoteSpec(self, projectName): |
67 | url = self.resolvedFetchUrl.rstrip('/') + '/' + projectName | 83 | url = self.resolvedFetchUrl.rstrip('/') + '/' + projectName |
@@ -110,11 +126,11 @@ class XmlManifest(object): | |||
110 | self.Override(name) | 126 | self.Override(name) |
111 | 127 | ||
112 | try: | 128 | try: |
113 | if os.path.exists(self.manifestFile): | 129 | if os.path.lexists(self.manifestFile): |
114 | os.remove(self.manifestFile) | 130 | os.remove(self.manifestFile) |
115 | os.symlink('manifests/%s' % name, self.manifestFile) | 131 | os.symlink('manifests/%s' % name, self.manifestFile) |
116 | except OSError: | 132 | except OSError as e: |
117 | raise ManifestParseError('cannot link manifest %s' % name) | 133 | raise ManifestParseError('cannot link manifest %s: %s' % (name, str(e))) |
118 | 134 | ||
119 | def _RemoteToXml(self, r, doc, root): | 135 | def _RemoteToXml(self, r, doc, root): |
120 | e = doc.createElement('remote') | 136 | e = doc.createElement('remote') |
@@ -130,9 +146,8 @@ class XmlManifest(object): | |||
130 | mp = self.manifestProject | 146 | mp = self.manifestProject |
131 | 147 | ||
132 | groups = mp.config.GetString('manifest.groups') | 148 | groups = mp.config.GetString('manifest.groups') |
133 | if not groups: | 149 | if groups: |
134 | groups = 'all' | 150 | groups = [x for x in re.split(r'[,\s]+', groups) if x] |
135 | groups = [x for x in re.split(r'[,\s]+', groups) if x] | ||
136 | 151 | ||
137 | doc = xml.dom.minidom.Document() | 152 | doc = xml.dom.minidom.Document() |
138 | root = doc.createElement('manifest') | 153 | root = doc.createElement('manifest') |
@@ -170,6 +185,9 @@ class XmlManifest(object): | |||
170 | if d.sync_c: | 185 | if d.sync_c: |
171 | have_default = True | 186 | have_default = True |
172 | e.setAttribute('sync-c', 'true') | 187 | e.setAttribute('sync-c', 'true') |
188 | if d.sync_s: | ||
189 | have_default = True | ||
190 | e.setAttribute('sync-s', 'true') | ||
173 | if have_default: | 191 | if have_default: |
174 | root.appendChild(e) | 192 | root.appendChild(e) |
175 | root.appendChild(doc.createTextNode('')) | 193 | root.appendChild(doc.createTextNode('')) |
@@ -180,20 +198,25 @@ class XmlManifest(object): | |||
180 | root.appendChild(e) | 198 | root.appendChild(e) |
181 | root.appendChild(doc.createTextNode('')) | 199 | root.appendChild(doc.createTextNode('')) |
182 | 200 | ||
183 | sort_projects = list(self.projects.keys()) | 201 | def output_projects(parent, parent_node, projects): |
184 | sort_projects.sort() | 202 | for p in projects: |
185 | 203 | output_project(parent, parent_node, self.projects[p]) | |
186 | for p in sort_projects: | ||
187 | p = self.projects[p] | ||
188 | 204 | ||
205 | def output_project(parent, parent_node, p): | ||
189 | if not p.MatchesGroups(groups): | 206 | if not p.MatchesGroups(groups): |
190 | continue | 207 | return |
208 | |||
209 | name = p.name | ||
210 | relpath = p.relpath | ||
211 | if parent: | ||
212 | name = self._UnjoinName(parent.name, name) | ||
213 | relpath = self._UnjoinRelpath(parent.relpath, relpath) | ||
191 | 214 | ||
192 | e = doc.createElement('project') | 215 | e = doc.createElement('project') |
193 | root.appendChild(e) | 216 | parent_node.appendChild(e) |
194 | e.setAttribute('name', p.name) | 217 | e.setAttribute('name', name) |
195 | if p.relpath != p.name: | 218 | if relpath != name: |
196 | e.setAttribute('path', p.relpath) | 219 | e.setAttribute('path', relpath) |
197 | if not d.remote or p.remote.name != d.remote.name: | 220 | if not d.remote or p.remote.name != d.remote.name: |
198 | e.setAttribute('remote', p.remote.name) | 221 | e.setAttribute('remote', p.remote.name) |
199 | if peg_rev: | 222 | if peg_rev: |
@@ -231,6 +254,19 @@ class XmlManifest(object): | |||
231 | if p.sync_c: | 254 | if p.sync_c: |
232 | e.setAttribute('sync-c', 'true') | 255 | e.setAttribute('sync-c', 'true') |
233 | 256 | ||
257 | if p.sync_s: | ||
258 | e.setAttribute('sync-s', 'true') | ||
259 | |||
260 | if p.subprojects: | ||
261 | sort_projects = [subp.name for subp in p.subprojects] | ||
262 | sort_projects.sort() | ||
263 | output_projects(p, e, sort_projects) | ||
264 | |||
265 | sort_projects = [key for key in self.projects.keys() | ||
266 | if not self.projects[key].parent] | ||
267 | sort_projects.sort() | ||
268 | output_projects(None, root, sort_projects) | ||
269 | |||
234 | if self._repo_hooks_project: | 270 | if self._repo_hooks_project: |
235 | root.appendChild(doc.createTextNode('')) | 271 | root.appendChild(doc.createTextNode('')) |
236 | e = doc.createElement('repo-hooks') | 272 | e = doc.createElement('repo-hooks') |
@@ -299,9 +335,30 @@ class XmlManifest(object): | |||
299 | 335 | ||
300 | local = os.path.join(self.repodir, LOCAL_MANIFEST_NAME) | 336 | local = os.path.join(self.repodir, LOCAL_MANIFEST_NAME) |
301 | if os.path.exists(local): | 337 | if os.path.exists(local): |
338 | print('warning: %s is deprecated; put local manifests in %s instead' | ||
339 | % (LOCAL_MANIFEST_NAME, LOCAL_MANIFESTS_DIR_NAME), | ||
340 | file=sys.stderr) | ||
302 | nodes.append(self._ParseManifestXml(local, self.repodir)) | 341 | nodes.append(self._ParseManifestXml(local, self.repodir)) |
303 | 342 | ||
304 | self._ParseManifest(nodes) | 343 | local_dir = os.path.abspath(os.path.join(self.repodir, LOCAL_MANIFESTS_DIR_NAME)) |
344 | try: | ||
345 | for local_file in sorted(os.listdir(local_dir)): | ||
346 | if local_file.endswith('.xml'): | ||
347 | try: | ||
348 | local = os.path.join(local_dir, local_file) | ||
349 | nodes.append(self._ParseManifestXml(local, self.repodir)) | ||
350 | except ManifestParseError as e: | ||
351 | print('%s' % str(e), file=sys.stderr) | ||
352 | except OSError: | ||
353 | pass | ||
354 | |||
355 | try: | ||
356 | self._ParseManifest(nodes) | ||
357 | except ManifestParseError as e: | ||
358 | # There was a problem parsing, unload ourselves in case they catch | ||
359 | # this error and try again later, we will show the correct error | ||
360 | self._Unload() | ||
361 | raise e | ||
305 | 362 | ||
306 | if self.IsMirror: | 363 | if self.IsMirror: |
307 | self._AddMetaProjectMirror(self.repoProject) | 364 | self._AddMetaProjectMirror(self.repoProject) |
@@ -310,7 +367,11 @@ class XmlManifest(object): | |||
310 | self._loaded = True | 367 | self._loaded = True |
311 | 368 | ||
312 | def _ParseManifestXml(self, path, include_root): | 369 | def _ParseManifestXml(self, path, include_root): |
313 | root = xml.dom.minidom.parse(path) | 370 | try: |
371 | root = xml.dom.minidom.parse(path) | ||
372 | except (OSError, xml.parsers.expat.ExpatError) as e: | ||
373 | raise ManifestParseError("error parsing manifest %s: %s" % (path, e)) | ||
374 | |||
314 | if not root or not root.childNodes: | 375 | if not root or not root.childNodes: |
315 | raise ManifestParseError("no root node in %s" % (path,)) | 376 | raise ManifestParseError("no root node in %s" % (path,)) |
316 | 377 | ||
@@ -323,35 +384,38 @@ class XmlManifest(object): | |||
323 | nodes = [] | 384 | nodes = [] |
324 | for node in manifest.childNodes: # pylint:disable=W0631 | 385 | for node in manifest.childNodes: # pylint:disable=W0631 |
325 | # We only get here if manifest is initialised | 386 | # We only get here if manifest is initialised |
326 | if node.nodeName == 'include': | 387 | if node.nodeName == 'include': |
327 | name = self._reqatt(node, 'name') | 388 | name = self._reqatt(node, 'name') |
328 | fp = os.path.join(include_root, name) | 389 | fp = os.path.join(include_root, name) |
329 | if not os.path.isfile(fp): | 390 | if not os.path.isfile(fp): |
330 | raise ManifestParseError, \ | 391 | raise ManifestParseError, \ |
331 | "include %s doesn't exist or isn't a file" % \ | 392 | "include %s doesn't exist or isn't a file" % \ |
332 | (name,) | 393 | (name,) |
333 | try: | 394 | try: |
334 | nodes.extend(self._ParseManifestXml(fp, include_root)) | 395 | nodes.extend(self._ParseManifestXml(fp, include_root)) |
335 | # should isolate this to the exact exception, but that's | 396 | # should isolate this to the exact exception, but that's |
336 | # tricky. actual parsing implementation may vary. | 397 | # tricky. actual parsing implementation may vary. |
337 | except (KeyboardInterrupt, RuntimeError, SystemExit): | 398 | except (KeyboardInterrupt, RuntimeError, SystemExit): |
338 | raise | 399 | raise |
339 | except Exception as e: | 400 | except Exception as e: |
340 | raise ManifestParseError( | 401 | raise ManifestParseError( |
341 | "failed parsing included manifest %s: %s", (name, e)) | 402 | "failed parsing included manifest %s: %s", (name, e)) |
342 | else: | 403 | else: |
343 | nodes.append(node) | 404 | nodes.append(node) |
344 | return nodes | 405 | return nodes |
345 | 406 | ||
346 | def _ParseManifest(self, node_list): | 407 | def _ParseManifest(self, node_list): |
347 | for node in itertools.chain(*node_list): | 408 | for node in itertools.chain(*node_list): |
348 | if node.nodeName == 'remote': | 409 | if node.nodeName == 'remote': |
349 | remote = self._ParseRemote(node) | 410 | remote = self._ParseRemote(node) |
350 | if self._remotes.get(remote.name): | 411 | if remote: |
351 | raise ManifestParseError( | 412 | if remote.name in self._remotes: |
352 | 'duplicate remote %s in %s' % | 413 | if remote != self._remotes[remote.name]: |
353 | (remote.name, self.manifestFile)) | 414 | raise ManifestParseError( |
354 | self._remotes[remote.name] = remote | 415 | 'remote %s already exists with different attributes' % |
416 | (remote.name)) | ||
417 | else: | ||
418 | self._remotes[remote.name] = remote | ||
355 | 419 | ||
356 | for node in itertools.chain(*node_list): | 420 | for node in itertools.chain(*node_list): |
357 | if node.nodeName == 'default': | 421 | if node.nodeName == 'default': |
@@ -375,19 +439,24 @@ class XmlManifest(object): | |||
375 | if node.nodeName == 'manifest-server': | 439 | if node.nodeName == 'manifest-server': |
376 | url = self._reqatt(node, 'url') | 440 | url = self._reqatt(node, 'url') |
377 | if self._manifest_server is not None: | 441 | if self._manifest_server is not None: |
378 | raise ManifestParseError( | 442 | raise ManifestParseError( |
379 | 'duplicate manifest-server in %s' % | 443 | 'duplicate manifest-server in %s' % |
380 | (self.manifestFile)) | 444 | (self.manifestFile)) |
381 | self._manifest_server = url | 445 | self._manifest_server = url |
382 | 446 | ||
447 | def recursively_add_projects(project): | ||
448 | if self._projects.get(project.name): | ||
449 | raise ManifestParseError( | ||
450 | 'duplicate project %s in %s' % | ||
451 | (project.name, self.manifestFile)) | ||
452 | self._projects[project.name] = project | ||
453 | for subproject in project.subprojects: | ||
454 | recursively_add_projects(subproject) | ||
455 | |||
383 | for node in itertools.chain(*node_list): | 456 | for node in itertools.chain(*node_list): |
384 | if node.nodeName == 'project': | 457 | if node.nodeName == 'project': |
385 | project = self._ParseProject(node) | 458 | project = self._ParseProject(node) |
386 | if self._projects.get(project.name): | 459 | recursively_add_projects(project) |
387 | raise ManifestParseError( | ||
388 | 'duplicate project %s in %s' % | ||
389 | (project.name, self.manifestFile)) | ||
390 | self._projects[project.name] = project | ||
391 | if node.nodeName == 'repo-hooks': | 460 | if node.nodeName == 'repo-hooks': |
392 | # Get the name of the project and the (space-separated) list of enabled. | 461 | # Get the name of the project and the (space-separated) list of enabled. |
393 | repo_hooks_project = self._reqatt(node, 'in-project') | 462 | repo_hooks_project = self._reqatt(node, 'in-project') |
@@ -414,9 +483,8 @@ class XmlManifest(object): | |||
414 | try: | 483 | try: |
415 | del self._projects[name] | 484 | del self._projects[name] |
416 | except KeyError: | 485 | except KeyError: |
417 | raise ManifestParseError( | 486 | raise ManifestParseError('remove-project element specifies non-existent ' |
418 | 'project %s not found' % | 487 | 'project: %s' % name) |
419 | (name)) | ||
420 | 488 | ||
421 | # If the manifest removes the hooks project, treat it as if it deleted | 489 | # If the manifest removes the hooks project, treat it as if it deleted |
422 | # the repo-hooks element too. | 490 | # the repo-hooks element too. |
@@ -496,6 +564,12 @@ class XmlManifest(object): | |||
496 | d.sync_c = False | 564 | d.sync_c = False |
497 | else: | 565 | else: |
498 | d.sync_c = sync_c.lower() in ("yes", "true", "1") | 566 | d.sync_c = sync_c.lower() in ("yes", "true", "1") |
567 | |||
568 | sync_s = node.getAttribute('sync-s') | ||
569 | if not sync_s: | ||
570 | d.sync_s = False | ||
571 | else: | ||
572 | d.sync_s = sync_s.lower() in ("yes", "true", "1") | ||
499 | return d | 573 | return d |
500 | 574 | ||
501 | def _ParseNotice(self, node): | 575 | def _ParseNotice(self, node): |
@@ -537,11 +611,19 @@ class XmlManifest(object): | |||
537 | 611 | ||
538 | return '\n'.join(cleanLines) | 612 | return '\n'.join(cleanLines) |
539 | 613 | ||
540 | def _ParseProject(self, node): | 614 | def _JoinName(self, parent_name, name): |
615 | return os.path.join(parent_name, name) | ||
616 | |||
617 | def _UnjoinName(self, parent_name, name): | ||
618 | return os.path.relpath(name, parent_name) | ||
619 | |||
620 | def _ParseProject(self, node, parent = None): | ||
541 | """ | 621 | """ |
542 | reads a <project> element from the manifest file | 622 | reads a <project> element from the manifest file |
543 | """ | 623 | """ |
544 | name = self._reqatt(node, 'name') | 624 | name = self._reqatt(node, 'name') |
625 | if parent: | ||
626 | name = self._JoinName(parent.name, name) | ||
545 | 627 | ||
546 | remote = self._get_remote(node) | 628 | remote = self._get_remote(node) |
547 | if remote is None: | 629 | if remote is None: |
@@ -579,44 +661,80 @@ class XmlManifest(object): | |||
579 | else: | 661 | else: |
580 | sync_c = sync_c.lower() in ("yes", "true", "1") | 662 | sync_c = sync_c.lower() in ("yes", "true", "1") |
581 | 663 | ||
664 | sync_s = node.getAttribute('sync-s') | ||
665 | if not sync_s: | ||
666 | sync_s = self._default.sync_s | ||
667 | else: | ||
668 | sync_s = sync_s.lower() in ("yes", "true", "1") | ||
669 | |||
582 | upstream = node.getAttribute('upstream') | 670 | upstream = node.getAttribute('upstream') |
583 | 671 | ||
584 | groups = '' | 672 | groups = '' |
585 | if node.hasAttribute('groups'): | 673 | if node.hasAttribute('groups'): |
586 | groups = node.getAttribute('groups') | 674 | groups = node.getAttribute('groups') |
587 | groups = [x for x in re.split('[,\s]+', groups) if x] | 675 | groups = [x for x in re.split(r'[,\s]+', groups) if x] |
588 | |||
589 | default_groups = ['all', 'name:%s' % name, 'path:%s' % path] | ||
590 | groups.extend(set(default_groups).difference(groups)) | ||
591 | 676 | ||
592 | if self.IsMirror: | 677 | if parent is None: |
593 | worktree = None | 678 | relpath, worktree, gitdir = self.GetProjectPaths(name, path) |
594 | gitdir = os.path.join(self.topdir, '%s.git' % name) | ||
595 | else: | 679 | else: |
596 | worktree = os.path.join(self.topdir, path).replace('\\', '/') | 680 | relpath, worktree, gitdir = self.GetSubprojectPaths(parent, path) |
597 | gitdir = os.path.join(self.repodir, 'projects/%s.git' % path) | 681 | |
682 | default_groups = ['all', 'name:%s' % name, 'path:%s' % relpath] | ||
683 | groups.extend(set(default_groups).difference(groups)) | ||
598 | 684 | ||
599 | project = Project(manifest = self, | 685 | project = Project(manifest = self, |
600 | name = name, | 686 | name = name, |
601 | remote = remote.ToRemoteSpec(name), | 687 | remote = remote.ToRemoteSpec(name), |
602 | gitdir = gitdir, | 688 | gitdir = gitdir, |
603 | worktree = worktree, | 689 | worktree = worktree, |
604 | relpath = path, | 690 | relpath = relpath, |
605 | revisionExpr = revisionExpr, | 691 | revisionExpr = revisionExpr, |
606 | revisionId = None, | 692 | revisionId = None, |
607 | rebase = rebase, | 693 | rebase = rebase, |
608 | groups = groups, | 694 | groups = groups, |
609 | sync_c = sync_c, | 695 | sync_c = sync_c, |
610 | upstream = upstream) | 696 | sync_s = sync_s, |
697 | upstream = upstream, | ||
698 | parent = parent) | ||
611 | 699 | ||
612 | for n in node.childNodes: | 700 | for n in node.childNodes: |
613 | if n.nodeName == 'copyfile': | 701 | if n.nodeName == 'copyfile': |
614 | self._ParseCopyFile(project, n) | 702 | self._ParseCopyFile(project, n) |
615 | if n.nodeName == 'annotation': | 703 | if n.nodeName == 'annotation': |
616 | self._ParseAnnotation(project, n) | 704 | self._ParseAnnotation(project, n) |
705 | if n.nodeName == 'project': | ||
706 | project.subprojects.append(self._ParseProject(n, parent = project)) | ||
617 | 707 | ||
618 | return project | 708 | return project |
619 | 709 | ||
710 | def GetProjectPaths(self, name, path): | ||
711 | relpath = path | ||
712 | if self.IsMirror: | ||
713 | worktree = None | ||
714 | gitdir = os.path.join(self.topdir, '%s.git' % name) | ||
715 | else: | ||
716 | worktree = os.path.join(self.topdir, path).replace('\\', '/') | ||
717 | gitdir = os.path.join(self.repodir, 'projects', '%s.git' % path) | ||
718 | return relpath, worktree, gitdir | ||
719 | |||
720 | def GetSubprojectName(self, parent, submodule_path): | ||
721 | return os.path.join(parent.name, submodule_path) | ||
722 | |||
723 | def _JoinRelpath(self, parent_relpath, relpath): | ||
724 | return os.path.join(parent_relpath, relpath) | ||
725 | |||
726 | def _UnjoinRelpath(self, parent_relpath, relpath): | ||
727 | return os.path.relpath(relpath, parent_relpath) | ||
728 | |||
729 | def GetSubprojectPaths(self, parent, path): | ||
730 | relpath = self._JoinRelpath(parent.relpath, path) | ||
731 | gitdir = os.path.join(parent.gitdir, 'subprojects', '%s.git' % path) | ||
732 | if self.IsMirror: | ||
733 | worktree = None | ||
734 | else: | ||
735 | worktree = os.path.join(parent.worktree, path).replace('\\', '/') | ||
736 | return relpath, worktree, gitdir | ||
737 | |||
620 | def _ParseCopyFile(self, project, node): | 738 | def _ParseCopyFile(self, project, node): |
621 | src = self._reqatt(node, 'src') | 739 | src = self._reqatt(node, 'src') |
622 | dest = self._reqatt(node, 'dest') | 740 | dest = self._reqatt(node, 'dest') |