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 if not type_ or not isinstance(type_, basestring): 387 raise ValueError("bad 'type' attribute") 388 if not url is None and not isinstance(url, basestring): 389 raise ValueError("bad 'url' attribute") 390 self.type = type_ 391 self.url = url
392 - def xml(self):
393 """ 394 @return: versioncontrol instance represented as manifest XML 395 @rtype: str 396 """ 397 if self.url: 398 return '<versioncontrol type="%s" url="%s" />'%(self.type, self.url) 399 else: 400 return '<versioncontrol type="%s" />'%self.type
401
402 -class _Manifest(object):
403 """ 404 Object representation of a ROS manifest file 405 """ 406 __slots__ = ['description', 'brief', \ 407 'author', 'license', 'license_url', 'url', \ 408 'depends', 'rosdeps','platforms',\ 409 'logo', 'exports', 'version',\ 410 'versioncontrol', 'status', 'notes',\ 411 'unknown_tags',\ 412 '_type']
413 - def __init__(self, _type='package'):
414 self.description = self.brief = self.author = \ 415 self.license = self.license_url = \ 416 self.url = self.logo = self.status = \ 417 self.version = self.notes = '' 418 self.depends = [] 419 self.rosdeps = [] 420 self.exports = [] 421 self.platforms = [] 422 self._type = _type 423 424 # store unrecognized tags during parsing 425 self.unknown_tags = []
426
427 - def __str__(self):
428 return self.xml()
429 - def get_export(self, tag, attr):
430 """ 431 @return: exports that match the specified tag and attribute, e.g. 'python', 'path' 432 @rtype: [L{Export}] 433 """ 434 return [e.get(attr) for e in self.exports if e.tag == tag if e.get(attr) is not None]
435 - def xml(self):
436 """ 437 @return: Manifest instance as ROS XML manifest 438 @rtype: str 439 """ 440 if not self.brief: 441 desc = " <description>%s</description>"%self.description 442 else: 443 desc = ' <description brief="%s">%s</description>'%(self.brief, self.description) 444 author = " <author>%s</author>"%self.author 445 if self.license_url: 446 license = ' <license url="%s">%s</license>'%(self.license_url, self.license) 447 else: 448 license = " <license>%s</license>"%self.license 449 versioncontrol = url = logo = exports = version = "" 450 if self.url: 451 url = " <url>%s</url>"%self.url 452 if self.version: 453 version = " <version>%s</version>"%self.version 454 if self.logo: 455 logo = " <logo>%s</logo>"%self.logo 456 depends = '\n'.join([" %s"%d.xml() for d in self.depends]) 457 rosdeps = '\n'.join([" %s"%rd.xml() for rd in self.rosdeps]) 458 platforms = '\n'.join([" %s"%p.xml() for p in self.platforms]) 459 if self.exports: 460 exports = ' <export>\n' + '\n'.join([" %s"%e.xml() for e in self.exports]) + ' </export>' 461 if self.versioncontrol: 462 versioncontrol = " %s"%self.versioncontrol.xml() 463 if self.status or self.notes: 464 review = ' <review status="%s" notes="%s" />'%(self.status, self.notes) 465 466 467 fields = filter(lambda x: x, 468 [desc, author, license, review, url, logo, depends, 469 rosdeps, platforms, exports, versioncontrol, version]) 470 return "<%s>\n"%self._type + "\n".join(fields) + "\n</%s>"%self._type
471
472 -def _get_text(nodes):
473 """ 474 DOM utility routine for getting contents of text nodes 475 """ 476 return "".join([n.data for n in nodes if n.nodeType == n.TEXT_NODE])
477
478 -def parse_file(m, file):
479 """ 480 Parse manifest file (package, stack) 481 @param m: field to populate 482 @type m: L{_Manifest} 483 @param file: manifest.xml file path 484 @type file: str 485 @return: return m, populated with parsed fields 486 @rtype: L{_Manifest} 487 """ 488 if not file: 489 raise ValueError("Missing manifest file argument") 490 if not os.path.isfile(file): 491 raise ValueError("Invalid/non-existent manifest file: %s"%file) 492 with open(file, 'r') as f: 493 text = f.read() 494 try: 495 return parse(m, text, file) 496 except ManifestException as e: 497 raise ManifestException("Invalid manifest file [%s]: %s"%(os.path.abspath(file), e))
498
499 -def parse(m, string, filename='string'):
500 """ 501 Parse manifest.xml string contents 502 @param string: manifest.xml contents 503 @type string: str 504 @param m: field to populate 505 @type m: L{_Manifest} 506 @return: return m, populated with parsed fields 507 @rtype: L{_Manifest} 508 """ 509 try: 510 d = dom.parseString(string) 511 except Exception as e: 512 raise ManifestException("invalid XML: %s"%e) 513 514 p = get_nodes_by_name(d, m._type) 515 if len(p) != 1: 516 raise ManifestException("manifest must have a single '%s' element"%m._type) 517 p = p[0] 518 m.description = check('description')(p, filename) 519 m.brief = '' 520 try: 521 tag = get_nodes_by_name(p, 'description')[0] 522 m.brief = tag.getAttribute('brief') or '' 523 except: 524 # means that 'description' tag is missing 525 pass 526 #TODO: figure out how to multiplex 527 if m._type == 'package': 528 m.depends = check_depends('depend')(p, filename) 529 elif m._type == 'stack': 530 m.depends = check_stack_depends('depend')(p, filename) 531 elif m._type == 'app': 532 # not implemented yet 533 pass 534 m.rosdeps = check('rosdep')(p, filename) 535 m.platforms = check('platform')(p, filename) 536 m.exports = check('export')(p, filename) 537 m.versioncontrol = check('versioncontrol')(p,filename) 538 m.license = check('license')(p, filename) 539 m.license_url = '' 540 try: 541 tag = get_nodes_by_name(p, 'license')[0] 542 m.license_url = tag.getAttribute('url') or '' 543 except: 544 pass #manifest is missing required 'license' tag 545 546 m.status='unreviewed' 547 try: 548 tag = get_nodes_by_name(p, 'review')[0] 549 m.status=tag.getAttribute('status') or '' 550 except: 551 pass #manifest is missing optional 'review status' tag 552 553 m.notes='' 554 try: 555 tag = get_nodes_by_name(p, 'review')[0] 556 m.notes=tag.getAttribute('notes') or '' 557 except: 558 pass #manifest is missing optional 'review notes' tag 559 560 m.author = check('author', True)(p, filename) 561 m.url = check('url')(p, filename) 562 m.version = check('version')(p, filename) 563 m.logo = check('logo')(p, filename) 564 565 # do some validation on what we just parsed 566 if m._type == 'stack': 567 if m.exports: 568 raise ManifestException("stack manifests are not allowed to have exports") 569 if m.rosdeps: 570 raise ManifestException("stack manifests are not allowed to have rosdeps") 571 572 # store unrecognized tags 573 m.unknown_tags = [e for e in p.childNodes if e.nodeType == e.ELEMENT_NODE and e.tagName not in VALID] 574 return m
575