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 sys 
 43  import os 
 44  import xml.dom 
 45  import xml.dom.minidom as dom 
 46   
 47  import roslib.exceptions 
 48   
 49  # stack.xml and manifest.xml have the same internal tags right now 
 50  REQUIRED = ['author', 'license'] 
 51  ALLOWXHTML = ['description'] 
 52  OPTIONAL = ['logo', 'url', 'brief', 'description', 'status', 
 53              'notes', 'depend', 'rosdep', 'export', 'review', 
 54              'versioncontrol', 'platform', 'version', 'rosbuild2', 
 55              'catkin'] 
 56  VALID = REQUIRED + OPTIONAL 
 57   
58 -class ManifestException(roslib.exceptions.ROSLibException): pass
59
60 -def get_nodes_by_name(n, name):
61 return [t for t in n.childNodes if t.nodeType == t.ELEMENT_NODE and t.tagName == name]
62
63 -def check_optional(name, allowXHTML=False, merge_multiple=False):
64 """ 65 Validator for optional elements. 66 @raise ManifestException: if validation fails 67 """ 68 def check(n, filename): 69 n = get_nodes_by_name(n, name) 70 if len(n) > 1 and not merge_multiple: 71 raise ManifestException("Invalid manifest file: must have a single '%s' element"%name) 72 if n: 73 values = [] 74 for child in n: 75 if allowXHTML: 76 values.append(''.join([x.toxml() for x in child.childNodes])) 77 else: 78 values.append(_get_text(child.childNodes).strip()) 79 return ', '.join(values)
80 return check 81
82 -def check_required(name, allowXHTML=False, merge_multiple=False):
83 """ 84 Validator for required elements. 85 @raise ManifestException: if validation fails 86 """ 87 def check(n, filename): 88 n = get_nodes_by_name(n, name) 89 if not n: 90 #print >> sys.stderr, "Invalid manifest file[%s]: missing required '%s' element"%(filename, name) 91 return '' 92 if len(n) != 1 and not merge_multiple: 93 raise ManifestException("Invalid manifest file: must have only one '%s' element"%name) 94 values = [] 95 for child in n: 96 if allowXHTML: 97 values.append(''.join([x.toxml() for x in child.childNodes])) 98 else: 99 values.append(_get_text(child.childNodes).strip()) 100 return ', '.join(values)
101 return check 102
103 -def check_platform(name):
104 """ 105 Validator for manifest platform. 106 @raise ManifestException: if validation fails 107 """ 108 def check(n, filename): 109 platforms = get_nodes_by_name(n, name) 110 try: 111 vals = [(p.attributes['os'].value, p.attributes['version'].value, p.getAttribute('notes')) for p in platforms] 112 except KeyError as e: 113 raise ManifestException("<platform> tag is missing required '%s' attribute"%str(e)) 114 return [Platform(*v) for v in vals]
115 return check 116
117 -def check_depends(name):
118 """ 119 Validator for manifest depends. 120 @raise ManifestException: if validation fails 121 """ 122 def check(n, filename): 123 nodes = get_nodes_by_name(n, name) 124 # TDS 20110419: this is a hack. 125 # rosbuild2 has a <depend thirdparty="depname"/> tag, 126 # which is confusing this subroutine with 127 # KeyError: 'package' 128 # for now, explicitly don't consider thirdparty depends 129 depends = [e.attributes for e in nodes if 'thirdparty' not in e.attributes.keys()] 130 try: 131 packages = [d['package'].value for d in depends] 132 except KeyError: 133 raise ManifestException("Invalid manifest file: depends is missing 'package' attribute") 134 135 return [Depend(p) for p in packages]
136 return check 137
138 -def check_stack_depends(name):
139 """ 140 Validator for stack depends. 141 @raise ManifestException: if validation fails 142 """ 143 def check(n, filename): 144 nodes = get_nodes_by_name(n, name) 145 depends = [e.attributes for e in nodes] 146 packages = [d['stack'].value for d in depends] 147 return [StackDepend(p) for p in packages]
148 return check 149
150 -def check_rosdeps(name):
151 """ 152 Validator for stack rosdeps. 153 @raise ManifestException: if validation fails 154 """ 155 def check(n, filename): 156 nodes = get_nodes_by_name(n, name) 157 rosdeps = [e.attributes for e in nodes] 158 names = [d['name'].value for d in rosdeps] 159 return [ROSDep(n) for n in names]
160 return check 161
162 -def _attrs(node):
163 attrs = {} 164 for k in node.attributes.keys(): 165 attrs[k] = node.attributes.get(k).value 166 return attrs
167
168 -def check_exports(name):
169 def check(n, filename): 170 ret_val = [] 171 for e in get_nodes_by_name(n, name): 172 elements = [c for c in e.childNodes if c.nodeType == c.ELEMENT_NODE] 173 ret_val.extend([Export(t.tagName, _attrs(t), _get_text(t.childNodes)) for t in elements]) 174 return ret_val
175 return check 176
177 -def check_versioncontrol(name):
178 def check(n, filename): 179 e = get_nodes_by_name(n, name) 180 if not e: 181 return None 182 # note: 'url' isn't actually required, but as we only support type=svn it implicitly is for now 183 return VersionControl(e[0].attributes['type'].value, e[0].attributes['url'].value)
184 return check 185
186 -def check(name, merge_multiple=False):
187 if name == 'depend': 188 return check_depends('depend') 189 elif name == 'export': 190 return check_exports('export') 191 elif name == 'versioncontrol': 192 return check_versioncontrol('versioncontrol') 193 elif name == 'rosdep': 194 return check_rosdeps('rosdep') 195 elif name == 'platform': 196 return check_platform('platform') 197 elif name in REQUIRED: 198 if name in ALLOWXHTML: 199 return check_required(name, True, merge_multiple) 200 return check_required(name, merge_multiple=merge_multiple) 201 elif name in OPTIONAL: 202 if name in ALLOWXHTML: 203 return check_optional(name, True, merge_multiple) 204 return check_optional(name, merge_multiple=merge_multiple)
205
206 -class Export(object):
207 """ 208 Manifest 'export' tag 209 """ 210
211 - def __init__(self, tag, attrs, str):
212 """ 213 Create new export instance. 214 @param tag: name of the XML tag 215 @type tag: str 216 @param attrs: dictionary of XML attributes for this export tag 217 @type attrs: dict 218 @param str: string value contained by tag, if any 219 @type str: str 220 """ 221 self.tag = tag 222 self.attrs = attrs 223 self.str = str
224
225 - def get(self, attr):
226 """ 227 @return: value of attribute or None if attribute not set 228 @rtype: str 229 """ 230 return self.attrs.get(attr, None)
231 - def xml(self):
232 """ 233 @return: export instance represented as manifest XML 234 @rtype: str 235 """ 236 attrs = ' '.join([' %s="%s"'%(k,v) for k,v in self.attrs.items()]) #py3k 237 if self.str: 238 return '<%s%s>%s</%s>'%(self.tag, attrs, self.str, self.tag) 239 else: 240 return '<%s%s />'%(self.tag, attrs)
241
242 -class Platform(object):
243 """ 244 Manifest 'platform' tag 245 """ 246 __slots__ = ['os', 'version', 'notes'] 247
248 - def __init__(self, os, version, notes=None):
249 """ 250 Create new depend instance. 251 @param os: OS name. must be non-empty 252 @type os: str 253 @param version: OS version. must be non-empty 254 @type version: str 255 @param notes: (optional) notes about platform support 256 @type notes: str 257 """ 258 if not os: 259 raise ValueError("bad 'os' attribute") 260 if not version: 261 raise ValueError("bad 'version' attribute") 262 self.os = os 263 self.version = version 264 self.notes = notes
265
266 - def __str__(self):
267 return "%s %s"%(self.os, self.version)
268 - def __repr__(self):
269 return "%s %s"%(self.os, self.version)
270 - def __eq__(self, obj):
271 """ 272 Override equality test. notes *are* considered in the equality test. 273 """ 274 if not isinstance(obj, Platform): 275 return False 276 return self.os == obj.os and self.version == obj.version and self.notes == obj.notes
277 - def xml(self):
278 """ 279 @return: instance represented as manifest XML 280 @rtype: str 281 """ 282 if self.notes is not None: 283 return '<platform os="%s" version="%s" notes="%s"/>'%(self.os, self.version, self.notes) 284 else: 285 return '<platform os="%s" version="%s"/>'%(self.os, self.version)
286
287 -class Depend(object):
288 """ 289 Manifest 'depend' tag 290 """ 291 __slots__ = ['package'] 292
293 - def __init__(self, package):
294 """ 295 Create new depend instance. 296 @param package: package name. must be non-empty 297 @type package: str 298 """ 299 if not package: 300 raise ValueError("bad 'package' attribute") 301 self.package = package
302 - def __str__(self):
303 return self.package
304 - def __repr__(self):
305 return self.package
306 - def __eq__(self, obj):
307 if not isinstance(obj, Depend): 308 return False 309 return self.package == obj.package
310 - def xml(self):
311 """ 312 @return: depend instance represented as manifest XML 313 @rtype: str 314 """ 315 return '<depend package="%s" />'%self.package
316
317 -class StackDepend(object):
318 """ 319 Stack Manifest 'depend' tag 320 """ 321 __slots__ = ['stack', 'annotation'] 322
323 - def __init__(self, stack):
324 """ 325 @param stack: stack name. must be non-empty 326 @type stack: str 327 """ 328 if not stack: 329 raise ValueError("bad 'stack' attribute") 330 self.stack = stack 331 self.annotation = None
332
333 - def __str__(self):
334 return self.stack
335 - def __repr__(self):
336 return self.stack
337 - def __eq__(self, obj):
338 if not isinstance(obj, StackDepend): 339 return False 340 return self.stack == obj.stack
341 - def xml(self):
342 """ 343 @return: stack depend instance represented as stack manifest XML 344 @rtype: str 345 """ 346 if self.annotation: 347 return '<depend stack="%s" /> <!-- %s -->'%(self.stack, self.annotation) 348 else: 349 return '<depend stack="%s" />'%self.stack
350
351 -class ROSDep(object):
352 """ 353 Manifest 'rosdep' tag 354 """ 355 __slots__ = ['name',] 356
357 - def __init__(self, name):
358 """ 359 Create new rosdep instance. 360 @param name: dependency name. Must be non-empty. 361 @type name: str 362 """ 363 if not name: 364 raise ValueError("bad 'name' attribute") 365 self.name = name
366 - def xml(self):
367 """ 368 @return: rosdep instance represented as manifest XML 369 @rtype: str 370 """ 371 return '<rosdep name="%s" />'%self.name
372
373 -class VersionControl(object):
374 """ 375 Manifest 'versioncontrol' tag 376 """ 377 __slots__ = ['type', 'url'] 378
379 - def __init__(self, type_, url):
380 """ 381 @param type_: version control type (e.g. 'svn'). must be non empty 382 @type type_: str 383 @param url: URL associated with version control. must be non empty 384 @type url: str 385 """ 386 def is_string_type(obj): 387 try: 388 return isinstance(obj, basestring) 389 except NameError: 390 return isinstance(obj, str)
391 392 if not type_ or not is_string_type(type_): 393 raise ValueError("bad 'type' attribute") 394 if not url is None and not is_string_type(url): 395 raise ValueError("bad 'url' attribute") 396 self.type = type_ 397 self.url = url
398 - def xml(self):
399 """ 400 @return: versioncontrol instance represented as manifest XML 401 @rtype: str 402 """ 403 if self.url: 404 return '<versioncontrol type="%s" url="%s" />'%(self.type, self.url) 405 else: 406 return '<versioncontrol type="%s" />'%self.type
407
408 -class _Manifest(object):
409 """ 410 Object representation of a ROS manifest file 411 """ 412 __slots__ = ['description', 'brief', \ 413 'author', 'license', 'license_url', 'url', \ 414 'depends', 'rosdeps','platforms',\ 415 'logo', 'exports', 'version',\ 416 'versioncontrol', 'status', 'notes',\ 417 'unknown_tags',\ 418 '_type']
419 - def __init__(self, _type='package'):
420 self.description = self.brief = self.author = \ 421 self.license = self.license_url = \ 422 self.url = self.logo = self.status = \ 423 self.version = self.notes = '' 424 self.depends = [] 425 self.rosdeps = [] 426 self.exports = [] 427 self.platforms = [] 428 self._type = _type 429 430 # store unrecognized tags during parsing 431 self.unknown_tags = []
432
433 - def __str__(self):
434 return self.xml()
435 - def get_export(self, tag, attr):
436 """ 437 @return: exports that match the specified tag and attribute, e.g. 'python', 'path' 438 @rtype: [L{Export}] 439 """ 440 return [e.get(attr) for e in self.exports if e.tag == tag if e.get(attr) is not None]
441 - def xml(self):
442 """ 443 @return: Manifest instance as ROS XML manifest 444 @rtype: str 445 """ 446 if not self.brief: 447 desc = " <description>%s</description>"%self.description 448 else: 449 desc = ' <description brief="%s">%s</description>'%(self.brief, self.description) 450 author = " <author>%s</author>"%self.author 451 if self.license_url: 452 license = ' <license url="%s">%s</license>'%(self.license_url, self.license) 453 else: 454 license = " <license>%s</license>"%self.license 455 versioncontrol = url = logo = exports = version = "" 456 if self.url: 457 url = " <url>%s</url>"%self.url 458 if self.version: 459 version = " <version>%s</version>"%self.version 460 if self.logo: 461 logo = " <logo>%s</logo>"%self.logo 462 depends = '\n'.join([" %s"%d.xml() for d in self.depends]) 463 rosdeps = '\n'.join([" %s"%rd.xml() for rd in self.rosdeps]) 464 platforms = '\n'.join([" %s"%p.xml() for p in self.platforms]) 465 if self.exports: 466 exports = ' <export>\n' + '\n'.join([" %s"%e.xml() for e in self.exports]) + ' </export>' 467 if self.versioncontrol: 468 versioncontrol = " %s"%self.versioncontrol.xml() 469 if self.status or self.notes: 470 review = ' <review status="%s" notes="%s" />'%(self.status, self.notes) 471 472 473 fields = filter(lambda x: x, 474 [desc, author, license, review, url, logo, depends, 475 rosdeps, platforms, exports, versioncontrol, version]) 476 return "<%s>\n"%self._type + "\n".join(fields) + "\n</%s>"%self._type
477
478 -def _get_text(nodes):
479 """ 480 DOM utility routine for getting contents of text nodes 481 """ 482 return "".join([n.data for n in nodes if n.nodeType == n.TEXT_NODE])
483
484 -def parse_file(m, file):
485 """ 486 Parse manifest file (package, stack) 487 @param m: field to populate 488 @type m: L{_Manifest} 489 @param file: manifest.xml file path 490 @type file: str 491 @return: return m, populated with parsed fields 492 @rtype: L{_Manifest} 493 """ 494 if not file: 495 raise ValueError("Missing manifest file argument") 496 if not os.path.isfile(file): 497 raise ValueError("Invalid/non-existent manifest file: %s"%file) 498 with open(file, 'r') as f: 499 text = f.read() 500 try: 501 return parse(m, text, file) 502 except ManifestException as e: 503 raise ManifestException("Invalid manifest file [%s]: %s"%(os.path.abspath(file), e))
504
505 -def parse(m, string, filename='string'):
506 """ 507 Parse manifest.xml string contents 508 @param string: manifest.xml contents 509 @type string: str 510 @param m: field to populate 511 @type m: L{_Manifest} 512 @return: return m, populated with parsed fields 513 @rtype: L{_Manifest} 514 """ 515 try: 516 d = dom.parseString(string) 517 except Exception as e: 518 raise ManifestException("invalid XML: %s"%e) 519 520 p = get_nodes_by_name(d, m._type) 521 if len(p) != 1: 522 raise ManifestException("manifest must have a single '%s' element"%m._type) 523 p = p[0] 524 m.description = check('description')(p, filename) 525 m.brief = '' 526 try: 527 tag = get_nodes_by_name(p, 'description')[0] 528 m.brief = tag.getAttribute('brief') or '' 529 except: 530 # means that 'description' tag is missing 531 pass 532 #TODO: figure out how to multiplex 533 if m._type == 'package': 534 m.depends = check_depends('depend')(p, filename) 535 elif m._type == 'stack': 536 m.depends = check_stack_depends('depend')(p, filename) 537 elif m._type == 'app': 538 # not implemented yet 539 pass 540 m.rosdeps = check('rosdep')(p, filename) 541 m.platforms = check('platform')(p, filename) 542 m.exports = check('export')(p, filename) 543 m.versioncontrol = check('versioncontrol')(p,filename) 544 m.license = check('license')(p, filename) 545 m.license_url = '' 546 try: 547 tag = get_nodes_by_name(p, 'license')[0] 548 m.license_url = tag.getAttribute('url') or '' 549 except: 550 pass #manifest is missing required 'license' tag 551 552 m.status='unreviewed' 553 try: 554 tag = get_nodes_by_name(p, 'review')[0] 555 m.status=tag.getAttribute('status') or '' 556 except: 557 pass #manifest is missing optional 'review status' tag 558 559 m.notes='' 560 try: 561 tag = get_nodes_by_name(p, 'review')[0] 562 m.notes=tag.getAttribute('notes') or '' 563 except: 564 pass #manifest is missing optional 'review notes' tag 565 566 m.author = check('author', True)(p, filename) 567 m.url = check('url')(p, filename) 568 m.version = check('version')(p, filename) 569 m.logo = check('logo')(p, filename) 570 571 # do some validation on what we just parsed 572 if m._type == 'stack': 573 if m.exports: 574 raise ManifestException("stack manifests are not allowed to have exports") 575 if m.rosdeps: 576 raise ManifestException("stack manifests are not allowed to have rosdeps") 577 578 # store unrecognized tags 579 m.unknown_tags = [e for e in p.childNodes if e.nodeType == e.ELEMENT_NODE and e.tagName not in VALID] 580 return m
581