Package roslib :: Module manifestlib
[frames] | no frames]

Source Code for Module roslib.manifestlib

  1  #! /usr/bin/env python 
  2  # Software License Agreement (BSD License) 
  3  # 
  4  # Copyright (c) 2008, Willow Garage, Inc. 
  5  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions 
  9  # are met: 
 10  # 
 11  #  * Redistributions of source code must retain the above copyright 
 12  #    notice, this list of conditions and the following disclaimer. 
 13  #  * Redistributions in binary form must reproduce the above 
 14  #    copyright notice, this list of conditions and the following 
 15  #    disclaimer in the documentation and/or other materials provided 
 16  #    with the distribution. 
 17  #  * Neither the name of Willow Garage, Inc. nor the names of its 
 18  #    contributors may be used to endorse or promote products derived 
 19  #    from this software without specific prior written permission. 
 20  # 
 21  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 22  # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 23  # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 24  # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 25  # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 26  # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 27  # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 28  # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 29  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 30  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 31  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 32  # POSSIBILITY OF SUCH DAMAGE. 
 33  # 
 34  # Revision $Id$ 
 35  # $Author$ 
 36   
 37  """ 
 38  Internal library for processing 'manifest' files, i.e. manifest.xml and stack.xml. 
 39  For external code apis, see L{roslib.manifest} and L{roslib.stack_manifest}. 
 40  """ 
 41   
 42  import os 
 43  import xml.dom.minidom as dom 
 44   
 45  import roslib.exceptions 
 46   
 47  # stack.xml and manifest.xml have the same internal tags right now 
 48  REQUIRED = ['author', 'license'] 
 49  ALLOWXHTML = ['description'] 
 50  OPTIONAL = ['logo', 'url', 'brief', 'description', 'status', 
 51              'notes', 'depend', 'rosdep', 'export', 'review', 
 52              'versioncontrol', 'platform', 'version', 'rosbuild2', 
 53              'catkin'] 
 54  VALID = REQUIRED + OPTIONAL 
 55   
 56   
57 -class ManifestException(roslib.exceptions.ROSLibException):
58 pass
59 60
61 -def get_nodes_by_name(n, name):
62 return [t for t in n.childNodes if t.nodeType == t.ELEMENT_NODE and t.tagName == name]
63 64
65 -def check_optional(name, allowXHTML=False, merge_multiple=False):
66 """ 67 Validator for optional elements. 68 @raise ManifestException: if validation fails 69 """ 70 def check(n, filename): 71 n = get_nodes_by_name(n, name) 72 if len(n) > 1 and not merge_multiple: 73 raise ManifestException("Invalid manifest file: must have a single '%s' element" % name) 74 if n: 75 values = [] 76 for child in n: 77 if allowXHTML: 78 values.append(''.join([x.toxml() for x in child.childNodes])) 79 else: 80 values.append(_get_text(child.childNodes).strip()) 81 return ', '.join(values)
82 return check 83 84
85 -def check_required(name, allowXHTML=False, merge_multiple=False):
86 """ 87 Validator for required elements. 88 @raise ManifestException: if validation fails 89 """ 90 def check(n, filename): 91 n = get_nodes_by_name(n, name) 92 if not n: 93 # print >> sys.stderr, "Invalid manifest file[%s]: missing required '%s' element"%(filename, name) 94 return '' 95 if len(n) != 1 and not merge_multiple: 96 raise ManifestException("Invalid manifest file: must have only one '%s' element" % name) 97 values = [] 98 for child in n: 99 if allowXHTML: 100 values.append(''.join([x.toxml() for x in child.childNodes])) 101 else: 102 values.append(_get_text(child.childNodes).strip()) 103 return ', '.join(values)
104 return check 105 106
107 -def check_platform(name):
108 """ 109 Validator for manifest platform. 110 @raise ManifestException: if validation fails 111 """ 112 def check(n, filename): 113 platforms = get_nodes_by_name(n, name) 114 try: 115 vals = [(p.attributes['os'].value, p.attributes['version'].value, p.getAttribute('notes')) for p in platforms] 116 except KeyError as e: 117 raise ManifestException("<platform> tag is missing required '%s' attribute" % str(e)) 118 return [Platform(*v) for v in vals]
119 return check 120 121
122 -def check_depends(name):
123 """ 124 Validator for manifest depends. 125 @raise ManifestException: if validation fails 126 """ 127 def check(n, filename): 128 nodes = get_nodes_by_name(n, name) 129 # TDS 20110419: this is a hack. 130 # rosbuild2 has a <depend thirdparty="depname"/> tag, 131 # which is confusing this subroutine with 132 # KeyError: 'package' 133 # for now, explicitly don't consider thirdparty depends 134 depends = [e.attributes for e in nodes if 'thirdparty' not in e.attributes.keys()] 135 try: 136 packages = [d['package'].value for d in depends] 137 except KeyError: 138 raise ManifestException("Invalid manifest file: depends is missing 'package' attribute") 139 140 return [Depend(p) for p in packages]
141 return check 142 143
144 -def check_stack_depends(name):
145 """ 146 Validator for stack depends. 147 @raise ManifestException: if validation fails 148 """ 149 def check(n, filename): 150 nodes = get_nodes_by_name(n, name) 151 depends = [e.attributes for e in nodes] 152 packages = [d['stack'].value for d in depends] 153 return [StackDepend(p) for p in packages]
154 return check 155 156
157 -def check_rosdeps(name):
158 """ 159 Validator for stack rosdeps. 160 @raise ManifestException: if validation fails 161 """ 162 def check(n, filename): 163 nodes = get_nodes_by_name(n, name) 164 rosdeps = [e.attributes for e in nodes] 165 names = [d['name'].value for d in rosdeps] 166 return [ROSDep(n) for n in names]
167 return check 168 169
170 -def _attrs(node):
171 attrs = {} 172 for k in node.attributes.keys(): 173 attrs[k] = node.attributes.get(k).value 174 return attrs
175 176
177 -def check_exports(name):
178 def check(n, filename): 179 ret_val = [] 180 for e in get_nodes_by_name(n, name): 181 elements = [c for c in e.childNodes if c.nodeType == c.ELEMENT_NODE] 182 ret_val.extend([Export(t.tagName, _attrs(t), _get_text(t.childNodes)) for t in elements]) 183 return ret_val
184 return check 185 186
187 -def check_versioncontrol(name):
188 def check(n, filename): 189 e = get_nodes_by_name(n, name) 190 if not e: 191 return None 192 # note: 'url' isn't actually required, but as we only support type=svn it implicitly is for now 193 return VersionControl(e[0].attributes['type'].value, e[0].attributes['url'].value)
194 return check 195 196
197 -def check(name, merge_multiple=False):
198 if name == 'depend': 199 return check_depends('depend') 200 elif name == 'export': 201 return check_exports('export') 202 elif name == 'versioncontrol': 203 return check_versioncontrol('versioncontrol') 204 elif name == 'rosdep': 205 return check_rosdeps('rosdep') 206 elif name == 'platform': 207 return check_platform('platform') 208 elif name in REQUIRED: 209 if name in ALLOWXHTML: 210 return check_required(name, True, merge_multiple) 211 return check_required(name, merge_multiple=merge_multiple) 212 elif name in OPTIONAL: 213 if name in ALLOWXHTML: 214 return check_optional(name, True, merge_multiple) 215 return check_optional(name, merge_multiple=merge_multiple)
216 217
218 -class Export(object):
219 """ 220 Manifest 'export' tag 221 """ 222
223 - def __init__(self, tag, attrs, str):
224 """ 225 Create new export instance. 226 @param tag: name of the XML tag 227 @type tag: str 228 @param attrs: dictionary of XML attributes for this export tag 229 @type attrs: dict 230 @param str: string value contained by tag, if any 231 @type str: str 232 """ 233 self.tag = tag 234 self.attrs = attrs 235 self.str = str
236
237 - def get(self, attr):
238 """ 239 @return: value of attribute or None if attribute not set 240 @rtype: str 241 """ 242 return self.attrs.get(attr, None)
243
244 - def xml(self):
245 """ 246 @return: export instance represented as manifest XML 247 @rtype: str 248 """ 249 attrs = ' '.join([' %s="%s"' % (k, v) for k, v in self.attrs.items()]) # py3k 250 if self.str: 251 return '<%s%s>%s</%s>' % (self.tag, attrs, self.str, self.tag) 252 else: 253 return '<%s%s />' % (self.tag, attrs)
254 255
256 -class Platform(object):
257 """ 258 Manifest 'platform' tag 259 """ 260 __slots__ = ['os', 'version', 'notes'] 261
262 - def __init__(self, os, version, notes=None):
263 """ 264 Create new depend instance. 265 @param os: OS name. must be non-empty 266 @type os: str 267 @param version: OS version. must be non-empty 268 @type version: str 269 @param notes: (optional) notes about platform support 270 @type notes: str 271 """ 272 if not os: 273 raise ValueError("bad 'os' attribute") 274 if not version: 275 raise ValueError("bad 'version' attribute") 276 self.os = os 277 self.version = version 278 self.notes = notes
279
280 - def __str__(self):
281 return '%s %s' % (self.os, self.version)
282
283 - def __repr__(self):
284 return '%s %s' % (self.os, self.version)
285
286 - def __eq__(self, obj):
287 """ 288 Override equality test. notes *are* considered in the equality test. 289 """ 290 if not isinstance(obj, Platform): 291 return False 292 return self.os == obj.os and self.version == obj.version and self.notes == obj.notes
293
294 - def xml(self):
295 """ 296 @return: instance represented as manifest XML 297 @rtype: str 298 """ 299 if self.notes is not None: 300 return '<platform os="%s" version="%s" notes="%s"/>' % (self.os, self.version, self.notes) 301 else: 302 return '<platform os="%s" version="%s"/>' % (self.os, self.version)
303 304
305 -class Depend(object):
306 """ 307 Manifest 'depend' tag 308 """ 309 __slots__ = ['package'] 310
311 - def __init__(self, package):
312 """ 313 Create new depend instance. 314 @param package: package name. must be non-empty 315 @type package: str 316 """ 317 if not package: 318 raise ValueError("bad 'package' attribute") 319 self.package = package
320
321 - def __str__(self):
322 return self.package
323
324 - def __repr__(self):
325 return self.package
326
327 - def __eq__(self, obj):
328 if not isinstance(obj, Depend): 329 return False 330 return self.package == obj.package
331
332 - def xml(self):
333 """ 334 @return: depend instance represented as manifest XML 335 @rtype: str 336 """ 337 return '<depend package="%s" />' % self.package
338 339
340 -class StackDepend(object):
341 """ 342 Stack Manifest 'depend' tag 343 """ 344 __slots__ = ['stack', 'annotation'] 345
346 - def __init__(self, stack):
347 """ 348 @param stack: stack name. must be non-empty 349 @type stack: str 350 """ 351 if not stack: 352 raise ValueError("bad 'stack' attribute") 353 self.stack = stack 354 self.annotation = None
355
356 - def __str__(self):
357 return self.stack
358
359 - def __repr__(self):
360 return self.stack
361
362 - def __eq__(self, obj):
363 if not isinstance(obj, StackDepend): 364 return False 365 return self.stack == obj.stack
366
367 - def xml(self):
368 """ 369 @return: stack depend instance represented as stack manifest XML 370 @rtype: str 371 """ 372 if self.annotation: 373 return '<depend stack="%s" /> <!-- %s -->' % (self.stack, self.annotation) 374 else: 375 return '<depend stack="%s" />' % self.stack
376 377
378 -class ROSDep(object):
379 """ 380 Manifest 'rosdep' tag 381 """ 382 __slots__ = ['name', ] 383
384 - def __init__(self, name):
385 """ 386 Create new rosdep instance. 387 @param name: dependency name. Must be non-empty. 388 @type name: str 389 """ 390 if not name: 391 raise ValueError("bad 'name' attribute") 392 self.name = name
393
394 - def xml(self):
395 """ 396 @return: rosdep instance represented as manifest XML 397 @rtype: str 398 """ 399 return '<rosdep name="%s" />' % self.name
400 401
402 -class VersionControl(object):
403 """ 404 Manifest 'versioncontrol' tag 405 """ 406 __slots__ = ['type', 'url'] 407
408 - def __init__(self, type_, url):
409 """ 410 @param type_: version control type (e.g. 'svn'). must be non empty 411 @type type_: str 412 @param url: URL associated with version control. must be non empty 413 @type url: str 414 """ 415 def is_string_type(obj): 416 try: 417 return isinstance(obj, basestring) 418 except NameError: 419 return isinstance(obj, str)
420 421 if not type_ or not is_string_type(type_): 422 raise ValueError("bad 'type' attribute") 423 if url is not None and not is_string_type(url): 424 raise ValueError("bad 'url' attribute") 425 self.type = type_ 426 self.url = url
427
428 - def xml(self):
429 """ 430 @return: versioncontrol instance represented as manifest XML 431 @rtype: str 432 """ 433 if self.url: 434 return '<versioncontrol type="%s" url="%s" />' % (self.type, self.url) 435 else: 436 return '<versioncontrol type="%s" />' % self.type
437 438
439 -class _Manifest(object):
440 """ 441 Object representation of a ROS manifest file 442 """ 443 __slots__ = ['description', 'brief', 444 'author', 'license', 'license_url', 'url', 445 'depends', 'rosdeps', 'platforms', 446 'logo', 'exports', 'version', 447 'versioncontrol', 'status', 'notes', 448 'unknown_tags', 449 '_type'] 450
451 - def __init__(self, _type='package'):
452 self.description = self.brief = self.author = \ 453 self.license = self.license_url = \ 454 self.url = self.logo = self.status = \ 455 self.version = self.notes = '' 456 self.depends = [] 457 self.rosdeps = [] 458 self.exports = [] 459 self.platforms = [] 460 self._type = _type 461 462 # store unrecognized tags during parsing 463 self.unknown_tags = []
464
465 - def __str__(self):
466 return self.xml()
467
468 - def get_export(self, tag, attr):
469 """ 470 @return: exports that match the specified tag and attribute, e.g. 'python', 'path' 471 @rtype: [L{Export}] 472 """ 473 return [e.get(attr) for e in self.exports if e.tag == tag if e.get(attr) is not None]
474
475 - def xml(self):
476 """ 477 @return: Manifest instance as ROS XML manifest 478 @rtype: str 479 """ 480 if not self.brief: 481 desc = ' <description>%s</description>' % self.description 482 else: 483 desc = ' <description brief="%s">%s</description>' % (self.brief, self.description) 484 author = ' <author>%s</author>' % self.author 485 if self.license_url: 486 license = ' <license url="%s">%s</license>' % (self.license_url, self.license) 487 else: 488 license = ' <license>%s</license>' % self.license 489 versioncontrol = url = logo = exports = version = '' 490 if self.url: 491 url = ' <url>%s</url>' % self.url 492 if self.version: 493 version = ' <version>%s</version>' % self.version 494 if self.logo: 495 logo = ' <logo>%s</logo>' % self.logo 496 depends = '\n'.join([' %s' % d.xml() for d in self.depends]) 497 rosdeps = '\n'.join([' %s' % rd.xml() for rd in self.rosdeps]) 498 platforms = '\n'.join([' %s' % p.xml() for p in self.platforms]) 499 if self.exports: 500 exports = ' <export>\n' + '\n'.join([' %s' % e.xml() for e in self.exports]) + ' </export>' 501 if self.versioncontrol: 502 versioncontrol = ' %s' % self.versioncontrol.xml() 503 if self.status or self.notes: 504 review = ' <review status="%s" notes="%s" />' % (self.status, self.notes) 505 506 fields = filter(lambda x: x, 507 [desc, author, license, review, url, logo, depends, 508 rosdeps, platforms, exports, versioncontrol, version]) 509 return '<%s>\n' % self._type + '\n'.join(fields) + '\n</%s>' % self._type
510 511
512 -def _get_text(nodes):
513 """ 514 DOM utility routine for getting contents of text nodes 515 """ 516 return ''.join([n.data for n in nodes if n.nodeType == n.TEXT_NODE])
517 518
519 -def parse_file(m, file):
520 """ 521 Parse manifest file (package, stack) 522 @param m: field to populate 523 @type m: L{_Manifest} 524 @param file: manifest.xml file path 525 @type file: str 526 @return: return m, populated with parsed fields 527 @rtype: L{_Manifest} 528 """ 529 if not file: 530 raise ValueError('Missing manifest file argument') 531 if not os.path.isfile(file): 532 raise ValueError('Invalid/non-existent manifest file: %s' % file) 533 with open(file, 'r') as f: 534 text = f.read() 535 try: 536 return parse(m, text, file) 537 except ManifestException as e: 538 raise ManifestException('Invalid manifest file [%s]: %s' % (os.path.abspath(file), e))
539 540
541 -def parse(m, string, filename='string'):
542 """ 543 Parse manifest.xml string contents 544 @param string: manifest.xml contents 545 @type string: str 546 @param m: field to populate 547 @type m: L{_Manifest} 548 @return: return m, populated with parsed fields 549 @rtype: L{_Manifest} 550 """ 551 try: 552 d = dom.parseString(string) 553 except Exception as e: 554 raise ManifestException('invalid XML: %s' % e) 555 556 p = get_nodes_by_name(d, m._type) 557 if len(p) != 1: 558 raise ManifestException("manifest must have a single '%s' element" % m._type) 559 p = p[0] 560 m.description = check('description')(p, filename) 561 m.brief = '' 562 try: 563 tag = get_nodes_by_name(p, 'description')[0] 564 m.brief = tag.getAttribute('brief') or '' 565 except Exception: 566 # means that 'description' tag is missing 567 pass 568 # TODO: figure out how to multiplex 569 if m._type == 'package': 570 m.depends = check_depends('depend')(p, filename) 571 elif m._type == 'stack': 572 m.depends = check_stack_depends('depend')(p, filename) 573 elif m._type == 'app': 574 # not implemented yet 575 pass 576 m.rosdeps = check('rosdep')(p, filename) 577 m.platforms = check('platform')(p, filename) 578 m.exports = check('export')(p, filename) 579 m.versioncontrol = check('versioncontrol')(p, filename) 580 m.license = check('license')(p, filename) 581 m.license_url = '' 582 try: 583 tag = get_nodes_by_name(p, 'license')[0] 584 m.license_url = tag.getAttribute('url') or '' 585 except Exception: 586 pass # manifest is missing required 'license' tag 587 588 m.status = 'unreviewed' 589 try: 590 tag = get_nodes_by_name(p, 'review')[0] 591 m.status = tag.getAttribute('status') or '' 592 except Exception: 593 pass # manifest is missing optional 'review status' tag 594 595 m.notes = '' 596 try: 597 tag = get_nodes_by_name(p, 'review')[0] 598 m.notes = tag.getAttribute('notes') or '' 599 except Exception: 600 pass # manifest is missing optional 'review notes' tag 601 602 m.author = check('author', True)(p, filename) 603 m.url = check('url')(p, filename) 604 m.version = check('version')(p, filename) 605 m.logo = check('logo')(p, filename) 606 607 # do some validation on what we just parsed 608 if m._type == 'stack': 609 if m.exports: 610 raise ManifestException('stack manifests are not allowed to have exports') 611 if m.rosdeps: 612 raise ManifestException('stack manifests are not allowed to have rosdeps') 613 614 # store unrecognized tags 615 m.unknown_tags = [e for e in p.childNodes if e.nodeType == e.ELEMENT_NODE and e.tagName not in VALID] 616 return m
617